Plugins logic inot a plugins app

This commit is contained in:
Marc Aymerich 2014-11-12 16:33:40 +00:00
parent 49b7be33e3
commit 971b1b6874
26 changed files with 212 additions and 148 deletions

23
TODO.md
View File

@ -169,17 +169,17 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* validate address.forward: if mailbox in account.mailboxes then: _("Please use mailboxes field") or consider removing mailbox support on forward (user@pangea.org instead)
* remove order in account admin and others admininlines
* remove ordering in account admin and others admininlines
* Databases.User add reverse M2M databases widget (like mailbox.addresses)
* Change permissions periodically on the web server, to ensure security
* Change (correct) permissions periodically on the web server, to ensure security ?
* Root owned logs on user's home ?
* Root owned logs on user's home ? yes
* reconsider binding webapps to systemusers (pangea multiple users wordpress-ftp, moodle-pangea, etc)
* Secondary user home in /home/secondaryuser and simlink to /home/main/webapps/app so it can have private storage?
* Grant permissions like in webfaction
* Grant permissions to systemusers, the problem of creating a related permission model is out of sync with the server-side. evaluate tradeoff
* Secondaryusers home should be under mainuser home. i.e. /home/mainuser/webapps/seconduser_webapp/
* Make one dedicated CGI user for each account only for CGI execution (fpm/fcgid). Different from the files owner, and without W permissions, so attackers can not inject backdors and malware.
@ -195,24 +195,13 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* domain validation parse named-checzone output to assign errors to fields
* Directory Protection on webapp and use webapp path as base path (validate)
* User [Group] webapp/website option (validation) which overrides default mainsystemuser
* validate systemuser.home
* Create plugin app
* Create options widget
* generic options fpm/fcgid/uwsgi webapps (num procs, idle io timeout)
* webapp backend option compatibility check?
* Route help text with model name when selecting backend
* Service instance name when selecting content_type
* Address.forward mailbbox validate not available on mailboxes
* Miscellaneous service construct form for specific data, fields, validation, uniquenes.. etc (domain usecase)
* miscellaneous.indentifier.endswith(('.org', '.es', '.cat'))

View File

@ -163,75 +163,6 @@ class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.Mod
return qs
class SelectPluginAdminMixin(object):
plugin = None
plugin_field = None
def get_form(self, request, obj=None, **kwargs):
if obj:
self.form = getattr(obj, '%s_class' % self.plugin_field)().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,
'plugin': self.plugin,
'plugins': self.plugin.get_plugins(),
}
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) or request.POST.get(self.plugin_field)
if plugin_value or len(self.plugin.get_plugins()) == 1:
self.plugin_value = plugin_value
if not plugin_value:
self.plugin_value = self.plugin.get_plugins()[0].get_plugin_name()
b = self.plugin_value
context = {
'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value),
}
context.update(extra_context or {})
return super(SelectPluginAdminMixin, self).add_view(request, form_url=form_url,
extra_context=context)
return redirect('./select-plugin/?%s' % request.META['QUERY_STRING'])
def change_view(self, request, object_id, form_url='', extra_context=None):
obj = self.get_object(request, unquote(object_id))
plugin_value = getattr(obj, self.plugin_field)
context = {
'title': _("Change %s") % camel_case_to_spaces(plugin_value),
}
context.update(extra_context or {})
return super(SelectPluginAdminMixin, self).change_view(request, object_id,
form_url=form_url, extra_context=context)
def save_model(self, request, obj, form, change):
if not change:
setattr(obj, self.plugin_field, self.plugin_value)
obj.save()
class ChangePasswordAdminMixin(object):
change_password_form = AdminPasswordChangeForm
change_user_password_template = 'admin/orchestra/change_password.html'

View File

@ -10,6 +10,14 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
from .models import MiscService, Miscellaneous
from orchestra.apps.plugins.admin import SelectPluginAdminMixin, PluginAdapter
class MiscServicePlugin(PluginAdapter):
model = MiscService
name_field = 'name'
class MiscServiceAdmin(ExtendedModelAdmin):
list_display = ('name', 'verbose_name', 'num_instances', 'has_amount', 'is_active')
list_editable = ('has_amount', 'is_active')
@ -32,15 +40,38 @@ class MiscServiceAdmin(ExtendedModelAdmin):
return qs.annotate(models.Count('instances', distinct=True))
class MiscellaneousAdmin(AccountAdminMixin, admin.ModelAdmin):
class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelAdmin):
list_display = ('service', 'amount', 'active', 'account_link')
plugin_field = 'service'
plugin = MiscServicePlugin
def get_service(self, obj):
if obj is None:
return self.plugin.get_plugin(self.plugin_value)().instance
else:
return obj.service
def get_fields(self, request, obj=None):
if obj is None:
return ('service', 'account', 'description', 'amount', 'is_active')
elif not obj.service.has_amount:
return ('service', 'account_link', 'description', 'is_active')
return ('service', 'account_link', 'description', 'amount', 'is_active')
fields = ['account', 'description', 'is_active']
if obj is not None:
fields = ['account_link', 'description', 'is_active']
service = self.get_service(obj)
if service.has_amount:
fields.insert(-1, 'amount')
# if service.has_identifier:
# fields.insert(1, 'identifier')
return fields
def get_form(self, request, obj=None, **kwargs):
form = super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs)
service = self.get_service(obj)
def clean_identifier(self, service=service):
validator = settings.MISCELLANEOUS_IDENTIFIER_VALIDATORS.get(service.name, None)
if validator:
validator(self.cleaned_data['identifier'])
form.clean_identifier = clean_identifier
return form
admin.site.register(MiscService, MiscServiceAdmin)

View File

@ -14,9 +14,9 @@ class MiscService(models.Model):
help_text=_("Human readable name"))
description = models.TextField(_("description"), blank=True,
help_text=_("Optional description"))
has_identifier = models.BooleanField(_("has identifier"), default=True,
help_text=_("Designates if this service has a <b>unique text</b> field that "
"identifies it or not."))
# has_identifier = models.BooleanField(_("has identifier"), default=True,
# help_text=_("Designates if this service has a <b>unique text</b> field that "
# "identifies it or not."))
has_amount = models.BooleanField(_("has amount"), default=False,
help_text=_("Designates whether this service has <tt>amount</tt> "
"property or not."))
@ -39,8 +39,8 @@ class Miscellaneous(models.Model):
related_name='instances')
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='miscellaneous')
identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
blank=True, help_text=_("A unique identifier for this service."))
# identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True,
# help_text=_("A unique identifier for this service."))
description = models.TextField(_("description"), blank=True)
amount = models.PositiveIntegerField(_("amount"), default=1)
is_active = models.BooleanField(_("active"), default=True,
@ -51,6 +51,7 @@ class Miscellaneous(models.Model):
verbose_name_plural = _("miscellaneous")
def __unicode__(self):
# return self.identifier or str(self.service)
return "{0}-{1}".format(str(self.service), str(self.account))
@cached_property
@ -61,8 +62,8 @@ class Miscellaneous(models.Model):
return self.is_active
def clean(self):
if self.identifier:
self.identifier = self.identifier.strip()
# if self.identifier:
# self.identifier = self.identifier.strip()
self.description = self.description.strip()

View File

@ -3,7 +3,7 @@ from functools import partial
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
from orchestra.apps import plugins
from . import methods

View File

@ -2,9 +2,10 @@ from django.contrib import admin
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeViewActionsMixin, SelectPluginAdminMixin, ExtendedModelAdmin
from orchestra.admin import ChangeViewActionsMixin, ExtendedModelAdmin
from orchestra.admin.utils import admin_colored, admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
from orchestra.apps.plugins.admin import SelectPluginAdminMixin
from . import actions
from .methods import PaymentMethod

View File

@ -2,7 +2,7 @@ from dateutil import relativedelta
from django import forms
from django.core.exceptions import ValidationError
from orchestra.utils import plugins
from orchestra.apps import plugins
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class

View File

@ -12,7 +12,7 @@ from django_iban.forms import IBANFormField
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
from rest_framework import serializers
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .. import settings
from .options import PaymentMethod

View File

@ -31,7 +31,7 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod
def metadata(self):
meta = super(PaymentSourceSerializer, self).metadata()
meta['data'] = {
method.get_plugin_name(): method().get_serializer()().metadata()
method.get_name(): method().get_serializer()().metadata()
for method in PaymentMethod.get_plugins()
}
return meta

View File

@ -0,0 +1 @@
from .options import *

View File

@ -0,0 +1,108 @@
from django.conf.urls import patterns, url
from django.contrib.admin.utils import unquote
from django.shortcuts import render, redirect
from django.utils.text import camel_case_to_spaces
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.utils import wrap_admin_view
from orchestra.utils.functional import cached
class SelectPluginAdminMixin(object):
plugin = None
plugin_field = None
def get_form(self, request, obj=None, **kwargs):
if obj:
self.form = getattr(obj, '%s_class' % self.plugin_field)().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,
'plugin': self.plugin,
'plugins': self.plugin.get_plugins(),
}
template = 'admin/plugins/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) or request.POST.get(self.plugin_field)
if plugin_value or len(self.plugin.get_plugins()) == 1:
self.plugin_value = plugin_value
if not plugin_value:
self.plugin_value = self.plugin.get_plugins()[0].get_name()
context = {
'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value),
}
context.update(extra_context or {})
return super(SelectPluginAdminMixin, self).add_view(request, form_url=form_url,
extra_context=context)
return redirect('./select-plugin/?%s' % request.META['QUERY_STRING'])
def change_view(self, request, object_id, form_url='', extra_context=None):
obj = self.get_object(request, unquote(object_id))
plugin_value = getattr(obj, self.plugin_field)
context = {
'title': _("Change %s") % camel_case_to_spaces(str(plugin_value)),
}
context.update(extra_context or {})
return super(SelectPluginAdminMixin, self).change_view(request, object_id,
form_url=form_url, extra_context=context)
def save_model(self, request, obj, form, change):
if not change:
setattr(obj, self.plugin_field, self.plugin_value)
obj.save()
class PluginAdapter(object):
""" Adapter class for using model classes as plugins """
model = None
name_field = None
def __init__(self, instance):
self.instance = instance
@classmethod
@cached
def get_plugins(cls):
plugins = []
for instance in cls.model.objects.filter(is_active=True):
plugins.append(cls(instance))
return plugins
@classmethod
def get_plugin(cls, name):
return cls(cls.model.objects.get(**{cls.name_field:name}))
@property
def verbose_name(self):
return self.instance.verbose_name or str(getattr(self.instance, self.name_field))
def get_name(self):
return getattr(self.instance, self.name_field)
def __call__(self):
return self

View File

@ -0,0 +1,27 @@
from django import forms
class PluginDataForm(forms.ModelForm):
data = forms.CharField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):
super(PluginDataForm, self).__init__(*args, **kwargs)
# TODO remove it well
try:
self.fields[self.plugin_field].widget = forms.HiddenInput()
except KeyError:
pass
instance = kwargs.get('instance')
if instance:
for field in self.declared_fields:
initial = self.fields[field].initial
self.fields[field].initial = instance.data.get(field, initial)
def clean(self):
data = {}
for field in self.declared_fields:
try:
data[field] = self.cleaned_data[field]
except KeyError:
data[field] = self.data[field]
self.cleaned_data['data'] = data

View File

@ -1,4 +1,4 @@
from .functional import cached
from orchestra.utils.functional import cached
class Plugin(object):
@ -8,7 +8,7 @@ class Plugin(object):
icon = None
@classmethod
def get_plugin_name(cls):
def get_name(cls):
return cls.__name__
@classmethod
@ -19,7 +19,7 @@ class Plugin(object):
@cached
def get_plugin(cls, name):
for plugin in cls.get_plugins():
if plugin.get_plugin_name() == name:
if plugin.get_name() == name:
return plugin
raise KeyError('This plugin is not registered')
@ -30,14 +30,14 @@ class Plugin(object):
if verbose[0]:
return cls.verbose_name
else:
return cls.get_plugin_name()
return cls.get_name()
@classmethod
def get_plugin_choices(cls):
choices = []
for plugin in cls.get_plugins():
verbose = plugin.get_verbose_name()
choices.append((plugin.get_plugin_name(), verbose))
choices.append((plugin.get_name(), verbose))
return sorted(choices, key=lambda e: e[1])

View File

@ -18,9 +18,9 @@
<div class="dashboard-module-content">
<ul class="fluent-dashboard-appiconlist clearfix" style="padding: 0">
{% for plugin in plugins %}
<li><a class="fluent-dashboard-icon" href="../?{{ field }}={{ plugin.get_plugin_name }}&{{ request.META.QUERY_STRING }}">
<li><a class="fluent-dashboard-icon" href="../?{{ field }}={{ plugin.get_name }}&{{ request.META.QUERY_STRING }}">
<img src="{% static plugin.icon %}" width="48" height="48" alt="{{ plugin.get_name }}"></a>
<a class="fluent-dashboard-icon-caption" href="../?{{ field }}={{ plugin.get_plugin_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</a></li>
<a class="fluent-dashboard-icon-caption" href="../?{{ field }}={{ plugin.get_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</a></li>
{% endfor %}
</ul>
</div>
@ -28,7 +28,7 @@
{% else %}
<ul>
{% for plugin in plugins %}
<li><a style="font-size:small;" href="../?{{ field }}={{ plugin.get_plugin_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</<a></li>
<li><a style="font-size:small;" href="../?{{ field }}={{ plugin.get_name }}&{{ request.META.QUERY_STRING }}">{{ plugin.verbose_name }}</<a></li>
{% endfor %}
</ul>
{% endif %}

View File

@ -1,7 +1,7 @@
from django.contrib import admin
from orchestra.admin import SelectPluginAdminMixin
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.apps.plugins.admin import SelectPluginAdminMixin
from .models import SaaS
from .services import SoftwareService

View File

@ -3,8 +3,8 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orchestra.apps.plugins.forms import PluginDataForm
from orchestra.core import validators
from orchestra.forms import PluginDataForm
from .options import SoftwareService

View File

@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .options import SoftwareService

View File

@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .options import SoftwareService

View File

@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .options import SoftwareService

View File

@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .options import SoftwareService

View File

@ -2,7 +2,7 @@ from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
from orchestra.apps import plugins
from orchestra.utils.functional import cached
from orchestra.utils.python import import_class

View File

@ -1,7 +1,7 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .options import SoftwareService

View File

@ -2,7 +2,7 @@ from django import forms
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.forms import PluginDataForm
from orchestra.apps.plugins.forms import PluginDataForm
from .options import SoftwareService

View File

@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins
from orchestra.apps import plugins
from orchestra.utils.humanize import text2int
from orchestra.utils.python import AttrDict

View File

@ -89,6 +89,7 @@ INSTALLED_APPS = (
'orchestra.apps.miscellaneous',
'orchestra.apps.bills',
'orchestra.apps.payments',
'orchestra.apps.plugins',
# Third-party apps
'django_extensions',

View File

@ -6,32 +6,6 @@ from .. import settings
from ..core.validators import validate_password
class PluginDataForm(forms.ModelForm):
data = forms.CharField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):
super(PluginDataForm, self).__init__(*args, **kwargs)
# TODO remove it well
try:
self.fields[self.plugin_field].widget = forms.HiddenInput()
except KeyError:
pass
instance = kwargs.get('instance')
if instance:
for field in self.declared_fields:
initial = self.fields[field].initial
self.fields[field].initial = instance.data.get(field, initial)
def clean(self):
data = {}
for field in self.declared_fields:
try:
data[field] = self.cleaned_data[field]
except KeyError:
data[field] = self.data[field]
self.cleaned_data['data'] = data
class UserCreationForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and