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):