diff --git a/TODO.md b/TODO.md index f1a1f3fd..834265d9 100644 --- a/TODO.md +++ b/TODO.md @@ -98,4 +98,8 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * mail backend related_models = ('resources__content_type') ?? * ignore orders -* Redmine, BSCW and other applications management +* Dropdown menu for Account services/management object-tools + +* Domain backend PowerDNS Bind validation support? + +* Maildir billing tests/ webdisk billing tests (avg metric) diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index eb470e3f..fdf8c534 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -3,8 +3,9 @@ from django.conf.urls import patterns, url from django.contrib import admin from django.contrib.admin.utils import unquote from django.forms.models import BaseInlineFormSet +from django.shortcuts import render, redirect -from .utils import set_url_query, action_to_view +from .utils import set_url_query, action_to_view, wrap_admin_view class ChangeListDefaultFilter(object): @@ -129,3 +130,58 @@ class ChangeAddFieldsMixin(object): class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.ModelAdmin): pass + + +class SelectPluginAdminMixin(object): + plugin = None + plugin_field = None + + def get_form(self, request, obj=None, **kwargs): + if obj: + self.form = obj.method_class().get_form() + else: + self.form = self.plugin.get_plugin(self.plugin_value)().get_form() + return super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs) + + def get_urls(self): + """ Hooks select account url """ + urls = super(SelectPluginAdminMixin, self).get_urls() + opts = self.model._meta + info = opts.app_label, opts.model_name + select_urls = patterns("", + url("/select-plugin/$", + wrap_admin_view(self, self.select_plugin_view), + name='%s_%s_select_plugin' % info), + ) + return select_urls + urls + + def select_plugin_view(self, request): + opts = self.model._meta + context = { + 'opts': opts, + 'app_label': opts.app_label, + 'field': self.plugin_field, + 'field_name': opts.get_field_by_name(self.plugin_field)[0].verbose_name, + 'plugins': self.plugin.get_plugin_choices(), + } + template = 'admin/orchestra/select_plugin.html' + return render(request, template, context) + + def add_view(self, request, form_url='', extra_context=None): + """ Redirects to select account view if required """ + if request.user.is_superuser: + plugin_value = request.GET.get(self.plugin_field) + if plugin_value or self.plugin.get_plugins() == 1: + self.plugin_value = plugin_value + if not plugin_value: + self.plugin_value = self.plugin.get_plugins()[0] + return super(SelectPluginAdminMixin, self).add_view(request, + form_url=form_url, extra_context=extra_context) + # TODO add plugin name on title + return redirect('./select-plugin/?%s' % request.META['QUERY_STRING']) + + def save_model(self, request, obj, form, change): + if not change: + setattr(obj, self.plugin_field, self.plugin_value) + obj.save() + diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index 18335ef5..4ce329b0 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -1,11 +1,9 @@ -from django.conf.urls import patterns, url from django.contrib import admin from django.core.urlresolvers import reverse -from django.shortcuts import render, redirect from django.utils.translation import ugettext_lazy as _ -from orchestra.admin import ChangeViewActionsMixin -from orchestra.admin.utils import admin_colored, admin_link, wrap_admin_view +from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin +from orchestra.admin.utils import admin_colored, admin_link from orchestra.apps.accounts.admin import AccountAdminMixin from . import actions @@ -22,55 +20,11 @@ STATE_COLORS = { } -class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin): +class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): list_display = ('label', 'method', 'number', 'account_link', 'is_active') list_filter = ('method', 'is_active') - - def get_form(self, request, obj=None, **kwargs): - if obj: - self.form = obj.method_class().get_form() - else: - self.form = PaymentMethod.get_plugin(self.method)().get_form() - return super(PaymentSourceAdmin, self).get_form(request, obj=obj, **kwargs) - - def get_urls(self): - """ Hooks select account url """ - urls = super(PaymentSourceAdmin, self).get_urls() - opts = self.model._meta - info = opts.app_label, opts.model_name - select_urls = patterns("", - url("/select-method/$", - wrap_admin_view(self, self.select_method_view), - name='%s_%s_select_method' % info), - ) - return select_urls + urls - - def select_method_view(self, request): - opts = self.model._meta - context = { - 'opts': opts, - 'app_label': opts.app_label, - 'methods': PaymentMethod.get_plugin_choices(), - } - template = 'admin/payments/payment_source/select_method.html' - return render(request, template, context) - - def add_view(self, request, form_url='', extra_context=None): - """ Redirects to select account view if required """ - if request.user.is_superuser: - method = request.GET.get('method') - if method or PaymentMethod.get_plugins() == 1: - self.method = method - if not method: - self.method = PaymentMethod.get_plugins()[0] - return super(PaymentSourceAdmin, self).add_view(request, - form_url=form_url, extra_context=extra_context) - return redirect('./select-method/?%s' % request.META['QUERY_STRING']) - - def save_model(self, request, obj, form, change): - if not change: - obj.method = self.method - obj.save() + plugin = PaymentMethod + plugin_field = 'method' class TransactionInline(admin.TabularInline): diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py index 8f9e1589..49e61ba6 100644 --- a/orchestra/apps/payments/methods/options.py +++ b/orchestra/apps/payments/methods/options.py @@ -44,7 +44,7 @@ class PaymentMethod(plugins.Plugin): class PaymentSourceDataForm(forms.ModelForm): class Meta: - exclude = ('data',) # TODO add 'method' + exclude = ('data', 'method') def __init__(self, *args, **kwargs): super(PaymentSourceDataForm, self).__init__(*args, **kwargs) diff --git a/orchestra/apps/saas/__init__.py b/orchestra/apps/saas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/saas/admin.py b/orchestra/apps/saas/admin.py new file mode 100644 index 00000000..8856c47a --- /dev/null +++ b/orchestra/apps/saas/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from orchestra.admin import SelectPluginAdminMixin +from orchestra.apps.accounts.admin import AccountAdminMixin + +from .models import SaaS +from .services import SoftwareService + + +class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): + list_display = ('id', 'service', 'account_link') + list_filter = ('service',) + plugin = SoftwareService + plugin_field = 'service' + + +admin.site.register(SaaS, SaaSAdmin) diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py new file mode 100644 index 00000000..6a3a938a --- /dev/null +++ b/orchestra/apps/saas/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField + +from orchestra.core import services + +from .services import SoftwareService + + +class SaaS(models.Model): + service = models.CharField(_("service"), max_length=32, + choices=SoftwareService.get_plugin_choices()) + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='saas') + data = JSONField(_("data")) + + class Meta: + verbose_name = "SaaS" + verbose_name_plural = "SaaS" + + +services.register(SaaS) diff --git a/orchestra/apps/saas/services/__init__.py b/orchestra/apps/saas/services/__init__.py new file mode 100644 index 00000000..4720b7c2 --- /dev/null +++ b/orchestra/apps/saas/services/__init__.py @@ -0,0 +1 @@ +from .options import SoftwareService diff --git a/orchestra/apps/saas/services/bscw.py b/orchestra/apps/saas/services/bscw.py new file mode 100644 index 00000000..7cc4cced --- /dev/null +++ b/orchestra/apps/saas/services/bscw.py @@ -0,0 +1,13 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .options import SoftwareService, SoftwareServiceForm + + +class BSCWForm(SoftwareServiceForm): + quota = forms.IntegerField(label=_("Quota")) + + +class BSCWService(SoftwareService): + verbose_name = "BSCW" + form = BSCWForm diff --git a/orchestra/apps/saas/services/gitlab.py b/orchestra/apps/saas/services/gitlab.py new file mode 100644 index 00000000..9aa5b57c --- /dev/null +++ b/orchestra/apps/saas/services/gitlab.py @@ -0,0 +1,13 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .options import SoftwareService, SoftwareServiceForm + + +class GitLabForm(SoftwareServiceForm): + project_name = forms.CharField(label=_("Project name"), max_length=64) + + +class GitLabService(SoftwareService): + verbose_name = "GitLab" + form = GitLabForm diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py new file mode 100644 index 00000000..49999bca --- /dev/null +++ b/orchestra/apps/saas/services/options.py @@ -0,0 +1,38 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils import plugins +from orchestra.utils.functional import cached +from orchestra.utils.python import import_class + +from .. import settings + + +class SoftwareServiceForm(forms.ModelForm): + username = forms.CharField(label=_("Username"), max_length=64) + password = forms.CharField(label=_("Password"), max_length=64) + + class Meta: + exclude = ('data', 'service') + + +class SoftwareService(plugins.Plugin): + label_field = 'label' + form = SoftwareServiceForm + serializer = None + + @classmethod + @cached + def get_plugins(cls): + plugins = [] + for cls in settings.SAAS_ENABLED_SERVICES: + plugins.append(import_class(cls)) + return plugins + + def get_form(self): + self.form.plugin = self + return self.form + + def get_serializer(self): + self.serializer.plugin = self + return self.serializer diff --git a/orchestra/apps/saas/settings.py b/orchestra/apps/saas/settings.py new file mode 100644 index 00000000..081564b1 --- /dev/null +++ b/orchestra/apps/saas/settings.py @@ -0,0 +1,7 @@ +from django.conf import settings + + +SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', ( + 'orchestra.apps.saas.services.bscw.BSCWService', + 'orchestra.apps.saas.services.gitlab.GitLabService', +)) diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index e8dec4f1..c9174c72 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -31,6 +31,9 @@ class ContractedPlan(models.Model): account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='plans') + class Meta: + verbose_name_plural = _("plans") + def __unicode__(self): return str(self.plan) diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index ec753252..f9e67fe4 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -78,6 +78,7 @@ INSTALLED_APPS = ( 'orchestra.apps.websites', 'orchestra.apps.databases', 'orchestra.apps.vps', + 'orchestra.apps.saas', 'orchestra.apps.issues', 'orchestra.apps.services', 'orchestra.apps.orders', @@ -184,6 +185,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'databases/databaseuser': 'postgresql.png', 'vps/vps': 'TuxBox.png', 'miscellaneous/miscellaneous': 'applications-other.png', + 'saas/saas': 'saas.png', # Accounts 'accounts/account': 'Face-monkey.png', 'contacts/contact': 'contact_book.png', diff --git a/orchestra/static/orchestra/icons/saas.png b/orchestra/static/orchestra/icons/saas.png new file mode 100644 index 00000000..31ea2dd1 Binary files /dev/null and b/orchestra/static/orchestra/icons/saas.png differ diff --git a/orchestra/static/orchestra/icons/saas.svg b/orchestra/static/orchestra/icons/saas.svg new file mode 100644 index 00000000..5eb15d98 --- /dev/null +++ b/orchestra/static/orchestra/icons/saas.svg @@ -0,0 +1,109 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/payments/templates/admin/payments/payment_source/select_method.html b/orchestra/templates/admin/orchestra/select_plugin.html similarity index 55% rename from orchestra/apps/payments/templates/admin/payments/payment_source/select_method.html rename to orchestra/templates/admin/orchestra/select_plugin.html index b274be31..8604e36b 100644 --- a/orchestra/apps/payments/templates/admin/payments/payment_source/select_method.html +++ b/orchestra/templates/admin/orchestra/select_plugin.html @@ -3,13 +3,13 @@ {% block content %} -

Select a method for the new payment source

+

Select a {{ field_name }} for the new {{ opts.object_name }} instance

{% csrf_token %}