diff --git a/TODO.md b/TODO.md index 03330caa..c9c6617c 100644 --- a/TODO.md +++ b/TODO.md @@ -369,3 +369,23 @@ pip3 install https://github.com/fantix/gevent/archive/master.zip # user order_id as bill line id # BUG Delete related services also deletes account! +# auto apend trailing slash + +# get_related service__rates__isnull=TRue is that correct? + +# uwsgi hot reload? http://uwsgi-docs.readthedocs.org/en/latest/articles/TheArtOfGracefulReloading.html + + +# change mailer.message.priority by, queue/sent inmediatelly or rename critical to noq + + +# method( + arg, arg, arg) + + + +# Finish Nested *resource* serializers, like websites.domains: make fields readonly: read_only_fields = ('name',) +# websites.directives full validation like directive formset: move formset validation out and call it with compat-data from both places + + +# apply normlocation function on unique_location validation diff --git a/orchestra/api/serializers.py b/orchestra/api/serializers.py index e554c906..ef67b074 100644 --- a/orchestra/api/serializers.py +++ b/orchestra/api/serializers.py @@ -1,5 +1,6 @@ import copy +from django.db import models from django.forms import widgets from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers @@ -19,6 +20,8 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): def validate(self, attrs): """ calls model.clean() """ attrs = super(HyperlinkedModelSerializer, self).validate(attrs) + if isinstance(attrs, models.Model): + return attrs validated_data = dict(attrs) ModelClass = self.Meta.model # Remove many-to-many relationships from validated_data. @@ -39,9 +42,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): def post_only_cleanning(self, instance, validated_data): """ removes postonly_fields from attrs """ model_attrs = dict(**validated_data) - if instance is not None: + post_only_fields = getattr(self, 'post_only_fields', None) + if instance is not None and post_only_fields: for attr, value in validated_data.items(): - if attr in self.Meta.postonly_fields: + if attr in post_only_fields: model_attrs.pop(attr) return model_attrs @@ -56,6 +60,21 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): return super(HyperlinkedModelSerializer, self).partial_update(instance, model_attrs) +class RelatedHyperlinkedModelSerializer(HyperlinkedModelSerializer): + """ returns object on to_internal_value based on URL """ + def to_internal_value(self, data): + url = data.get('url') + if not url: + raise ValidationError({ + 'url': "URL is required." + }) + account = self.get_account() + queryset = self.Meta.model.objects.filter(account=self.get_account()) + self.fields['url'].queryset = queryset + obj = self.fields['url'].to_internal_value(url) + return obj + + class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, required=False, diff --git a/orchestra/contrib/accounts/serializers.py b/orchestra/contrib/accounts/serializers.py index 0579d9e0..e58bb268 100644 --- a/orchestra/contrib/accounts/serializers.py +++ b/orchestra/contrib/accounts/serializers.py @@ -16,10 +16,12 @@ class AccountSerializerMixin(object): def __init__(self, *args, **kwargs): super(AccountSerializerMixin, self).__init__(*args, **kwargs) self.account = None + + def get_account(self): request = self.context.get('request') if request: - self.account = request.user + return request.user def create(self, validated_data): - validated_data['account'] = self.account + validated_data['account'] = self.get_account() return super(AccountSerializerMixin, self).create(validated_data) diff --git a/orchestra/contrib/bills/templates/bills/microspective.css b/orchestra/contrib/bills/templates/bills/microspective.css index 15d76266..b9c3205f 100644 --- a/orchestra/contrib/bills/templates/bills/microspective.css +++ b/orchestra/contrib/bills/templates/bills/microspective.css @@ -170,12 +170,12 @@ a:hover { } #lines .column-id { - width: 5%; + width: 8%; text-align: right; } #lines .column-description { - width: 45%; + width: 42%; text-align: left; } diff --git a/orchestra/contrib/bills/templates/bills/microspective.html b/orchestra/contrib/bills/templates/bills/microspective.html index c51fd7c8..ad764b9d 100644 --- a/orchestra/contrib/bills/templates/bills/microspective.html +++ b/orchestra/contrib/bills/templates/bills/microspective.html @@ -77,24 +77,32 @@ {% trans "subtotal" %}
{% for line in lines %} - {% with sublines=line.sublines.all %} - {{ line.id }} - {{ line.description }} - {{ line.get_verbose_period }} - {{ line.get_verbose_quantity|default:" "|safe }} - {% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %} - {{ line.subtotal }} &{{ currency.lower }}; -
- {% for subline in sublines %} -   - {{ subline.description }} -   -   -   - {{ subline.total }} &{{ currency.lower }}; -
- {% endfor %} - {% endwith %} + {% with sublines=line.sublines.all description=line.description|slice:"40:" %} + {% if not line.order_id %}L{% endif %}{{ line.order_id }} + {{ line.description|slice:":40" }} + {{ line.get_verbose_period }} + {{ line.get_verbose_quantity|default:" "|safe }} + {% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %} + {{ line.subtotal }} &{{ currency.lower }}; +
+ {% if description %} +   + {{ description|truncatechars:41 }} +   +   +   +   + {% endif %} + {% for subline in sublines %} +   + {{ subline.description|truncatechars:41 }} +   +   +   + {{ subline.total }} &{{ currency.lower }}; +
+ {% endfor %} + {% endwith %} {% endfor %}
diff --git a/orchestra/contrib/databases/serializers.py b/orchestra/contrib/databases/serializers.py index 2c653704..0ac90f08 100644 --- a/orchestra/contrib/databases/serializers.py +++ b/orchestra/contrib/databases/serializers.py @@ -3,20 +3,17 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from orchestra.api.serializers import HyperlinkedModelSerializer, SetPasswordHyperlinkedSerializer +from orchestra.api.serializers import (HyperlinkedModelSerializer, + SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer) from orchestra.contrib.accounts.serializers import AccountSerializerMixin from .models import Database, DatabaseUser -class RelatedDatabaseUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedDatabaseUserSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = DatabaseUser fields = ('url', 'id', 'username') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, username=data['username']) class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): @@ -35,14 +32,10 @@ class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): return attrs -class RelatedDatabaseSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedDatabaseSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = Database fields = ('url', 'id', 'name',) - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) class DatabaseUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): diff --git a/orchestra/contrib/domains/admin.py b/orchestra/contrib/domains/admin.py index 1d7e7ce5..cfe14696 100644 --- a/orchestra/contrib/domains/admin.py +++ b/orchestra/contrib/domains/admin.py @@ -2,7 +2,7 @@ import re from django import forms from django.contrib import admin -from django.db.models.functions import Concat +from django.db.models.functions import Concat, Coalesce from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -100,7 +100,10 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin): qs = super(DomainAdmin, self).get_queryset(request) qs = qs.select_related('top', 'account') if request.method == 'GET': - qs = qs.annotate(structured_name=Concat('top__name', 'name')).order_by('structured_name') + qs = qs.annotate( + structured_id=Coalesce('top__id', 'id'), + structured_name=Concat('top__name', 'name') + ).order_by('-structured_id', 'structured_name') if apps.isinstalled('orchestra.contrib.websites'): qs = qs.prefetch_related('websites') return qs diff --git a/orchestra/contrib/domains/forms.py b/orchestra/contrib/domains/forms.py index 3bbf466f..59560894 100644 --- a/orchestra/contrib/domains/forms.py +++ b/orchestra/contrib/domains/forms.py @@ -9,7 +9,7 @@ from .models import Domain class BatchDomainCreationAdminForm(forms.ModelForm): name = forms.CharField(label=_("Names"), widget=forms.Textarea(attrs={'rows': 5, 'cols': 50}), - help_text=_("Domain per line. All domains will share the same attributes.")) + help_text=_("Domain per line. All domains will have the provided account and records.")) def clean_name(self): self.extra_names = [] diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py index 7fae0ba1..994a7405 100644 --- a/orchestra/contrib/domains/models.py +++ b/orchestra/contrib/domains/models.py @@ -162,7 +162,9 @@ class Domain(models.Model): type=Record.SOA, value=' '.join(soa) )) - is_host = self.is_top or not types or Record.A in types or Record.AAAA in types + has_a = Record.A in types + has_aaaa = Record.AAAA in types + is_host = self.is_top or not types or has_a or has_aaaa if is_host: if Record.MX not in types: for mx in settings.DOMAINS_DEFAULT_MX: @@ -170,18 +172,19 @@ class Domain(models.Model): type=Record.MX, value=mx )) - default_a = settings.DOMAINS_DEFAULT_A - if default_a and Record.A not in types: - records.append(AttrDict( - type=Record.A, - value=default_a - )) - default_aaaa = settings.DOMAINS_DEFAULT_AAAA - if default_aaaa and Record.AAAA not in types: - records.append(AttrDict( - type=Record.AAAA, - value=default_aaaa - )) + if not has_a and not has_aaaa: + default_a = settings.DOMAINS_DEFAULT_A + if default_a: + records.append(AttrDict( + type=Record.A, + value=default_a + )) + default_aaaa = settings.DOMAINS_DEFAULT_AAAA + if default_aaaa: + records.append(AttrDict( + type=Record.AAAA, + value=default_aaaa + )) return records def render_records(self): diff --git a/orchestra/contrib/domains/serializers.py b/orchestra/contrib/domains/serializers.py index b0a87f0f..3d23cc33 100644 --- a/orchestra/contrib/domains/serializers.py +++ b/orchestra/contrib/domains/serializers.py @@ -36,11 +36,11 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): raise ValidationError(_("Can not create subdomains of other users domains")) return attrs - def full_clean(self, instance): + def validate(self, data): """ Checks if everything is consistent """ - instance = super(DomainSerializer, self).full_clean(instance) - if instance and instance.name: - records = self.init_data.get('records', []) - domain = domain_for_validation(instance, records) + data = super(DomainSerializer, self).validate(data) + if self.instance and data.get('name'): + records = data['records'] + domain = domain_for_validation(self.instance, records) validators.validate_zone(domain.render_zone()) - return instance + return data diff --git a/orchestra/contrib/lists/serializers.py b/orchestra/contrib/lists/serializers.py index b3624e87..22676841 100644 --- a/orchestra/contrib/lists/serializers.py +++ b/orchestra/contrib/lists/serializers.py @@ -4,21 +4,17 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from rest_framework import serializers -from orchestra.api.serializers import SetPasswordHyperlinkedSerializer +from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer from orchestra.contrib.accounts.serializers import AccountSerializerMixin from orchestra.core.validators import validate_password from .models import List -class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = List.address_domain.field.rel.to fields = ('url', 'id', 'name') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) class ListSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index cc834981..a6ac948c 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -3,20 +3,16 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from orchestra.api.serializers import SetPasswordHyperlinkedSerializer +from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer from orchestra.contrib.accounts.serializers import AccountSerializerMixin from .models import Mailbox, Address -class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = Address.domain.field.rel.to fields = ('url', 'id', 'name') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): @@ -42,14 +38,10 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer postonly_fields = ('name', 'password') -class RelatedMailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = Mailbox fields = ('url', 'id', 'name') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): diff --git a/orchestra/contrib/resources/serializers.py b/orchestra/contrib/resources/serializers.py index a2cde99e..fa21610c 100644 --- a/orchestra/contrib/resources/serializers.py +++ b/orchestra/contrib/resources/serializers.py @@ -15,8 +15,8 @@ class ResourceSerializer(serializers.ModelSerializer): fields = ('name', 'used', 'allocated', 'unit') read_only_fields = ('used',) - def from_native(self, raw_data, files=None): - data = super(ResourceSerializer, self).from_native(raw_data, files=files) + def to_internal_value(self, raw_data): + data = super(ResourceSerializer, self).to_internal_value(raw_data) if not data.resource_id: data.resource = Resource.objects.get(name=raw_data['name']) return data diff --git a/orchestra/contrib/systemusers/serializers.py b/orchestra/contrib/systemusers/serializers.py index 25282d3e..401ad132 100644 --- a/orchestra/contrib/systemusers/serializers.py +++ b/orchestra/contrib/systemusers/serializers.py @@ -3,25 +3,21 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from orchestra.api.serializers import SetPasswordHyperlinkedSerializer +from orchestra.api.serializers import SetPasswordHyperlinkedSerializer, RelatedHyperlinkedModelSerializer from orchestra.contrib.accounts.serializers import AccountSerializerMixin from .models import SystemUser from .validators import validate_home -class GroupSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedGroupSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = SystemUser fields = ('url', 'id', 'username',) - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, username=data['username']) class SystemUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): - groups = GroupSerializer(many=True, required=False) + groups = RelatedGroupSerializer(many=True, required=False) class Meta: model = SystemUser @@ -36,7 +32,7 @@ class SystemUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSeriali username=attrs.get('username') or self.instance.username, shell=attrs.get('shell') or self.instance.shell, ) - validate_home(user, attrs, self.account) + validate_home(user, attrs, self.get_account()) return attrs def validate_groups(self, attrs, source): diff --git a/orchestra/contrib/websites/admin.py b/orchestra/contrib/websites/admin.py index 8f50d564..0463ecd8 100644 --- a/orchestra/contrib/websites/admin.py +++ b/orchestra/contrib/websites/admin.py @@ -111,6 +111,14 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): qset = Q(qset & ~Q(websites__pk=object_id)) formfield.queryset = formfield.queryset.exclude(qset) return formfield + + def _create_formsets(self, request, obj, change): + """ bind contents formset to directive formset for unique location cross-validation """ + formsets, inline_instances = super(WebsiteAdmin, self)._create_formsets(request, obj, change) + if request.method == 'POST': + contents, directives = formsets + directives.content_formset = contents + return formsets, inline_instances admin.site.register(Website, WebsiteAdmin) diff --git a/orchestra/contrib/websites/directives.py b/orchestra/contrib/websites/directives.py index bb22f304..a2db9fa2 100644 --- a/orchestra/contrib/websites/directives.py +++ b/orchestra/contrib/websites/directives.py @@ -1,4 +1,5 @@ import re +from collections import defaultdict from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -19,6 +20,7 @@ class SiteDirective(Plugin): help_text = "" unique_name = False unique_value = False + unique_location = False @classmethod @cached @@ -50,6 +52,37 @@ class SiteDirective(Plugin): for group, options in options.items(): yield (group, [(op.name, op.verbose_name) for op in options]) + def validate_uniqueness(self, directive, values, locations): + """ Validates uniqueness location, name and value """ + errors = defaultdict(list) + # location uniqueness + location = None + if self.unique_location: + location = directive['value'].split()[0] + if location is not None and location in locations: + errors['value'].append(ValidationError( + "Location '%s' already in use by other content/directive." % location + )) + else: + locations.add(location) + + # name uniqueness + if self.unique_name and self.name in values: + errors[None].append(ValidationError( + _("Only one %s can be defined.") % self.get_verbose_name() + )) + + # value uniqueness + value = directive.get('value', None) + if value is not None: + if self.unique_value and value in values.get(self.name, []): + errors['value'].append(ValidationError( + _("This value is already used by other %s.") % force_text(self.get_verbose_name()) + )) + values[self.name].append(value) + if errors: + raise ValidationError(errors) + def validate(self, website): if self.regex and not re.match(self.regex, website.value): raise ValidationError({ @@ -68,6 +101,7 @@ class Redirect(SiteDirective): regex = r'^[^ ]+\s[^ ]+$' group = SiteDirective.HTTPD unique_value = True + unique_location = True class Proxy(SiteDirective): @@ -77,6 +111,7 @@ class Proxy(SiteDirective): regex = r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$' group = SiteDirective.HTTPD unique_value = True + unique_location = True class ErrorDocument(SiteDirective): @@ -125,6 +160,7 @@ class SecRuleRemove(SiteDirective): help_text = _("Space separated ModSecurity rule IDs.") regex = r'^[0-9\s]+$' group = SiteDirective.SEC + unique_location = True class SecEngine(SiteDirective): @@ -143,6 +179,7 @@ class WordPressSaaS(SiteDirective): group = SiteDirective.SAAS regex = r'^/[^ ]*$' unique_value = True + unique_location = True class DokuWikiSaaS(SiteDirective): @@ -152,6 +189,7 @@ class DokuWikiSaaS(SiteDirective): group = SiteDirective.SAAS regex = r'^/[^ ]*$' unique_value = True + unique_location = True class DrupalSaaS(SiteDirective): @@ -161,3 +199,4 @@ class DrupalSaaS(SiteDirective): group = SiteDirective.SAAS regex = r'^/[^ ]*$' unique_value = True + unique_location = True diff --git a/orchestra/contrib/websites/forms.py b/orchestra/contrib/websites/forms.py index 7c241ea7..ccb68b42 100644 --- a/orchestra/contrib/websites/forms.py +++ b/orchestra/contrib/websites/forms.py @@ -1,8 +1,11 @@ +from collections import defaultdict + from django import forms from django.core.exceptions import ValidationError from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ +from .directives import SiteDirective from .validators import validate_domain_protocol @@ -24,24 +27,22 @@ class WebsiteAdminForm(forms.ModelForm): class WebsiteDirectiveInlineFormSet(forms.models.BaseInlineFormSet): - """ Validate uniqueness """ def clean(self): - values = {} + # directives formset cross-validation with contents for unique locations + locations = set() + for form in self.content_formset.forms: + location = form.cleaned_data.get('path') + if location is not None: + locations.add(location) + directives = [] + + values = defaultdict(list) for form in self.forms: - name = form.cleaned_data.get('name', None) - if name is not None: - directive = form.instance.directive_class - if directive.unique_name and name in values: - form.add_error(None, ValidationError( - _("Only one %s can be defined.") % directive.get_verbose_name() - )) - value = form.cleaned_data.get('value', None) - if value is not None: - if directive.unique_value and value in values.get(name, []): - form.add_error('value', ValidationError( - _("This value is already used by other %s.") % force_text(directive.get_verbose_name()) - )) + website = form.instance + directive = form.cleaned_data + if directive.get('name') is not None: try: - values[name].append(value) - except KeyError: - values[name] = [value] + website.directive_instance.validate_uniqueness(directive, values, locations) + except ValidationError as err: + for k,v in err.error_dict.items(): + form.add_error(k, v) diff --git a/orchestra/contrib/websites/serializers.py b/orchestra/contrib/websites/serializers.py index 5620ce9f..25476b4c 100644 --- a/orchestra/contrib/websites/serializers.py +++ b/orchestra/contrib/websites/serializers.py @@ -2,34 +2,28 @@ from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404 from rest_framework import serializers -from orchestra.api.serializers import HyperlinkedModelSerializer +from orchestra.api.serializers import HyperlinkedModelSerializer, RelatedHyperlinkedModelSerializer from orchestra.contrib.accounts.serializers import AccountSerializerMixin +from .directives import SiteDirective from .models import Website, Content, WebsiteDirective from .validators import validate_domain_protocol -class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): + +class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = Website.domains.field.rel.to fields = ('url', 'id', 'name') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) -class RelatedWebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class RelatedWebAppSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer): class Meta: model = Content.webapp.field.rel.to fields = ('url', 'id', 'name', 'type') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) -class ContentSerializer(serializers.HyperlinkedModelSerializer): +class ContentSerializer(serializers.ModelSerializer): webapp = RelatedWebAppSerializer() class Meta: @@ -53,9 +47,8 @@ class DirectiveSerializer(serializers.ModelSerializer): class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): - domains = RelatedDomainSerializer(many=True, required=False) #allow_add_remove=True - contents = ContentSerializer(required=False, many=True, #allow_add_remove=True, - source='content_set') + domains = RelatedDomainSerializer(many=True, required=False) + contents = ContentSerializer(required=False, many=True, source='content_set') directives = DirectiveSerializer(required=False) class Meta: @@ -63,15 +56,37 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): fields = ('url', 'id', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives') postonly_fileds = ('name',) - def full_clean(self, instance): + def validate(self, data): """ Prevent multiples domains on the same protocol """ - for domain in instance._m2m_data['domains']: + # Validate location and directive uniqueness + errors = [] + directives = data.get('directives', []) + if directives: + locations = set() + for content in data.get('content_set', []): + location = content.get('path') + if location is not None: + locations.add(location) + values = defaultdict(list) + for name, value in directives.items(): + directive = { + 'name': name, + 'value': value, + } + try: + SiteDirective.get(name).validate_uniqueness(directive, values, locations) + except ValidationError as err: + errors.append(err) + # Validate domain protocol uniqueness + instance = self.instance + for domain in data['domains']: try: - validate_domain_protocol(instance, domain, instance.protocol) - except ValidationError as e: - # TODO not sure about this one - self.add_error(None, e) - return instance + validate_domain_protocol(instance, domain, data['protocol']) + except ValidationError as err: + errors.append(err) + if errors: + raise ValidationError(errors) + return data def create(self, validated_data): directives_data = validated_data.pop('directives') @@ -80,9 +95,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): WebsiteDirective.objects.create(webapp=webapp, name=key, value=value) return webap - def update(self, instance, validated_data): - directives_data = validated_data.pop('directives') - instance = super(WebsiteSerializer, self).update(instance, validated_data) + def update_directives(self, instance, directives_data): existing = {} for obj in instance.directives.all(): existing[obj.name] = obj @@ -99,4 +112,19 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): directive.save(update_fields=('value',)) for to_delete in set(existing.keys())-posted: existing[to_delete].delete() + + def update_contents(self, instance, contents_data): + raise NotImplementedError + + def update_domains(self, instance, domains_data): + raise NotImplementedError + + def update(self, instance, validated_data): + directives_data = validated_data.pop('directives') + domains_data = validated_data.pop('domains') + contents_data = validated_data.pop('content_set') + instance = super(WebsiteSerializer, self).update(instance, validated_data) + self.update_directives(instance, directives_data) + self.update_contents(instance, contents_data) + self.update_domains(instance, domains_data) return instance diff --git a/orchestra/core/__init__.py b/orchestra/core/__init__.py index f29687bb..afeed785 100644 --- a/orchestra/core/__init__.py +++ b/orchestra/core/__init__.py @@ -21,16 +21,18 @@ class Register(object): kwargs['verbose_name'] = model._meta.verbose_name if 'verbose_name_plural' not in kwargs: kwargs['verbose_name_plural'] = model._meta.verbose_name_plural - self._registry[model] = AttrDict(**kwargs) + defaults = { + 'menu': True, + } + defaults.update(kwargs) + self._registry[model] = AttrDict(**defaults) def register_view(self, view_name, **kwargs): - if view_name in self._registry: - raise KeyError("%s already registered" % view_name) if 'verbose_name' not in kwargs: raise KeyError("%s verbose_name is required for views" % view_name) if 'verbose_name_plural' not in kwargs: kwargs['verbose_name_plural'] = string_concat(kwargs['verbose_name'], 's') - self._registry[view_name] = AttrDict(**kwargs) + self.register(view_name, **kwargs) def get(self, *args): if args: