diff --git a/TODO.md b/TODO.md index 76dd02cc..98882ae9 100644 --- a/TODO.md +++ b/TODO.md @@ -169,8 +169,6 @@ 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) -* reespell systemuser to system_user - * remove order in account admin and others admininlines * Databases.User add reverse M2M databases widget (like mailbox.addresses) @@ -194,3 +192,11 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * resource min max allocation with validation * mailman needs both aliases when address_name is provided (default messages and bounces and all) + +* 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 diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 2883d605..3d0b3ef9 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -25,7 +25,7 @@ from .models import Account class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin): - list_display = ('username', 'type', 'is_active') + list_display = ('username', 'full_name', 'type', 'is_active') list_filter = ( 'type', 'is_active', HasMainUserListFilter ) @@ -55,7 +55,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) 'fields': ('last_login', 'date_joined') }), ) - search_fields = ('username',) + search_fields = ('username', 'short_name', 'full_name') add_form = AccountCreationForm form = UserChangeForm filter_horizontal = () @@ -64,6 +64,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) actions = [disable] change_view_actions = actions list_select_related = ('billcontact',) + ordering = () main_systemuser_link = admin_link('main_systemuser') @@ -115,10 +116,8 @@ admin.site.register(Account, AccountAdmin) class AccountListAdmin(AccountAdmin): """ Account list to allow account selection when creating new services """ - list_display = ('select_account', 'type', 'username') + list_display = ('select_account', 'username', 'type', 'username') actions = None - search_fields = ['username',] - ordering = ('username',) def select_account(self, instance): # TODO get query string from request.META['QUERY_STRING'] to preserve filters diff --git a/orchestra/apps/domains/admin.py b/orchestra/apps/domains/admin.py index adbed919..9d965078 100644 --- a/orchestra/apps/domains/admin.py +++ b/orchestra/apps/domains/admin.py @@ -68,9 +68,6 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): list_filter = [TopDomainListFilter] change_readonly_fields = ('name',) search_fields = ['name',] - default_changelist_filters = ( - ('top_domain', 'True'), - ) add_form = CreateDomainAdminForm change_view_actions = [view_zone] diff --git a/orchestra/apps/domains/filters.py b/orchestra/apps/domains/filters.py index 71d933f5..02c8b11f 100644 --- a/orchestra/apps/domains/filters.py +++ b/orchestra/apps/domains/filters.py @@ -11,25 +11,8 @@ class TopDomainListFilter(SimpleListFilter): def lookups(self, request, model_admin): return ( ('True', _("Top domains")), - ('False', _("All")), ) def queryset(self, request, queryset): if self.value() == 'True': return queryset.filter(top__isnull=True) - - def choices(self, cl): - """ Enable default selection different than All """ - for lookup, title in self.lookup_choices: - title = title._proxy____args[0] - selected = self.value() == force_text(lookup) - if not selected and title == "Top domains" and self.value() is None: - selected = True - # end of workaround - yield { - 'selected': selected, - 'query_string': cl.get_query_string({ - self.parameter_name: lookup, - }, []), - 'display': title, - } diff --git a/orchestra/apps/mailboxes/admin.py b/orchestra/apps/mailboxes/admin.py index 0dbeafc5..04b50ac7 100644 --- a/orchestra/apps/mailboxes/admin.py +++ b/orchestra/apps/mailboxes/admin.py @@ -100,7 +100,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): list_filter = (HasMailboxListFilter, HasForwardListFilter) fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward') inlines = [AutoresponseInline] - search_fields = ('name', 'domain__name',) + search_fields = ('name', 'domain__name', 'forward', 'mailboxes__name', 'account__username') readonly_fields = ('account_link', 'domain_link', 'email_link') filter_by_account_fields = ('domain', 'mailboxes') filter_horizontal = ['mailboxes'] diff --git a/orchestra/apps/mailboxes/forms.py b/orchestra/apps/mailboxes/forms.py index 55fffed4..f0149c31 100644 --- a/orchestra/apps/mailboxes/forms.py +++ b/orchestra/apps/mailboxes/forms.py @@ -42,6 +42,7 @@ class MailboxForm(forms.ModelForm): self.fields['addresses'].initial = self.instance.addresses.all() def clean_custom_filtering(self): + # TODO move to model.clean? filtering = self.cleaned_data['filtering'] custom_filtering = self.cleaned_data['custom_filtering'] if filtering == self._meta.model.CUSTOM and not custom_filtering: @@ -49,6 +50,10 @@ class MailboxForm(forms.ModelForm): 'custom_filtering': _("You didn't provide any custom filtering.") }) return custom_filtering + + def clean(self): + if not self.cleaned_data['mailboxes'] and not self.cleaned_data['forward']: + raise ValidationError("A mailbox or forward address should be provided.") class MailboxChangeForm(UserChangeForm, MailboxForm): diff --git a/orchestra/apps/mailboxes/serializers.py b/orchestra/apps/mailboxes/serializers.py index 88314a71..017120a1 100644 --- a/orchestra/apps/mailboxes/serializers.py +++ b/orchestra/apps/mailboxes/serializers.py @@ -81,5 +81,5 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri def validate(self, attrs): if not attrs['mailboxes'] and not attrs['forward']: - raise serializers.ValidationError("mailboxes or forward addresses should be provided") + raise serializers.ValidationError("A mailbox or forward address should be provided.") return attrs diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py index d3d9e1fc..1937500a 100644 --- a/orchestra/apps/payments/methods/options.py +++ b/orchestra/apps/payments/methods/options.py @@ -1,5 +1,6 @@ from dateutil import relativedelta from django import forms +from django.core.exceptions import ValidationError from orchestra.utils import plugins from orchestra.utils.functional import cached @@ -26,8 +27,12 @@ class PaymentMethod(plugins.Plugin): @classmethod def clean_data(cls, data): - """ model clean """ - return data + """ model clean, uses cls.serializer by default """ + serializer = cls.serializer(data=data) + if not serializer.is_valid(): + serializer.errors.pop('non_field_errors', None) + raise ValidationError(serializer.errors) + return serializer.data def get_form(self): self.form.plugin = self diff --git a/orchestra/apps/payments/methods/sepadirectdebit.py b/orchestra/apps/payments/methods/sepadirectdebit.py index 64380795..2169e732 100644 --- a/orchestra/apps/payments/methods/sepadirectdebit.py +++ b/orchestra/apps/payments/methods/sepadirectdebit.py @@ -19,7 +19,7 @@ from .options import PaymentMethod class SEPADirectDebitForm(PluginDataForm): - iban = IBANFormField(label='IBAN', + iban = forms.CharField(label='IBAN', widget=forms.TextInput(attrs={'size': '50'})) name = forms.CharField(max_length=128, label=_("Name"), widget=forms.TextInput(attrs={'size': '50'})) @@ -29,6 +29,11 @@ class SEPADirectDebitSerializer(serializers.Serializer): iban = serializers.CharField(label='IBAN', validators=[IBANValidator()], min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34) name = serializers.CharField(label=_("Name"), max_length=128) + + def validate(self, data): + data['iban'] = data['iban'].strip() + data['name'] = data['name'].strip() + return data class SEPADirectDebit(PaymentMethod): @@ -44,13 +49,6 @@ class SEPADirectDebit(PaymentMethod): return _("This bill will been automatically charged to your bank account " " with IBAN number
%s.") % source.number - @classmethod - def clean_data(cls, data): - data['iban'] = data['iban'].strip() - data['name'] = data['name'].strip() - IBANValidator()(data['iban']) - return data - @classmethod def process(cls, transactions): debts = [] diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index b6a2f642..639675a4 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -128,10 +128,11 @@ class ResourceData(models.Model): resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource")) content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) object_id = models.PositiveIntegerField(_("object id")) - used = models.DecimalField(_("used"), max_digits=16, decimal_places=2, null=True) - updated_at = models.DateTimeField(_("updated"), null=True) - allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True) - + used = models.DecimalField(_("used"), max_digits=16, decimal_places=2, null=True, + editable=False) + updated_at = models.DateTimeField(_("updated"), null=True, editable=False) + allocated = models.DecimalField(_("allocated"), max_digits=8, decimal_places=2, + null=True, blank=True) content_object = GenericForeignKey() class Meta: @@ -193,6 +194,12 @@ def create_resource_relation(): """ account.resources.web """ def __getattr__(self, attr): """ get or build ResourceData """ + try: + return self.obj.__resource_cache[attr] + except AttributeError: + self.obj.__resource_cache = {} + except KeyError: + pass try: data = self.obj.resource_set.get(resource__name=attr) except ResourceData.DoesNotExist: @@ -201,6 +208,7 @@ def create_resource_relation(): is_active=True) data = ResourceData(content_object=self.obj, resource=resource, allocated=resource.default_allocation) + self.obj.__resource_cache[attr] = data return data def __get__(self, obj, cls): diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py index 4c071f63..b9b3a705 100644 --- a/orchestra/apps/saas/models.py +++ b/orchestra/apps/saas/models.py @@ -11,7 +11,8 @@ 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') + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='saas') data = JSONField(_("data")) class Meta: @@ -28,6 +29,9 @@ class SaaS(models.Model): @cached_property def description(self): return self.service_class().get_description(self.data) + + def clean(self): + self.data = self.service_class().clean_data(self) services.register(SaaS) diff --git a/orchestra/apps/saas/services/bscw.py b/orchestra/apps/saas/services/bscw.py index 8b4670af..07346e6e 100644 --- a/orchestra/apps/saas/services/bscw.py +++ b/orchestra/apps/saas/services/bscw.py @@ -1,19 +1,53 @@ from django import forms +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from orchestra.core import validators from orchestra.forms import PluginDataForm from .options import SoftwareService +# TODO monitor quota since out of sync? + class BSCWForm(PluginDataForm): username = forms.CharField(label=_("Username"), max_length=64) - password = forms.CharField(label=_("Password"), max_length=64) - quota = forms.IntegerField(label=_("Quota")) + password = forms.CharField(label=_("Password"), max_length=256, required=False) + email = forms.EmailField(label=_("Email")) + quota = forms.IntegerField(label=_("Quota"), help_text=_("Disk quota in MB.")) + + +class SEPADirectDebitSerializer(serializers.Serializer): + username = serializers.CharField(label=_("Username"), max_length=64, + validators=[validators.validate_name]) + password = serializers.CharField(label=_("Password"), max_length=256, required=False, + write_only=True) + email = serializers.EmailField(label=_("Email")) + quota = serializers.IntegerField(label=_("Quota"), help_text=_("Disk quota in MB.")) + + def validate(self, data): + data['username'] = data['username'].strip() + return data class BSCWService(SoftwareService): verbose_name = "BSCW" form = BSCWForm + serializer = SEPADirectDebitSerializer description_field = 'username' icon = 'saas/icons/BSCW.png' + + @classmethod + def clean_data(cls, saas): + try: + data = super(BSCWService, cls).clean_data(saas) + except ValidationError, error: + if not saas.pk and 'password' not in saas.data: + error.error_dict['password'] = [_("Password is required.")] + raise error + if not saas.pk and 'password' not in saas.data: + raise ValidationError({ + 'password': _("Password is required.") + }) + return data diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py index f38e5cf5..f1905fc8 100644 --- a/orchestra/apps/saas/services/options.py +++ b/orchestra/apps/saas/services/options.py @@ -1,4 +1,5 @@ from django import forms +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from orchestra.utils import plugins @@ -8,8 +9,10 @@ from orchestra.utils.python import import_class from .. import settings +# TODO if unique_description: make description_field create only class SoftwareService(plugins.Plugin): description_field = '' + unique_description = True form = None serializer = None icon = 'orchestra/icons/apps.png' @@ -23,6 +26,21 @@ class SoftwareService(plugins.Plugin): plugins.append(import_class(cls)) return plugins + @classmethod + def clean_data(cls, saas): + """ model clean, uses cls.serizlier by default """ + if cls.unique_description and not saas.pk: + from ..models import SaaS + field = cls.description_field + if SaaS.objects.filter(data__contains='"%s":"%s"' % (field, saas.data[field])).exists(): + raise ValidationError({ + field: _("SaaS service with this %(field)s already exists.") + }, params={'field': field}) + serializer = cls.serializer(data=saas.data) + if not serializer.is_valid(): + raise ValidationError(serializer.errors) + return serializer.data + def get_form(self): self.form.plugin = self self.form.plugin_field = 'service' diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index 83048060..7d1f570d 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -27,12 +27,15 @@ class WebApp(models.Model): verbose_name_plural = _("Web Apps") def __unicode__(self): - return self.name or settings.WEBAPPS_BLANK_NAME + return self.get_name() @cached def get_options(self): return { opt.name: opt.value for opt in self.options.all() } + def get_name(self): + return return self.name or settings.WEBAPPS_BLANK_NAME + def get_fpm_port(self): return settings.WEBAPPS_FPM_START_PORT + self.account.pk diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 8290b584..6d6cc1b8 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -17,7 +17,6 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') -# TODO ssl ca, ssl cert, ssl key WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { # { name: ( verbose_name, validation_regex ) } 'directory_protection': ( @@ -48,6 +47,10 @@ WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { _("HTTPD - Disable Modsecurity"), r'^[\w/_]+$' ), + 'user_group': ( + _("HTTPD - SuexecUserGroup"), + r'^[\w/_]+\s[\w/_]+$' + ), }) diff --git a/orchestra/forms/options.py b/orchestra/forms/options.py index 6f415431..f25ec257 100644 --- a/orchestra/forms/options.py +++ b/orchestra/forms/options.py @@ -7,12 +7,11 @@ from ..core.validators import validate_password class PluginDataForm(forms.ModelForm): - class Meta: - exclude = ('data',) + data = forms.CharField(widget=forms.HiddenInput, required=False) def __init__(self, *args, **kwargs): super(PluginDataForm, self).__init__(*args, **kwargs) - # TODO remove it weel + # TODO remove it well try: self.fields[self.plugin_field].widget = forms.HiddenInput() except KeyError: @@ -23,13 +22,14 @@ class PluginDataForm(forms.ModelForm): initial = self.fields[field].initial self.fields[field].initial = instance.data.get(field, initial) - def save(self, commit=True): - plugin = self.plugin - setattr(self.instance, self.plugin_field, plugin.get_plugin_name()) - self.instance.data = { - field: self.cleaned_data[field] for field in self.declared_fields - } - return super(PluginDataForm, self).save(commit=commit) + 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):