From eec726fcc6ccb29b706de72244808abdfb1b782b Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 26 Sep 2014 19:21:09 +0000 Subject: [PATCH] Added SaaS application --- TODO.md | 6 +- orchestra/admin/options.py | 58 +++++++++- orchestra/apps/payments/admin.py | 56 +-------- orchestra/apps/payments/methods/options.py | 2 +- orchestra/apps/saas/__init__.py | 0 orchestra/apps/saas/admin.py | 17 +++ orchestra/apps/saas/models.py | 21 ++++ orchestra/apps/saas/services/__init__.py | 1 + orchestra/apps/saas/services/bscw.py | 13 +++ orchestra/apps/saas/services/gitlab.py | 13 +++ orchestra/apps/saas/services/options.py | 38 ++++++ orchestra/apps/saas/settings.py | 7 ++ orchestra/apps/services/models.py | 3 + orchestra/conf/base_settings.py | 2 + orchestra/static/orchestra/icons/saas.png | Bin 0 -> 2214 bytes orchestra/static/orchestra/icons/saas.svg | 109 ++++++++++++++++++ .../admin/orchestra/select_plugin.html} | 6 +- 17 files changed, 295 insertions(+), 57 deletions(-) create mode 100644 orchestra/apps/saas/__init__.py create mode 100644 orchestra/apps/saas/admin.py create mode 100644 orchestra/apps/saas/models.py create mode 100644 orchestra/apps/saas/services/__init__.py create mode 100644 orchestra/apps/saas/services/bscw.py create mode 100644 orchestra/apps/saas/services/gitlab.py create mode 100644 orchestra/apps/saas/services/options.py create mode 100644 orchestra/apps/saas/settings.py create mode 100644 orchestra/static/orchestra/icons/saas.png create mode 100644 orchestra/static/orchestra/icons/saas.svg rename orchestra/{apps/payments/templates/admin/payments/payment_source/select_method.html => templates/admin/orchestra/select_plugin.html} (55%) 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 0000000000000000000000000000000000000000..31ea2dd1c62d0fb8ce8610f566078d018b3f447d GIT binary patch literal 2214 zcmV;X2wC@uP)@2(+4p0u&9cli1X;O>|w$T-WPk2m+f1C0Uvbs5S~~(gz8Y z);a}HX!0N6DW}8r zUUJkDL&5@VcB>KC3OqydLr(>E0b867S6`C0F_Dk}o86iR93?nZMuStp_ni(`>nQz% z5m$iCZq);S1s)iqI{yuP!|8DO#===5L*ZwP9eIG+z>cwU7PALzcB>iq3`iTR3I`ww zu3ojkQs9R`31A?&g1rSeoDNqYo}+jIb^|{d1H#eZ2Cf1#HS#|NzU_3l+G070H-sJ@ z0kK43Mw>hYd*q1CZk-g%iKYOX-I@d3IWlhANJ&Wv$Bjm#H%@U2!8xdjSUpggFslS$ zGMQMvZavR$ejZbSiPEX1{9@Nj#;Vm-y(uXv{uriqo83BID>K$sVoU`T2o=jo@O20&F+);+Uskch0zELN>r#gyVH09?O*oozd| zk(HIjhV>gLEG%Su&32B}9i#S8tyVQgt&CxFz-G6m+U(X{1iLLN7B5=N+`H!jV6j+k zI#U!Z77L1^aQ}k)nLlqn=^5$Fshq>OapM3ua`Xt@-QBp|Zkk(~0m#Y8VZnV1w5kI* zrj;@1Jz%q2OMy3lSy~3m9$Ci24+U4(*0xsem~;mQgCU&X)6+wLe?QsT*#OiYs>NtD zGHu#4%oELcJRV;D&CBHG=Cb-nt1%c1w6wI)+|taU!-t|(`H9ovDvc|^X1C4;>VZtH z!j>Wnn>KFJ%8rVzt}dS2^c-1PS?t`gb6EcQ&(E`M`?gpvnt`7JZ#o^WVQ*b!fc!YX z6^lH;x%1~}Y-${q-s$PYebF6t0=xd~LRD2JTP6=G@AC!sa)lEL(d%QLtELki6?Hbd zb;i)lkmYZI;#ginaJ${%xResd8xHE~>rhpdjEoGd%dOn=&3nRSIyyT5INN%bhK2_6 z^77d8yFKKL&tdJ_wba+w69~lJ@&L>O{$;aUUjnL~4woN5tXR2x67V@UkAzWC^>`H( z6&0kXrH9iR-f!U5KfXGsoT4bau;m3NPMjDPdHM2X_8-{K$&)7smDTBViIN|R8mGgx z1%g9pWjyi#X0v$^d4N+No*Gt8RaH)(IURl$LQq&(NNLINc{oDy02^&~>s=5OU{AzC zRn=i>2E8FlNk}!O4ogemWt9wcz#nXOYpSk%M%k}P;9%qNc|hIxvi~@+QYTzbUHH9Gg$h)rQvq~z{cv0baZquwPY%5)~rDh3a{;Z4NH-Q zh2L68dwV-I+iNtpAaU@^r0#_b1_MjJvt&@(W@e6*Jix4^u16-ACXkz(n^X;D)63B7^`xcWJe9iLZaO%b4Rsg0%N_Ta4apA%R=FFLc*X!l&ci$#6Ba`vtZ*JF{o11BDYNWWhn4X><_P(;0 zXE!_>-s>EzJI3x`?WX=8^~{<%3zNyjgAY7N$&?bB{@IkQfG_mGVW2!wi_>RL^Wx4I zxp?U!eSLlO`}+rN==M4G(ca$9i4!OA_xn**m8{IHaQ@Y+SAk&5U%7Gxv)K#;C;td= zT~|J%4B)Y3tUMl1cn$CB>Y}8ign$~Lt*wnCM~?vD?eWsv+l$ZV<813${C+=0Un{~= zWI+hQqD70CSuqoz&&Pp-2RK-Jkgo2oWCiRMD^@N~0X_hR?K8#_`T6*%RQhY*5!_s+v?HsgHLNm^2$ch5K>Bn5K4imAZ7l2 z_iohbbkziJ$o^m9_51y+-g)aU2c?t&L2x6d3x&lGExao`JNx%Sh=jJ+|4T?I+q%2D z?S~Kj?L(j+2}?z2%!ogR3T6WLPdyx5JCz;Kp^~HulIxZj@F$70t2BxA|43M0TCh`Cny@v z%8(Fhm_ZFW5Y!fnlXZK3a>1*7C^K zkNor+PlT435b>x)6Z*(EASOgIa>xjc5J^6Sp(50X-=QJ`1V?sUa!G$D4jTqVg@|g2 o7>Y>pk#mAX$VTOHyZ_zhzn+RO=;q<)P5=M^07*qoM6N<$f| + + + + + + + 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 %}