diff --git a/TODO.md b/TODO.md index 9c8b7a93..7991786a 100644 --- a/TODO.md +++ b/TODO.md @@ -135,19 +135,15 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * multiple domains creation; line separated domains * Move MU webapps to SaaS? -* DN: Transaction atomicity and backend failure - -* SaaS Icons - * offer to create mailbox on account creation - * init.d celery scripts -# Required-Start: $network $local_fs $remote_fs postgresql celeryd -# Required-Stop: $network $local_fs $remote_fs postgresql celeryd -* POST only fields (account, username, name) etc - +* POST only fields (account, username, name) etc http://inka-labs.com/blog/2013/04/18/post-only-fields-django-rest-framework/ * for list virtual_domains cleaning up we need to know the old domain name when a list changes its address domain, but this is not possible with the current design. - +* regenerate virtual_domains every time (configure a separate file for orchestra on postfix) * update_fields=[] doesn't trigger post save! + +* lists -> SaaS ? diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index c90065a4..2330fd69 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -235,7 +235,7 @@ class ChangePasswordAdminMixin(object): def change_password(self, request, id, form_url=''): if not self.has_change_permission(request): raise PermissionDenied - # TODO use this insetad of self.get_object() + # TODO use this insetad of self.get_object(), in other places user = get_object_or_404(self.get_queryset(request), pk=id) related = [] diff --git a/orchestra/api/serializers.py b/orchestra/api/serializers.py index 817431d5..e38b654a 100644 --- a/orchestra/api/serializers.py +++ b/orchestra/api/serializers.py @@ -10,6 +10,55 @@ class SetPasswordSerializer(serializers.Serializer): widget=widgets.PasswordInput, validators=[validate_password]) + +from rest_framework.serializers import (HyperlinkedModelSerializerOptions, + HyperlinkedModelSerializer) + + +class tHyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions): + """ Options for PostHyperlinkedModelSerializer """ + + def __init__(self, meta): + super(HyperlinkedModelSerializerOptions, self).__init__(meta) + self.postonly_fields = getattr(meta, 'postonly_fields', ()) + + +class HyperlinkedModelSerializer(HyperlinkedModelSerializer): + _options_class = HyperlinkedModelSerializerOptions + + def to_native(self, obj): + """ Serialize objects -> primitives. """ + ret = self._dict_class() + ret.fields = {} + + for field_name, field in self.fields.items(): + # Ignore all postonly_fields fron serialization + if field_name in self.opts.postonly_fields: + continue + field.initialize(parent=self, field_name=field_name) + key = self.get_field_key(field_name) + value = field.field_to_native(obj, field_name) + ret[key] = value + ret.fields[key] = field + return ret + + def restore_object(self, attrs, instance=None): + model_attrs, post_attrs = {}, {} + for attr, value in attrs.iteritems(): + if attr in self.opts.postonly_fields: + post_attrs[attr] = value + else: + model_attrs[attr] = value + obj = super(HyperlinkedModelSerializer, self).restore_object(model_attrs, instance) + # Method to process ignored postonly_fields + self.process_postonly_fields(obj, post_attrs) + return obj + + def process_postonly_fields(self, obj, post_attrs): + """ Placeholder method for processing data sent in POST. """ + pass + + class MultiSelectField(serializers.ChoiceField): widget = widgets.CheckboxSelectMultiple diff --git a/orchestra/apps/databases/serializers.py b/orchestra/apps/databases/serializers.py index a9c70dc6..6045317d 100644 --- a/orchestra/apps/databases/serializers.py +++ b/orchestra/apps/databases/serializers.py @@ -2,6 +2,7 @@ from django.forms import widgets from django.utils.translation import ugettext, ugettext_lazy as _ from rest_framework import serializers +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from orchestra.core.validators import validate_password @@ -17,12 +18,14 @@ class RelatedDatabaseUserSerializer(serializers.HyperlinkedModelSerializer): return DatabaseUser.objects.get(username=data['username']) -class DatabaseSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): users = RelatedDatabaseUserSerializer(many=True, allow_add_remove=True) + # TODO clean user.type = db.type class Meta: model = Database fields = ('url', 'name', 'type', 'users') + postonly_fields = ('name', 'type') class RelatedDatabaseSerializer(serializers.HyperlinkedModelSerializer): @@ -34,15 +37,17 @@ class RelatedDatabaseSerializer(serializers.HyperlinkedModelSerializer): return Database.objects.get(name=data['name']) -class DatabaseUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class DatabaseUserSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, widget=widgets.PasswordInput) databases = RelatedDatabaseSerializer(many=True, allow_add_remove=True, required=False) - + # TODO clean user.type = db.type + class Meta: model = DatabaseUser fields = ('url', 'username', 'password', 'type', 'databases') + postonly_fields = ('username', 'type') def save_object(self, obj, **kwargs): # FIXME this method will be called when saving nested serializers :( diff --git a/orchestra/apps/databases/tests/functional_tests/tests.py b/orchestra/apps/databases/tests/functional_tests/tests.py index 5b4fd725..95e64d85 100644 --- a/orchestra/apps/databases/tests/functional_tests/tests.py +++ b/orchestra/apps/databases/tests/functional_tests/tests.py @@ -165,6 +165,8 @@ class MySQLBackendMixin(object): """mysql mysql -e 'SELECT * FROM db WHERE db="%(name)s";'""" % context, display=False).stdout) self.assertEqual('', sshrun(self.MASTER_SERVER, """mysql mysql -e 'SELECT * FROM user WHERE user="%(username)s";'""" % context, display=False).stdout) + + # TODO remove used from database class RESTDatabaseMixin(DatabaseTestMixin): diff --git a/orchestra/apps/domains/serializers.py b/orchestra/apps/domains/serializers.py index dffc2dab..740882bf 100644 --- a/orchestra/apps/domains/serializers.py +++ b/orchestra/apps/domains/serializers.py @@ -1,6 +1,7 @@ from django.core.exceptions import ValidationError from rest_framework import serializers +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from .helpers import domain_for_validation @@ -17,13 +18,14 @@ class RecordSerializer(serializers.ModelSerializer): return data.get('value') -class DomainSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): """ Validates if this zone generates a correct zone file """ records = RecordSerializer(required=False, many=True, allow_add_remove=True) class Meta: model = Domain - fields = ('url', 'id', 'name', 'records') + fields = ('url', 'name', 'records') + postonly_fields = ('name',) def full_clean(self, instance): """ Checks if everything is consistent """ diff --git a/orchestra/apps/lists/serializers.py b/orchestra/apps/lists/serializers.py index cdebcccd..da6377d8 100644 --- a/orchestra/apps/lists/serializers.py +++ b/orchestra/apps/lists/serializers.py @@ -2,15 +2,14 @@ from django.forms import widgets from django.utils.translation import ugettext, ugettext_lazy as _ from rest_framework import serializers +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from orchestra.core.validators import validate_password from .models import List -# TODO create PasswordSerializerMixin - -class ListSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class ListSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, required=False, widget=widgets.PasswordInput) @@ -18,6 +17,7 @@ class ListSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeriali class Meta: model = List fields = ('url', 'name', 'address_name', 'address_domain', 'admin_email') + postonly_fields = ('name',) def validate_password(self, attrs, source): """ POST only password """ diff --git a/orchestra/apps/mails/serializers.py b/orchestra/apps/mails/serializers.py index 30fcaa27..fe053de1 100644 --- a/orchestra/apps/mails/serializers.py +++ b/orchestra/apps/mails/serializers.py @@ -2,13 +2,14 @@ from django.forms import widgets from django.utils.translation import ugettext, ugettext_lazy as _ from rest_framework import serializers +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from orchestra.core.validators import validate_password from .models import Mailbox, Address -class MailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class MailboxSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, required=False, widget=widgets.PasswordInput) @@ -18,6 +19,7 @@ class MailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri fields = ( 'url', 'name', 'password', 'filtering', 'custom_filtering', 'addresses', 'is_active' ) + postonly_fields = ('name',) def validate_password(self, attrs, source): """ POST only password """ @@ -54,4 +56,3 @@ class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri if not attrs['mailboxes'] and not attrs['forward']: raise serializers.ValidationError("mailboxes or forward addresses should be provided") return attrs - diff --git a/orchestra/apps/systemusers/serializers.py b/orchestra/apps/systemusers/serializers.py index 3d2c923d..9a1911a3 100644 --- a/orchestra/apps/systemusers/serializers.py +++ b/orchestra/apps/systemusers/serializers.py @@ -3,6 +3,7 @@ from django.forms import widgets from django.utils.translation import ugettext, ugettext_lazy as _ from rest_framework import serializers +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from orchestra.core.validators import validate_password @@ -18,7 +19,7 @@ class GroupSerializer(serializers.ModelSerializer): return SystemUser.objects.get(username=data['username']) -class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class SystemUserSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): password = serializers.CharField(max_length=128, label=_('Password'), validators=[validate_password], write_only=True, required=False, widget=widgets.PasswordInput) @@ -29,6 +30,7 @@ class SystemUserSerializer(AccountSerializerMixin, serializers.HyperlinkedModelS fields = ( 'url', 'username', 'password', 'home', 'shell', 'groups', 'is_active', ) + postonly_fields = ('username',) def validate_password(self, attrs, source): """ POST only password """ diff --git a/orchestra/apps/webapps/serializers.py b/orchestra/apps/webapps/serializers.py index 111abcae..c11d675a 100644 --- a/orchestra/apps/webapps/serializers.py +++ b/orchestra/apps/webapps/serializers.py @@ -1,14 +1,16 @@ from rest_framework import serializers from orchestra.api.fields import OptionField +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from .models import WebApp -class WebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): options = OptionField(required=False) class Meta: model = WebApp fields = ('url', 'name', 'type', 'options') + postonly_fields = ('name', 'type') diff --git a/orchestra/apps/websites/serializers.py b/orchestra/apps/websites/serializers.py index 1b0e79d3..94ab20f6 100644 --- a/orchestra/apps/websites/serializers.py +++ b/orchestra/apps/websites/serializers.py @@ -1,6 +1,7 @@ from rest_framework import serializers from orchestra.api.fields import OptionField +from orchestra.api.serializers import HyperlinkedModelSerializer from orchestra.apps.accounts.serializers import AccountSerializerMixin from .models import Website, Content @@ -15,7 +16,7 @@ class ContentSerializer(serializers.HyperlinkedModelSerializer): return '%s-%s' % (data.get('website'), data.get('path')) -class WebsiteSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): +class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): contents = ContentSerializer(required=False, many=True, allow_add_remove=True, source='content_set') options = OptionField(required=False) @@ -23,3 +24,4 @@ class WebsiteSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeri class Meta: model = Website fields = ('url', 'name', 'port', 'domains', 'is_active', 'contents', 'options') + postonly_fileds = ('name',)