Added SaaS application
This commit is contained in:
parent
99b14f9c9f
commit
eec726fcc6
6
TODO.md
6
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') ??
|
* mail backend related_models = ('resources__content_type') ??
|
||||||
* ignore orders
|
* 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)
|
||||||
|
|
|
@ -3,8 +3,9 @@ from django.conf.urls import patterns, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.forms.models import BaseInlineFormSet
|
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):
|
class ChangeListDefaultFilter(object):
|
||||||
|
@ -129,3 +130,58 @@ class ChangeAddFieldsMixin(object):
|
||||||
|
|
||||||
class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.ModelAdmin):
|
class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.ModelAdmin):
|
||||||
pass
|
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()
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
from django.conf.urls import patterns, url
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.shortcuts import render, redirect
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin import ChangeViewActionsMixin
|
from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin
|
||||||
from orchestra.admin.utils import admin_colored, admin_link, wrap_admin_view
|
from orchestra.admin.utils import admin_colored, admin_link
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
|
|
||||||
from . import actions
|
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_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
||||||
list_filter = ('method', 'is_active')
|
list_filter = ('method', 'is_active')
|
||||||
|
plugin = PaymentMethod
|
||||||
def get_form(self, request, obj=None, **kwargs):
|
plugin_field = 'method'
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionInline(admin.TabularInline):
|
class TransactionInline(admin.TabularInline):
|
||||||
|
|
|
@ -44,7 +44,7 @@ class PaymentMethod(plugins.Plugin):
|
||||||
|
|
||||||
class PaymentSourceDataForm(forms.ModelForm):
|
class PaymentSourceDataForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
exclude = ('data',) # TODO add 'method'
|
exclude = ('data', 'method')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(PaymentSourceDataForm, self).__init__(*args, **kwargs)
|
super(PaymentSourceDataForm, self).__init__(*args, **kwargs)
|
||||||
|
|
0
orchestra/apps/saas/__init__.py
Normal file
0
orchestra/apps/saas/__init__.py
Normal file
17
orchestra/apps/saas/admin.py
Normal file
17
orchestra/apps/saas/admin.py
Normal file
|
@ -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)
|
21
orchestra/apps/saas/models.py
Normal file
21
orchestra/apps/saas/models.py
Normal file
|
@ -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)
|
1
orchestra/apps/saas/services/__init__.py
Normal file
1
orchestra/apps/saas/services/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .options import SoftwareService
|
13
orchestra/apps/saas/services/bscw.py
Normal file
13
orchestra/apps/saas/services/bscw.py
Normal file
|
@ -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
|
13
orchestra/apps/saas/services/gitlab.py
Normal file
13
orchestra/apps/saas/services/gitlab.py
Normal file
|
@ -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
|
38
orchestra/apps/saas/services/options.py
Normal file
38
orchestra/apps/saas/services/options.py
Normal file
|
@ -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
|
7
orchestra/apps/saas/settings.py
Normal file
7
orchestra/apps/saas/settings.py
Normal file
|
@ -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',
|
||||||
|
))
|
|
@ -31,6 +31,9 @@ class ContractedPlan(models.Model):
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='plans')
|
related_name='plans')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = _("plans")
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return str(self.plan)
|
return str(self.plan)
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,7 @@ INSTALLED_APPS = (
|
||||||
'orchestra.apps.websites',
|
'orchestra.apps.websites',
|
||||||
'orchestra.apps.databases',
|
'orchestra.apps.databases',
|
||||||
'orchestra.apps.vps',
|
'orchestra.apps.vps',
|
||||||
|
'orchestra.apps.saas',
|
||||||
'orchestra.apps.issues',
|
'orchestra.apps.issues',
|
||||||
'orchestra.apps.services',
|
'orchestra.apps.services',
|
||||||
'orchestra.apps.orders',
|
'orchestra.apps.orders',
|
||||||
|
@ -184,6 +185,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
|
||||||
'databases/databaseuser': 'postgresql.png',
|
'databases/databaseuser': 'postgresql.png',
|
||||||
'vps/vps': 'TuxBox.png',
|
'vps/vps': 'TuxBox.png',
|
||||||
'miscellaneous/miscellaneous': 'applications-other.png',
|
'miscellaneous/miscellaneous': 'applications-other.png',
|
||||||
|
'saas/saas': 'saas.png',
|
||||||
# Accounts
|
# Accounts
|
||||||
'accounts/account': 'Face-monkey.png',
|
'accounts/account': 'Face-monkey.png',
|
||||||
'contacts/contact': 'contact_book.png',
|
'contacts/contact': 'contact_book.png',
|
||||||
|
|
BIN
orchestra/static/orchestra/icons/saas.png
Normal file
BIN
orchestra/static/orchestra/icons/saas.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
109
orchestra/static/orchestra/icons/saas.svg
Normal file
109
orchestra/static/orchestra/icons/saas.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 11 KiB |
|
@ -3,13 +3,13 @@
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Select a method for the new payment source</h1>
|
<h1>Select a {{ field_name }} for the new {{ opts.object_name }} instance</h1>
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
<div>
|
<div>
|
||||||
<div style="margin:20px;">
|
<div style="margin:20px;">
|
||||||
<ul>
|
<ul>
|
||||||
{% for name, verbose in methods %}
|
{% for name, verbose in plugins %}
|
||||||
<li><a href="../?method={{ name }}&{{ request.META.QUERY_STRING }}">{{ verbose }}</<a></li>
|
<li><a href="../?{{ field }}={{ name }}&{{ request.META.QUERY_STRING }}">{{ verbose }}</<a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
Loading…
Reference in a new issue