Fixes on serializers DRF3 compat

This commit is contained in:
Marc Aymerich 2015-05-18 15:21:42 +00:00
parent 3963f6ce86
commit 907250d2e7
19 changed files with 241 additions and 131 deletions

20
TODO.md
View File

@ -369,3 +369,23 @@ pip3 install https://github.com/fantix/gevent/archive/master.zip
# user order_id as bill line id # user order_id as bill line id
# BUG Delete related services also deletes account! # 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

View File

@ -1,5 +1,6 @@
import copy import copy
from django.db import models
from django.forms import widgets from django.forms import widgets
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
@ -19,6 +20,8 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
def validate(self, attrs): def validate(self, attrs):
""" calls model.clean() """ """ calls model.clean() """
attrs = super(HyperlinkedModelSerializer, self).validate(attrs) attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
if isinstance(attrs, models.Model):
return attrs
validated_data = dict(attrs) validated_data = dict(attrs)
ModelClass = self.Meta.model ModelClass = self.Meta.model
# Remove many-to-many relationships from validated_data. # Remove many-to-many relationships from validated_data.
@ -39,9 +42,10 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
def post_only_cleanning(self, instance, validated_data): def post_only_cleanning(self, instance, validated_data):
""" removes postonly_fields from attrs """ """ removes postonly_fields from attrs """
model_attrs = dict(**validated_data) 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(): for attr, value in validated_data.items():
if attr in self.Meta.postonly_fields: if attr in post_only_fields:
model_attrs.pop(attr) model_attrs.pop(attr)
return model_attrs return model_attrs
@ -56,6 +60,21 @@ class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
return super(HyperlinkedModelSerializer, self).partial_update(instance, model_attrs) 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): class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
password = serializers.CharField(max_length=128, label=_('Password'), password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False, validators=[validate_password], write_only=True, required=False,

View File

@ -16,10 +16,12 @@ class AccountSerializerMixin(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AccountSerializerMixin, self).__init__(*args, **kwargs) super(AccountSerializerMixin, self).__init__(*args, **kwargs)
self.account = None self.account = None
def get_account(self):
request = self.context.get('request') request = self.context.get('request')
if request: if request:
self.account = request.user return request.user
def create(self, validated_data): def create(self, validated_data):
validated_data['account'] = self.account validated_data['account'] = self.get_account()
return super(AccountSerializerMixin, self).create(validated_data) return super(AccountSerializerMixin, self).create(validated_data)

View File

@ -170,12 +170,12 @@ a:hover {
} }
#lines .column-id { #lines .column-id {
width: 5%; width: 8%;
text-align: right; text-align: right;
} }
#lines .column-description { #lines .column-description {
width: 45%; width: 42%;
text-align: left; text-align: left;
} }

View File

@ -77,24 +77,32 @@
<span class="title column-subtotal">{% trans "subtotal" %}</span> <span class="title column-subtotal">{% trans "subtotal" %}</span>
<br> <br>
{% for line in lines %} {% for line in lines %}
{% with sublines=line.sublines.all %} {% with sublines=line.sublines.all description=line.description|slice:"40:" %}
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-id">{% if not line.order_id %}L{% endif %}{{ line.order_id }}</span>
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-description">{{ line.description|slice:":40" }}</span>
<span class="{% if not sublines %}last {% endif %}column-period">{{ line.get_verbose_period }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:"&nbsp;"|safe }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:"&nbsp;"|safe }}</span>
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span> <span class="{% if not sublines and not description %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span>
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span> <span class="{% if not sublines and not description %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
<br> <br>
{% for subline in sublines %} {% if description %}
<span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-id">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span> <span class="{% if not sublines %}last {% endif %}subline column-description">{{ description|truncatechars:41 }}</span>
<span class="{% if forloop.last %}last {% endif %}subline column-period">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-period">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-quantity">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-rate">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span> <span class="{% if not sublines %}last {% endif %}subline column-subtotal">&nbsp;</span>
<br> {% endif %}
{% endfor %} {% for subline in sublines %}
{% endwith %} <span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description|truncatechars:41 }}</span>
<span class="{% if forloop.last %}last {% endif %}subline column-period">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
<br>
{% endfor %}
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
<div id="totals"> <div id="totals">

View File

@ -3,21 +3,18 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers 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 orchestra.contrib.accounts.serializers import AccountSerializerMixin
from .models import Database, DatabaseUser from .models import Database, DatabaseUser
class RelatedDatabaseUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedDatabaseUserSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = DatabaseUser model = DatabaseUser
fields = ('url', 'id', 'username') 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): class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
users = RelatedDatabaseUserSerializer(many=True) #allow_add_remove=True users = RelatedDatabaseUserSerializer(many=True) #allow_add_remove=True
@ -35,15 +32,11 @@ class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
return attrs return attrs
class RelatedDatabaseSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedDatabaseSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = Database model = Database
fields = ('url', 'id', 'name',) 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): class DatabaseUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
databases = RelatedDatabaseSerializer(many=True, required=False) # allow_add_remove=True databases = RelatedDatabaseSerializer(many=True, required=False) # allow_add_remove=True

View File

@ -2,7 +2,7 @@ import re
from django import forms from django import forms
from django.contrib import admin 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 django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
@ -100,7 +100,10 @@ class DomainAdmin(AccountAdminMixin, ExtendedModelAdmin):
qs = super(DomainAdmin, self).get_queryset(request) qs = super(DomainAdmin, self).get_queryset(request)
qs = qs.select_related('top', 'account') qs = qs.select_related('top', 'account')
if request.method == 'GET': 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'): if apps.isinstalled('orchestra.contrib.websites'):
qs = qs.prefetch_related('websites') qs = qs.prefetch_related('websites')
return qs return qs

View File

@ -9,7 +9,7 @@ from .models import Domain
class BatchDomainCreationAdminForm(forms.ModelForm): class BatchDomainCreationAdminForm(forms.ModelForm):
name = forms.CharField(label=_("Names"), widget=forms.Textarea(attrs={'rows': 5, 'cols': 50}), 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): def clean_name(self):
self.extra_names = [] self.extra_names = []

View File

@ -162,7 +162,9 @@ class Domain(models.Model):
type=Record.SOA, type=Record.SOA,
value=' '.join(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 is_host:
if Record.MX not in types: if Record.MX not in types:
for mx in settings.DOMAINS_DEFAULT_MX: for mx in settings.DOMAINS_DEFAULT_MX:
@ -170,18 +172,19 @@ class Domain(models.Model):
type=Record.MX, type=Record.MX,
value=mx value=mx
)) ))
default_a = settings.DOMAINS_DEFAULT_A if not has_a and not has_aaaa:
if default_a and Record.A not in types: default_a = settings.DOMAINS_DEFAULT_A
records.append(AttrDict( if default_a:
type=Record.A, records.append(AttrDict(
value=default_a type=Record.A,
)) value=default_a
default_aaaa = settings.DOMAINS_DEFAULT_AAAA ))
if default_aaaa and Record.AAAA not in types: default_aaaa = settings.DOMAINS_DEFAULT_AAAA
records.append(AttrDict( if default_aaaa:
type=Record.AAAA, records.append(AttrDict(
value=default_aaaa type=Record.AAAA,
)) value=default_aaaa
))
return records return records
def render_records(self): def render_records(self):

View File

@ -36,11 +36,11 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
raise ValidationError(_("Can not create subdomains of other users domains")) raise ValidationError(_("Can not create subdomains of other users domains"))
return attrs return attrs
def full_clean(self, instance): def validate(self, data):
""" Checks if everything is consistent """ """ Checks if everything is consistent """
instance = super(DomainSerializer, self).full_clean(instance) data = super(DomainSerializer, self).validate(data)
if instance and instance.name: if self.instance and data.get('name'):
records = self.init_data.get('records', []) records = data['records']
domain = domain_for_validation(instance, records) domain = domain_for_validation(self.instance, records)
validators.validate_zone(domain.render_zone()) validators.validate_zone(domain.render_zone())
return instance return data

View File

@ -4,22 +4,18 @@ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import serializers 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.contrib.accounts.serializers import AccountSerializerMixin
from orchestra.core.validators import validate_password from orchestra.core.validators import validate_password
from .models import List from .models import List
class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = List.address_domain.field.rel.to model = List.address_domain.field.rel.to
fields = ('url', 'id', 'name') 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): class ListSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
password = serializers.CharField(max_length=128, label=_('Password'), password = serializers.CharField(max_length=128, label=_('Password'),

View File

@ -3,21 +3,17 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers 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.contrib.accounts.serializers import AccountSerializerMixin
from .models import Mailbox, Address from .models import Mailbox, Address
class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = Address.domain.field.rel.to model = Address.domain.field.rel.to
fields = ('url', 'id', 'name') 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): class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
domain = RelatedDomainSerializer() domain = RelatedDomainSerializer()
@ -42,15 +38,11 @@ class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer
postonly_fields = ('name', 'password') postonly_fields = ('name', 'password')
class RelatedMailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedMailboxSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = Mailbox model = Mailbox
fields = ('url', 'id', 'name') 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): class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
domain = RelatedDomainSerializer() domain = RelatedDomainSerializer()

View File

@ -15,8 +15,8 @@ class ResourceSerializer(serializers.ModelSerializer):
fields = ('name', 'used', 'allocated', 'unit') fields = ('name', 'used', 'allocated', 'unit')
read_only_fields = ('used',) read_only_fields = ('used',)
def from_native(self, raw_data, files=None): def to_internal_value(self, raw_data):
data = super(ResourceSerializer, self).from_native(raw_data, files=files) data = super(ResourceSerializer, self).to_internal_value(raw_data)
if not data.resource_id: if not data.resource_id:
data.resource = Resource.objects.get(name=raw_data['name']) data.resource = Resource.objects.get(name=raw_data['name'])
return data return data

View File

@ -3,25 +3,21 @@ from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers 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.contrib.accounts.serializers import AccountSerializerMixin
from .models import SystemUser from .models import SystemUser
from .validators import validate_home from .validators import validate_home
class GroupSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedGroupSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = SystemUser model = SystemUser
fields = ('url', 'id', 'username',) 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): class SystemUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
groups = GroupSerializer(many=True, required=False) groups = RelatedGroupSerializer(many=True, required=False)
class Meta: class Meta:
model = SystemUser model = SystemUser
@ -36,7 +32,7 @@ class SystemUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSeriali
username=attrs.get('username') or self.instance.username, username=attrs.get('username') or self.instance.username,
shell=attrs.get('shell') or self.instance.shell, shell=attrs.get('shell') or self.instance.shell,
) )
validate_home(user, attrs, self.account) validate_home(user, attrs, self.get_account())
return attrs return attrs
def validate_groups(self, attrs, source): def validate_groups(self, attrs, source):

View File

@ -112,5 +112,13 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
formfield.queryset = formfield.queryset.exclude(qset) formfield.queryset = formfield.queryset.exclude(qset)
return formfield 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) admin.site.register(Website, WebsiteAdmin)

View File

@ -1,4 +1,5 @@
import re import re
from collections import defaultdict
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -19,6 +20,7 @@ class SiteDirective(Plugin):
help_text = "" help_text = ""
unique_name = False unique_name = False
unique_value = False unique_value = False
unique_location = False
@classmethod @classmethod
@cached @cached
@ -50,6 +52,37 @@ class SiteDirective(Plugin):
for group, options in options.items(): for group, options in options.items():
yield (group, [(op.name, op.verbose_name) for op in options]) 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): def validate(self, website):
if self.regex and not re.match(self.regex, website.value): if self.regex and not re.match(self.regex, website.value):
raise ValidationError({ raise ValidationError({
@ -68,6 +101,7 @@ class Redirect(SiteDirective):
regex = r'^[^ ]+\s[^ ]+$' regex = r'^[^ ]+\s[^ ]+$'
group = SiteDirective.HTTPD group = SiteDirective.HTTPD
unique_value = True unique_value = True
unique_location = True
class Proxy(SiteDirective): class Proxy(SiteDirective):
@ -77,6 +111,7 @@ class Proxy(SiteDirective):
regex = r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$' regex = r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$'
group = SiteDirective.HTTPD group = SiteDirective.HTTPD
unique_value = True unique_value = True
unique_location = True
class ErrorDocument(SiteDirective): class ErrorDocument(SiteDirective):
@ -125,6 +160,7 @@ class SecRuleRemove(SiteDirective):
help_text = _("Space separated ModSecurity rule IDs.") help_text = _("Space separated ModSecurity rule IDs.")
regex = r'^[0-9\s]+$' regex = r'^[0-9\s]+$'
group = SiteDirective.SEC group = SiteDirective.SEC
unique_location = True
class SecEngine(SiteDirective): class SecEngine(SiteDirective):
@ -143,6 +179,7 @@ class WordPressSaaS(SiteDirective):
group = SiteDirective.SAAS group = SiteDirective.SAAS
regex = r'^/[^ ]*$' regex = r'^/[^ ]*$'
unique_value = True unique_value = True
unique_location = True
class DokuWikiSaaS(SiteDirective): class DokuWikiSaaS(SiteDirective):
@ -152,6 +189,7 @@ class DokuWikiSaaS(SiteDirective):
group = SiteDirective.SAAS group = SiteDirective.SAAS
regex = r'^/[^ ]*$' regex = r'^/[^ ]*$'
unique_value = True unique_value = True
unique_location = True
class DrupalSaaS(SiteDirective): class DrupalSaaS(SiteDirective):
@ -161,3 +199,4 @@ class DrupalSaaS(SiteDirective):
group = SiteDirective.SAAS group = SiteDirective.SAAS
regex = r'^/[^ ]*$' regex = r'^/[^ ]*$'
unique_value = True unique_value = True
unique_location = True

View File

@ -1,8 +1,11 @@
from collections import defaultdict
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .directives import SiteDirective
from .validators import validate_domain_protocol from .validators import validate_domain_protocol
@ -24,24 +27,22 @@ class WebsiteAdminForm(forms.ModelForm):
class WebsiteDirectiveInlineFormSet(forms.models.BaseInlineFormSet): class WebsiteDirectiveInlineFormSet(forms.models.BaseInlineFormSet):
""" Validate uniqueness """
def clean(self): 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: for form in self.forms:
name = form.cleaned_data.get('name', None) website = form.instance
if name is not None: directive = form.cleaned_data
directive = form.instance.directive_class if directive.get('name') is not None:
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())
))
try: try:
values[name].append(value) website.directive_instance.validate_uniqueness(directive, values, locations)
except KeyError: except ValidationError as err:
values[name] = [value] for k,v in err.error_dict.items():
form.add_error(k, v)

View File

@ -2,34 +2,28 @@ from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import serializers 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 orchestra.contrib.accounts.serializers import AccountSerializerMixin
from .directives import SiteDirective
from .models import Website, Content, WebsiteDirective from .models import Website, Content, WebsiteDirective
from .validators import validate_domain_protocol from .validators import validate_domain_protocol
class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class RelatedDomainSerializer(AccountSerializerMixin, RelatedHyperlinkedModelSerializer):
class Meta: class Meta:
model = Website.domains.field.rel.to model = Website.domains.field.rel.to
fields = ('url', 'id', 'name') 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, RelatedHyperlinkedModelSerializer):
class RelatedWebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Content.webapp.field.rel.to model = Content.webapp.field.rel.to
fields = ('url', 'id', 'name', 'type') 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.ModelSerializer):
class ContentSerializer(serializers.HyperlinkedModelSerializer):
webapp = RelatedWebAppSerializer() webapp = RelatedWebAppSerializer()
class Meta: class Meta:
@ -53,9 +47,8 @@ class DirectiveSerializer(serializers.ModelSerializer):
class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
domains = RelatedDomainSerializer(many=True, required=False) #allow_add_remove=True domains = RelatedDomainSerializer(many=True, required=False)
contents = ContentSerializer(required=False, many=True, #allow_add_remove=True, contents = ContentSerializer(required=False, many=True, source='content_set')
source='content_set')
directives = DirectiveSerializer(required=False) directives = DirectiveSerializer(required=False)
class Meta: class Meta:
@ -63,15 +56,37 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
fields = ('url', 'id', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives') fields = ('url', 'id', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives')
postonly_fileds = ('name',) postonly_fileds = ('name',)
def full_clean(self, instance): def validate(self, data):
""" Prevent multiples domains on the same protocol """ """ 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: try:
validate_domain_protocol(instance, domain, instance.protocol) validate_domain_protocol(instance, domain, data['protocol'])
except ValidationError as e: except ValidationError as err:
# TODO not sure about this one errors.append(err)
self.add_error(None, e) if errors:
return instance raise ValidationError(errors)
return data
def create(self, validated_data): def create(self, validated_data):
directives_data = validated_data.pop('directives') directives_data = validated_data.pop('directives')
@ -80,9 +95,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
WebsiteDirective.objects.create(webapp=webapp, name=key, value=value) WebsiteDirective.objects.create(webapp=webapp, name=key, value=value)
return webap return webap
def update(self, instance, validated_data): def update_directives(self, instance, directives_data):
directives_data = validated_data.pop('directives')
instance = super(WebsiteSerializer, self).update(instance, validated_data)
existing = {} existing = {}
for obj in instance.directives.all(): for obj in instance.directives.all():
existing[obj.name] = obj existing[obj.name] = obj
@ -99,4 +112,19 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
directive.save(update_fields=('value',)) directive.save(update_fields=('value',))
for to_delete in set(existing.keys())-posted: for to_delete in set(existing.keys())-posted:
existing[to_delete].delete() 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 return instance

View File

@ -21,16 +21,18 @@ class Register(object):
kwargs['verbose_name'] = model._meta.verbose_name kwargs['verbose_name'] = model._meta.verbose_name
if 'verbose_name_plural' not in kwargs: if 'verbose_name_plural' not in kwargs:
kwargs['verbose_name_plural'] = model._meta.verbose_name_plural 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): 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: if 'verbose_name' not in kwargs:
raise KeyError("%s verbose_name is required for views" % view_name) raise KeyError("%s verbose_name is required for views" % view_name)
if 'verbose_name_plural' not in kwargs: if 'verbose_name_plural' not in kwargs:
kwargs['verbose_name_plural'] = string_concat(kwargs['verbose_name'], 's') 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): def get(self, *args):
if args: if args: