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: