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
|
# 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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,17 +77,25 @@
|
||||||
<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:" "|safe }}</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 %}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-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>
|
<span class="{% if not sublines and not description %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||||
<br>
|
<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 %}
|
{% for subline in sublines %}
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-id"> </span>
|
<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-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-period"> </span>
|
||||||
<span class="{% if forloop.last %}last {% endif %}subline column-quantity"> </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-rate"> </span>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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,14 +172,15 @@ class Domain(models.Model):
|
||||||
type=Record.MX,
|
type=Record.MX,
|
||||||
value=mx
|
value=mx
|
||||||
))
|
))
|
||||||
|
if not has_a and not has_aaaa:
|
||||||
default_a = settings.DOMAINS_DEFAULT_A
|
default_a = settings.DOMAINS_DEFAULT_A
|
||||||
if default_a and Record.A not in types:
|
if default_a:
|
||||||
records.append(AttrDict(
|
records.append(AttrDict(
|
||||||
type=Record.A,
|
type=Record.A,
|
||||||
value=default_a
|
value=default_a
|
||||||
))
|
))
|
||||||
default_aaaa = settings.DOMAINS_DEFAULT_AAAA
|
default_aaaa = settings.DOMAINS_DEFAULT_AAAA
|
||||||
if default_aaaa and Record.AAAA not in types:
|
if default_aaaa:
|
||||||
records.append(AttrDict(
|
records.append(AttrDict(
|
||||||
type=Record.AAAA,
|
type=Record.AAAA,
|
||||||
value=default_aaaa
|
value=default_aaaa
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
try:
|
||||||
validate_domain_protocol(instance, domain, instance.protocol)
|
SiteDirective.get(name).validate_uniqueness(directive, values, locations)
|
||||||
except ValidationError as e:
|
except ValidationError as err:
|
||||||
# TODO not sure about this one
|
errors.append(err)
|
||||||
self.add_error(None, e)
|
# Validate domain protocol uniqueness
|
||||||
return instance
|
instance = self.instance
|
||||||
|
for domain in data['domains']:
|
||||||
|
try:
|
||||||
|
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):
|
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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue