Partially implemented BSCW support

This commit is contained in:
Marc Aymerich 2014-11-09 10:16:07 +00:00
parent 8cce7b58f6
commit 9ed4be4d2e
16 changed files with 121 additions and 58 deletions

10
TODO.md
View File

@ -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) * 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 * remove order in account admin and others admininlines
* Databases.User add reverse M2M databases widget (like mailbox.addresses) * 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 * resource min max allocation with validation
* mailman needs both aliases when address_name is provided (default messages and bounces and all) * 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

View File

@ -25,7 +25,7 @@ from .models import Account
class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin): class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin):
list_display = ('username', 'type', 'is_active') list_display = ('username', 'full_name', 'type', 'is_active')
list_filter = ( list_filter = (
'type', 'is_active', HasMainUserListFilter 'type', 'is_active', HasMainUserListFilter
) )
@ -55,7 +55,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
'fields': ('last_login', 'date_joined') 'fields': ('last_login', 'date_joined')
}), }),
) )
search_fields = ('username',) search_fields = ('username', 'short_name', 'full_name')
add_form = AccountCreationForm add_form = AccountCreationForm
form = UserChangeForm form = UserChangeForm
filter_horizontal = () filter_horizontal = ()
@ -64,6 +64,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin)
actions = [disable] actions = [disable]
change_view_actions = actions change_view_actions = actions
list_select_related = ('billcontact',) list_select_related = ('billcontact',)
ordering = ()
main_systemuser_link = admin_link('main_systemuser') main_systemuser_link = admin_link('main_systemuser')
@ -115,10 +116,8 @@ admin.site.register(Account, AccountAdmin)
class AccountListAdmin(AccountAdmin): class AccountListAdmin(AccountAdmin):
""" Account list to allow account selection when creating new services """ """ 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 actions = None
search_fields = ['username',]
ordering = ('username',)
def select_account(self, instance): def select_account(self, instance):
# TODO get query string from request.META['QUERY_STRING'] to preserve filters # TODO get query string from request.META['QUERY_STRING'] to preserve filters

View File

@ -68,9 +68,6 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_filter = [TopDomainListFilter] list_filter = [TopDomainListFilter]
change_readonly_fields = ('name',) change_readonly_fields = ('name',)
search_fields = ['name',] search_fields = ['name',]
default_changelist_filters = (
('top_domain', 'True'),
)
add_form = CreateDomainAdminForm add_form = CreateDomainAdminForm
change_view_actions = [view_zone] change_view_actions = [view_zone]

View File

@ -11,25 +11,8 @@ class TopDomainListFilter(SimpleListFilter):
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return ( return (
('True', _("Top domains")), ('True', _("Top domains")),
('False', _("All")),
) )
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'True': if self.value() == 'True':
return queryset.filter(top__isnull=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,
}

View File

@ -100,7 +100,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
list_filter = (HasMailboxListFilter, HasForwardListFilter) list_filter = (HasMailboxListFilter, HasForwardListFilter)
fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward') fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward')
inlines = [AutoresponseInline] 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') readonly_fields = ('account_link', 'domain_link', 'email_link')
filter_by_account_fields = ('domain', 'mailboxes') filter_by_account_fields = ('domain', 'mailboxes')
filter_horizontal = ['mailboxes'] filter_horizontal = ['mailboxes']

View File

@ -42,6 +42,7 @@ class MailboxForm(forms.ModelForm):
self.fields['addresses'].initial = self.instance.addresses.all() self.fields['addresses'].initial = self.instance.addresses.all()
def clean_custom_filtering(self): def clean_custom_filtering(self):
# TODO move to model.clean?
filtering = self.cleaned_data['filtering'] filtering = self.cleaned_data['filtering']
custom_filtering = self.cleaned_data['custom_filtering'] custom_filtering = self.cleaned_data['custom_filtering']
if filtering == self._meta.model.CUSTOM and not 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.") 'custom_filtering': _("You didn't provide any custom filtering.")
}) })
return 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): class MailboxChangeForm(UserChangeForm, MailboxForm):

View File

@ -81,5 +81,5 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri
def validate(self, attrs): def validate(self, attrs):
if not attrs['mailboxes'] and not attrs['forward']: 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 return attrs

View File

@ -1,5 +1,6 @@
from dateutil import relativedelta from dateutil import relativedelta
from django import forms from django import forms
from django.core.exceptions import ValidationError
from orchestra.utils import plugins from orchestra.utils import plugins
from orchestra.utils.functional import cached from orchestra.utils.functional import cached
@ -26,8 +27,12 @@ class PaymentMethod(plugins.Plugin):
@classmethod @classmethod
def clean_data(cls, data): def clean_data(cls, data):
""" model clean """ """ model clean, uses cls.serializer by default """
return data 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): def get_form(self):
self.form.plugin = self self.form.plugin = self

View File

@ -19,7 +19,7 @@ from .options import PaymentMethod
class SEPADirectDebitForm(PluginDataForm): class SEPADirectDebitForm(PluginDataForm):
iban = IBANFormField(label='IBAN', iban = forms.CharField(label='IBAN',
widget=forms.TextInput(attrs={'size': '50'})) widget=forms.TextInput(attrs={'size': '50'}))
name = forms.CharField(max_length=128, label=_("Name"), name = forms.CharField(max_length=128, label=_("Name"),
widget=forms.TextInput(attrs={'size': '50'})) widget=forms.TextInput(attrs={'size': '50'}))
@ -29,6 +29,11 @@ class SEPADirectDebitSerializer(serializers.Serializer):
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()], iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34) min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
name = serializers.CharField(label=_("Name"), max_length=128) 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): class SEPADirectDebit(PaymentMethod):
@ -44,13 +49,6 @@ class SEPADirectDebit(PaymentMethod):
return _("This bill will been automatically charged to your bank account " return _("This bill will been automatically charged to your bank account "
" with IBAN number<br><strong>%s</strong>.") % source.number " with IBAN number<br><strong>%s</strong>.") % source.number
@classmethod
def clean_data(cls, data):
data['iban'] = data['iban'].strip()
data['name'] = data['name'].strip()
IBANValidator()(data['iban'])
return data
@classmethod @classmethod
def process(cls, transactions): def process(cls, transactions):
debts = [] debts = []

View File

@ -128,10 +128,11 @@ class ResourceData(models.Model):
resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource")) resource = models.ForeignKey(Resource, related_name='dataset', verbose_name=_("resource"))
content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
object_id = models.PositiveIntegerField(_("object id")) object_id = models.PositiveIntegerField(_("object id"))
used = models.DecimalField(_("used"), max_digits=16, decimal_places=2, null=True) used = models.DecimalField(_("used"), max_digits=16, decimal_places=2, null=True,
updated_at = models.DateTimeField(_("updated"), null=True) editable=False)
allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True) 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() content_object = GenericForeignKey()
class Meta: class Meta:
@ -193,6 +194,12 @@ def create_resource_relation():
""" account.resources.web """ """ account.resources.web """
def __getattr__(self, attr): def __getattr__(self, attr):
""" get or build ResourceData """ """ get or build ResourceData """
try:
return self.obj.__resource_cache[attr]
except AttributeError:
self.obj.__resource_cache = {}
except KeyError:
pass
try: try:
data = self.obj.resource_set.get(resource__name=attr) data = self.obj.resource_set.get(resource__name=attr)
except ResourceData.DoesNotExist: except ResourceData.DoesNotExist:
@ -201,6 +208,7 @@ def create_resource_relation():
is_active=True) is_active=True)
data = ResourceData(content_object=self.obj, resource=resource, data = ResourceData(content_object=self.obj, resource=resource,
allocated=resource.default_allocation) allocated=resource.default_allocation)
self.obj.__resource_cache[attr] = data
return data return data
def __get__(self, obj, cls): def __get__(self, obj, cls):

View File

@ -11,7 +11,8 @@ from .services import SoftwareService
class SaaS(models.Model): class SaaS(models.Model):
service = models.CharField(_("service"), max_length=32, service = models.CharField(_("service"), max_length=32,
choices=SoftwareService.get_plugin_choices()) 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")) data = JSONField(_("data"))
class Meta: class Meta:
@ -28,6 +29,9 @@ class SaaS(models.Model):
@cached_property @cached_property
def description(self): def description(self):
return self.service_class().get_description(self.data) return self.service_class().get_description(self.data)
def clean(self):
self.data = self.service_class().clean_data(self)
services.register(SaaS) services.register(SaaS)

View File

@ -1,19 +1,53 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orchestra.core import validators
from orchestra.forms import PluginDataForm from orchestra.forms import PluginDataForm
from .options import SoftwareService from .options import SoftwareService
# TODO monitor quota since out of sync?
class BSCWForm(PluginDataForm): class BSCWForm(PluginDataForm):
username = forms.CharField(label=_("Username"), max_length=64) username = forms.CharField(label=_("Username"), max_length=64)
password = forms.CharField(label=_("Password"), max_length=64) password = forms.CharField(label=_("Password"), max_length=256, required=False)
quota = forms.IntegerField(label=_("Quota")) 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): class BSCWService(SoftwareService):
verbose_name = "BSCW" verbose_name = "BSCW"
form = BSCWForm form = BSCWForm
serializer = SEPADirectDebitSerializer
description_field = 'username' description_field = 'username'
icon = 'saas/icons/BSCW.png' 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

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.utils import plugins from orchestra.utils import plugins
@ -8,8 +9,10 @@ from orchestra.utils.python import import_class
from .. import settings from .. import settings
# TODO if unique_description: make description_field create only
class SoftwareService(plugins.Plugin): class SoftwareService(plugins.Plugin):
description_field = '' description_field = ''
unique_description = True
form = None form = None
serializer = None serializer = None
icon = 'orchestra/icons/apps.png' icon = 'orchestra/icons/apps.png'
@ -23,6 +26,21 @@ class SoftwareService(plugins.Plugin):
plugins.append(import_class(cls)) plugins.append(import_class(cls))
return plugins 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): def get_form(self):
self.form.plugin = self self.form.plugin = self
self.form.plugin_field = 'service' self.form.plugin_field = 'service'

View File

@ -27,12 +27,15 @@ class WebApp(models.Model):
verbose_name_plural = _("Web Apps") verbose_name_plural = _("Web Apps")
def __unicode__(self): def __unicode__(self):
return self.name or settings.WEBAPPS_BLANK_NAME return self.get_name()
@cached @cached
def get_options(self): def get_options(self):
return { opt.name: opt.value for opt in self.options.all() } 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): def get_fpm_port(self):
return settings.WEBAPPS_FPM_START_PORT + self.account.pk return settings.WEBAPPS_FPM_START_PORT + self.account.pk

View File

@ -17,7 +17,6 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*')
WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain')
# TODO ssl ca, ssl cert, ssl key
WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
# { name: ( verbose_name, validation_regex ) } # { name: ( verbose_name, validation_regex ) }
'directory_protection': ( 'directory_protection': (
@ -48,6 +47,10 @@ WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', {
_("HTTPD - Disable Modsecurity"), _("HTTPD - Disable Modsecurity"),
r'^[\w/_]+$' r'^[\w/_]+$'
), ),
'user_group': (
_("HTTPD - SuexecUserGroup"),
r'^[\w/_]+\s[\w/_]+$'
),
}) })

View File

@ -7,12 +7,11 @@ from ..core.validators import validate_password
class PluginDataForm(forms.ModelForm): class PluginDataForm(forms.ModelForm):
class Meta: data = forms.CharField(widget=forms.HiddenInput, required=False)
exclude = ('data',)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PluginDataForm, self).__init__(*args, **kwargs) super(PluginDataForm, self).__init__(*args, **kwargs)
# TODO remove it weel # TODO remove it well
try: try:
self.fields[self.plugin_field].widget = forms.HiddenInput() self.fields[self.plugin_field].widget = forms.HiddenInput()
except KeyError: except KeyError:
@ -23,13 +22,14 @@ class PluginDataForm(forms.ModelForm):
initial = self.fields[field].initial initial = self.fields[field].initial
self.fields[field].initial = instance.data.get(field, initial) self.fields[field].initial = instance.data.get(field, initial)
def save(self, commit=True): def clean(self):
plugin = self.plugin data = {}
setattr(self.instance, self.plugin_field, plugin.get_plugin_name()) for field in self.declared_fields:
self.instance.data = { try:
field: self.cleaned_data[field] for field in self.declared_fields data[field] = self.cleaned_data[field]
} except KeyError:
return super(PluginDataForm, self).save(commit=commit) data[field] = self.data[field]
self.cleaned_data['data'] = data
class UserCreationForm(forms.ModelForm): class UserCreationForm(forms.ModelForm):