diff --git a/TODO.md b/TODO.md index 374574d0..404c8ae4 100644 --- a/TODO.md +++ b/TODO.md @@ -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')) diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index ae847031..1be4dc71 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -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' diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py index d0bbc53a..37747033 100644 --- a/orchestra/apps/miscellaneous/admin.py +++ b/orchestra/apps/miscellaneous/admin.py @@ -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) diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py index 289073e4..365710fe 100644 --- a/orchestra/apps/miscellaneous/models.py +++ b/orchestra/apps/miscellaneous/models.py @@ -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 unique text field that " - "identifies it or not.")) +# has_identifier = models.BooleanField(_("has identifier"), default=True, +# help_text=_("Designates if this service has a unique text field that " +# "identifies it or not.")) has_amount = models.BooleanField(_("has amount"), default=False, help_text=_("Designates whether this service has amount " "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() diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index 911abda7..c231821d 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -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 diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index 99eeeb9f..70056795 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -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 diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py index 1937500a..46636dfb 100644 --- a/orchestra/apps/payments/methods/options.py +++ b/orchestra/apps/payments/methods/options.py @@ -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 diff --git a/orchestra/apps/payments/methods/sepadirectdebit.py b/orchestra/apps/payments/methods/sepadirectdebit.py index 2169e732..48722cc7 100644 --- a/orchestra/apps/payments/methods/sepadirectdebit.py +++ b/orchestra/apps/payments/methods/sepadirectdebit.py @@ -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 diff --git a/orchestra/apps/payments/serializers.py b/orchestra/apps/payments/serializers.py index 1e85f623..aca7846b 100644 --- a/orchestra/apps/payments/serializers.py +++ b/orchestra/apps/payments/serializers.py @@ -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 diff --git a/orchestra/apps/plugins/__init__.py b/orchestra/apps/plugins/__init__.py new file mode 100644 index 00000000..921dfe14 --- /dev/null +++ b/orchestra/apps/plugins/__init__.py @@ -0,0 +1 @@ +from .options import * diff --git a/orchestra/apps/plugins/admin.py b/orchestra/apps/plugins/admin.py new file mode 100644 index 00000000..878f7cb2 --- /dev/null +++ b/orchestra/apps/plugins/admin.py @@ -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 diff --git a/orchestra/apps/plugins/forms.py b/orchestra/apps/plugins/forms.py new file mode 100644 index 00000000..6467ca75 --- /dev/null +++ b/orchestra/apps/plugins/forms.py @@ -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 diff --git a/orchestra/utils/plugins.py b/orchestra/apps/plugins/options.py similarity index 88% rename from orchestra/utils/plugins.py rename to orchestra/apps/plugins/options.py index a0888fd1..47080434 100644 --- a/orchestra/utils/plugins.py +++ b/orchestra/apps/plugins/options.py @@ -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]) diff --git a/orchestra/templates/admin/orchestra/select_plugin.html b/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html similarity index 81% rename from orchestra/templates/admin/orchestra/select_plugin.html rename to orchestra/apps/plugins/templates/admin/plugins/select_plugin.html index b5421f5e..12ea44c6 100644 --- a/orchestra/templates/admin/orchestra/select_plugin.html +++ b/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html @@ -18,9 +18,9 @@