Fixes on serializers DRF3 compat
This commit is contained in:
parent
3963f6ce86
commit
907250d2e7
20
TODO.md
20
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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -77,24 +77,32 @@
|
|||
<span class="title column-subtotal">{% trans "subtotal" %}</span>
|
||||
<br>
|
||||
{% for line in lines %}
|
||||
{% with sublines=line.sublines.all %}
|
||||
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||
<br>
|
||||
{% for subline in sublines %}
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-period"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-quantity"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-rate"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% with sublines=line.sublines.all description=line.description|slice:"40:" %}
|
||||
<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 and not description %}last {% endif %}column-description">{{ line.description|slice:":40" }}</span>
|
||||
<span class="{% if not sublines and not description %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
|
||||
<span class="{% if not sublines and not description %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
||||
<span class="{% if not sublines and not description %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
||||
<span class="{% if not sublines and not description %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||
<br>
|
||||
{% if description %}
|
||||
<span class="{% if not sublines %}last {% endif %}subline column-id"> </span>
|
||||
<span class="{% if not sublines %}last {% endif %}subline column-description">{{ description|truncatechars:41 }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}subline column-period"> </span>
|
||||
<span class="{% if not sublines %}last {% endif %}subline column-quantity"> </span>
|
||||
<span class="{% if not sublines %}last {% endif %}subline column-rate"> </span>
|
||||
<span class="{% if not sublines %}last {% endif %}subline column-subtotal"> </span>
|
||||
{% endif %}
|
||||
{% for subline in sublines %}
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </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"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-quantity"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-rate"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="totals">
|
||||
|
|
|
@ -3,21 +3,18 @@ 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):
|
||||
users = RelatedDatabaseUserSerializer(many=True) #allow_add_remove=True
|
||||
|
@ -35,15 +32,11 @@ 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):
|
||||
databases = RelatedDatabaseSerializer(many=True, required=False) # allow_add_remove=True
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,22 +4,18 @@ 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):
|
||||
password = serializers.CharField(max_length=128, label=_('Password'),
|
||||
|
|
|
@ -3,21 +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 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):
|
||||
domain = RelatedDomainSerializer()
|
||||
|
@ -42,15 +38,11 @@ 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):
|
||||
domain = RelatedDomainSerializer()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -112,5 +112,13 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue