Upgraded to DRF3

This commit is contained in:
Marc Aymerich 2015-04-23 14:34:04 +00:00
parent fbb3c6ab03
commit 1a78317028
52 changed files with 374 additions and 348 deletions

View File

@ -285,4 +285,4 @@ https://code.djangoproject.com/ticket/24576
# replace multichoicefield and jsonfield by ArrayField, HStoreField
# Amend lines???
# Add icon on select contact view

View File

@ -1,12 +1,12 @@
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from .serializers import SetPasswordSerializer
class SetPasswordApiMixin(object):
@action(serializer_class=SetPasswordSerializer)
@detail_route(serializer_class=SetPasswordSerializer)
def set_password(self, request, pk):
obj = self.get_object()
data = request.DATA

View File

@ -1,51 +0,0 @@
import json
from rest_framework import serializers, exceptions
class OptionField(serializers.WritableField):
"""
Dict-like representation of a OptionField
A bit hacky, objects get deleted on from_native method and Serializer will
need a custom override of restore_object method.
"""
def to_native(self, value):
""" dict-like representation of a Property Model"""
return dict((prop.name, prop.value) for prop in value.all())
def from_native(self, value):
""" Convert a dict-like representation back to a WebOptionField """
parent = self.parent
related_manager = getattr(parent.object, self.source or 'options', False)
properties = serializers.RelationsList()
if value:
model = getattr(parent.opts.model, self.source or 'options').related.model
if isinstance(value, str):
try:
value = json.loads(value)
except:
raise exceptions.ParseError("Malformed property: %s" % str(value))
if not related_manager:
# POST (new parent object)
return [model(name=n, value=v) for n,v in value.items()]
# PUT
to_save = []
for (name, value) in value.items():
try:
# Update existing property
prop = related_manager.get(name=name)
except model.DoesNotExist:
# Create a new one
prop = model(name=name, value=value)
else:
prop.value = value
to_save.append(prop.pk)
properties.append(prop)
# Discart old values
if related_manager:
properties._deleted = [] # Redefine class attribute
for obj in related_manager.all():
if not value or obj.pk not in to_save:
properties._deleted.append(obj)
return properties

View File

@ -3,7 +3,7 @@ from rest_framework.reverse import reverse
def link_wrap(view, view_names):
def wrapper(self, request, view=view, *args, **kwargs):
def wrapper(self, request, *args, **kwargs):
""" wrapper function that inserts HTTP links on view """
links = []
for name in view_names:

View File

@ -12,37 +12,42 @@ from .helpers import insert_links
class LogApiMixin(object):
def post(self, request, *args, **kwargs):
def create(self, request, *args, **kwargs):
from django.contrib.admin.models import ADDITION
response = super(LogApiMixin, self).post(request, *args, **kwargs)
response = super(LogApiMixin, self).create(request, *args, **kwargs)
message = _('Added.')
self.log_addition(request, message, ADDITION)
self.log(request, message, ADDITION, instance=self.serializer.instance)
return response
def put(self, request, *args, **kwargs):
def perform_create(self, serializer):
""" stores serializer for accessing instance on create() """
super(LogApiMixin, self).perform_create(serializer)
self.serializer = serializer
def update(self, request, *args, **kwargs):
from django.contrib.admin.models import CHANGE
response = super(LogApiMixin, self).put(request, *args, **kwargs)
message = _('Changed')
response = super(LogApiMixin, self).update(request, *args, **kwargs)
message = _('Changed data')
self.log(request, message, CHANGE)
return response
def patch(self, request, *args, **kwargs):
def partial_update(self, request, *args, **kwargs):
from django.contrib.admin.models import CHANGE
response = super(LogApiMixin, self).put(request, *args, **kwargs)
response = super(LogApiMixin, self).partial_update(request, *args, **kwargs)
message = _('Changed %s') % str(response.data)
self.log(request, message, CHANGE)
return response
def delete(self, request, *args, **kwargs):
def destroy(self, request, *args, **kwargs):
from django.contrib.admin.models import DELETION
message = _('Deleted')
self.log(request, message, DELETION)
response = super(LogApiMixin, self).put(request, *args, **kwargs)
response = super(LogApiMixin, self).destroy(request, *args, **kwargs)
return response
def log(self, request, message, action):
def log(self, request, message, action, instance=None):
from django.contrib.admin.models import LogEntry
instance = self.get_object()
instance = instance or self.get_object()
LogEntry.objects.log_action(
user_id=request.user.pk,
content_type_id=get_content_type_for_model(instance).pk,
@ -69,7 +74,7 @@ class LinkHeaderRouter(DefaultRouter):
def get_viewset(self, prefix_or_model):
for _prefix, viewset, __ in self.registry:
if _prefix == prefix_or_model or viewset.model == prefix_or_model:
if _prefix == prefix_or_model or viewset.queryset.model == prefix_or_model:
return viewset
msg = "%s does not have a regiestered viewset" % prefix_or_model
raise KeyError(msg)
@ -80,7 +85,7 @@ class LinkHeaderRouter(DefaultRouter):
# setattr(viewset, 'inserted', getattr(viewset, 'inserted', []))
if viewset.serializer_class is None:
viewset.serializer_class = viewset().get_serializer_class()
viewset.serializer_class.base_fields.update({name: field(**kwargs)})
viewset.serializer_class._declared_fields.update({name: field(**kwargs)})
# if not name in viewset.inserted:
viewset.serializer_class.Meta.fields += (name,)
# viewset.inserted.append(name)

View File

@ -38,7 +38,7 @@ class APIRoot(views.APIView):
kwargs = {}
url = reverse(url_name, request=request, format=format, kwargs=kwargs)
links.append('<%s>; rel="%s"' % (url, url_name))
model = viewset.model
model = viewset.queryset.model
group = None
if model in services:
group = 'services'
@ -61,10 +61,10 @@ class APIRoot(views.APIView):
})
return Response(body, headers=headers)
def metadata(self, request):
ret = super(APIRoot, self).metadata(request)
ret['settings'] = {
def options(self, request):
metadata = super(APIRoot, self).options(request)
metadata.data['settings'] = {
name.lower(): getattr(settings, name, None)
for name in self.names
}
return ret
return metadata

View File

@ -7,47 +7,90 @@ from ..core.validators import validate_password
class SetPasswordSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, label=_('Password'),
widget=widgets.PasswordInput, validators=[validate_password])
style={'widget': widgets.PasswordInput}, validators=[validate_password])
class HyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
def __init__(self, meta):
super(HyperlinkedModelSerializerOptions, self).__init__(meta)
self.postonly_fields = getattr(meta, 'postonly_fields', ())
#class HyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
# def __init__(self, meta):
# super(HyperlinkedModelSerializerOptions, self).__init__(meta)
# self.postonly_fields = getattr(meta, 'postonly_fields', ())
class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
""" support for postonly_fields, fields whose value can only be set on post """
_options_class = HyperlinkedModelSerializerOptions
# _options_class = HyperlinkedModelSerializerOptions
def restore_object(self, attrs, instance=None):
def validate(self, attrs):
""" calls model.clean() """
attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
instance = self.Meta.model(**attrs)
instance.clean()
return attrs
# TODO raise validationError instead of silently removing fields
def update(self, instance, validated_data):
""" removes postonly_fields from attrs when not posting """
model_attrs = dict(**attrs)
model_attrs = dict(**validated_data)
if instance is not None:
for attr, value in attrs.items():
if attr in self.opts.postonly_fields:
for attr, value in validated_data.items():
if attr in self.Meta.postonly_fields:
model_attrs.pop(attr)
return super(HyperlinkedModelSerializer, self).restore_object(model_attrs, instance)
return super(HyperlinkedModelSerializer, self).update(instance, model_attrs)
class MultiSelectField(serializers.ChoiceField):
widget = widgets.CheckboxSelectMultiple
class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False,
style={'widget': widgets.PasswordInput})
def field_from_native(self, data, files, field_name, into):
""" convert multiselect data into comma separated string """
if field_name in data:
data = data.copy()
try:
# data is a querydict when using forms
data[field_name] = ','.join(data.getlist(field_name))
except AttributeError:
data[field_name] = ','.join(data[field_name])
return super(MultiSelectField, self).field_from_native(data, files, field_name, into)
def validate_password(self, attrs, source):
""" POST only password """
if self.instance:
if 'password' in attrs:
raise serializers.ValidationError(_("Can not set password"))
elif 'password' not in attrs:
raise serializers.ValidationError(_("Password required"))
return attrs
def valid_value(self, value):
""" checks for each item if is a valid value """
for val in value.split(','):
valid = super(MultiSelectField, self).valid_value(val)
if not valid:
return False
return True
def validate(self, attrs):
""" remove password in case is not a real model field """
try:
self.Meta.model._meta.get_field_by_name('password')
except models.FieldDoesNotExist:
pass
else:
password = attrs.pop('password', None)
attrs = super(SetPasswordSerializer, self).validate()
if password is not None:
attrs['password'] = password
return attrs
def create(self, validated_data):
password = validated_data.pop('password')
instance = self.Meta.model(**validated_data)
instance.set_password(password)
instance.save()
return instance
#class MultiSelectField(serializers.ChoiceField):
# widget = widgets.CheckboxSelectMultiple
#
# def field_from_native(self, data, files, field_name, into):
# """ convert multiselect data into comma separated string """
# if field_name in data:
# data = data.copy()
# try:
# # data is a querydict when using forms
# data[field_name] = ','.join(data.getlist(field_name))
# except AttributeError:
# data[field_name] = ','.join(data[field_name])
# return super(MultiSelectField, self).field_from_native(data, files, field_name, into)
#
# def valid_value(self, value):
# """ checks for each item if is a valid value """
# for val in value.split(','):
# valid = super(MultiSelectField, self).valid_value(val)
# if not valid:
# return False
# return True

View File

@ -147,11 +147,11 @@ function install_requirements () {
kombu==3.0.23 \
billiard==3.3.0.18 \
Markdown==2.4 \
djangorestframework==2.4.4 \
djangorestframework==3.1.1 \
paramiko==1.15.1 \
ecdsa==0.11 \
Pygments==1.6 \
django-filter==0.7 \
django-filter==0.9.2 \
passlib==1.6.2 \
jsonfield==0.9.22 \
lxml==3.3.5 \

View File

@ -14,7 +14,7 @@ class AccountApiMixin(object):
class AccountViewSet(LogApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
model = Account
queryset = Account.objects.all()
serializer_class = AccountSerializer
singleton_pk = lambda _,request: request.user.pk

View File

@ -20,6 +20,6 @@ class AccountSerializerMixin(object):
if request:
self.account = request.user
def save_object(self, obj, **kwargs):
obj.account = self.account
super(AccountSerializerMixin, self).save_object(obj, **kwargs)
def create(self, validated_data):
validated_data['account'] = self.account
return super(AccountSerializerMixin, self).create(validated_data)

View File

@ -12,7 +12,7 @@ from .serializers import BillSerializer
class BillViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Bill
queryset = Bill.objects.all()
serializer_class = BillSerializer
@detail_route(methods=['get'])

View File

@ -8,7 +8,7 @@ from .serializers import ContactSerializer
class ContactViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Contact
queryset = Contact.objects.all()
serializer_class = ContactSerializer

View File

@ -9,7 +9,7 @@ class EmailUsageListFilter(SimpleListFilter):
parameter_name = 'email_usages'
def lookups(self, request, model_admin):
return Contact.email_usage.field.choices
return Contact.EMAIL_USAGES
def queryset(self, request, queryset):
value = self.value()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,14 @@
from rest_framework import serializers
from orchestra.api.serializers import MultiSelectField
#from orchestra.api.serializers import MultiSelectField
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from .models import Contact
class ContactSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
email_usage = MultiSelectField(choices=Contact.EMAIL_USAGES)
email_usage = serializers.MultipleChoiceField(choices=Contact.EMAIL_USAGES)
class Meta:
model = Contact
fields = (

View File

@ -8,13 +8,13 @@ from .serializers import DatabaseSerializer, DatabaseUserSerializer
class DatabaseViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Database
queryset = Database.objects.all()
serializer_class = DatabaseSerializer
filter_fields = ('name',)
class DatabaseUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
model = DatabaseUser
queryset = DatabaseUser.objects.all()
serializer_class = DatabaseUserSerializer
filter_fields = ('username',)

View File

@ -3,9 +3,8 @@ 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
from orchestra.api.serializers import HyperlinkedModelSerializer, SetPasswordHyperlinkedSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from orchestra.core.validators import validate_password
from .models import Database, DatabaseUser
@ -21,7 +20,7 @@ class RelatedDatabaseUserSerializer(AccountSerializerMixin, serializers.Hyperlin
class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
users = RelatedDatabaseUserSerializer(many=True, allow_add_remove=True)
users = RelatedDatabaseUserSerializer(many=True) #allow_add_remove=True
class Meta:
model = Database
@ -29,6 +28,7 @@ class DatabaseSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
postonly_fields = ('name', 'type')
def validate(self, attrs):
attrs = super(DatabaseSerializer, self).validate(attrs)
for user in attrs['users']:
if user.type != attrs['type']:
raise serializers.ValidationError("User type must be" % attrs['type'])
@ -45,25 +45,17 @@ class RelatedDatabaseSerializer(AccountSerializerMixin, serializers.HyperlinkedM
return get_object_or_404(queryset, name=data['name'])
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)
class DatabaseUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
databases = RelatedDatabaseSerializer(many=True, required=False) # allow_add_remove=True
class Meta:
model = DatabaseUser
fields = ('url', 'username', 'password', 'type', 'databases')
postonly_fields = ('username', 'type')
postonly_fields = ('username', 'type', 'password')
def validate(self, attrs):
attrs = super(DatabaseUserSerializer, self).validate(attrs)
for database in attrs.get('databases', []):
if database.type != attrs['type']:
raise serializers.ValidationError("Database type must be" % attrs['type'])
return attrs
def save_object(self, obj, **kwargs):
# FIXME this method will be called when saving nested serializers :(
if not obj.pk:
obj.set_password(obj.password)
super(DatabaseUserSerializer, self).save_object(obj, **kwargs)

View File

@ -1,5 +1,5 @@
from rest_framework import viewsets
from rest_framework.decorators import link
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from orchestra.api import router
@ -11,28 +11,28 @@ from .serializers import DomainSerializer
class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
model = Domain
serializer_class = DomainSerializer
filter_fields = ('name',)
queryset = Domain.objects.all()
def get_queryset(self):
qs = super(DomainViewSet, self).get_queryset()
return qs.prefetch_related('records')
@link()
@detail_route()
def view_zone(self, request, pk=None):
domain = self.get_object()
return Response({
'zone': domain.render_zone()
})
def metadata(self, request):
ret = super(DomainViewSet, self).metadata(request)
def options(self, request):
metadata = super(DomainViewSet, self).options(request)
names = ['DOMAINS_DEFAULT_A', 'DOMAINS_DEFAULT_MX', 'DOMAINS_DEFAULT_NS']
ret['settings'] = {
metadata.data['settings'] = {
name.lower(): getattr(settings, name, None) for name in names
}
return ret
return metadata
router.register(r'domains', DomainViewSet)

View File

@ -21,7 +21,7 @@ class RecordSerializer(serializers.ModelSerializer):
class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
""" Validates if this zone generates a correct zone file """
records = RecordSerializer(required=False, many=True, allow_add_remove=True)
records = RecordSerializer(required=False, many=True) #allow_add_remove=True)
class Meta:
model = Domain

View File

@ -1,5 +1,5 @@
from rest_framework import viewsets, mixins
from rest_framework.decorators import action
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from orchestra.api import router, LogApiMixin
@ -10,16 +10,16 @@ from .serializers import TicketSerializer, QueueSerializer
class TicketViewSet(LogApiMixin, viewsets.ModelViewSet):
model = Ticket
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
@action()
@detail_route()
def mark_as_read(self, request, pk=None):
ticket = self.get_object()
ticket.mark_as_read_by(request.user)
return Response({'status': 'Ticket marked as read'})
@action()
@detail_route()
def mark_as_unread(self, request, pk=None):
ticket = self.get_object()
ticket.mark_as_unread_by(request.user)
@ -36,7 +36,7 @@ class QueueViewSet(LogApiMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
model = Queue
queryset = Queue.objects.all()
serializer_class = QueueSerializer

View File

@ -27,7 +27,7 @@ class MessageSerializer(serializers.HyperlinkedModelSerializer):
class TicketSerializer(serializers.HyperlinkedModelSerializer):
""" Validates if this zone generates a correct zone file """
messages = MessageSerializer(required=False, many=True)
is_read = serializers.SerializerMethodField('get_is_read')
is_read = serializers.SerializerMethodField()
class Meta:
model = Ticket

View File

@ -8,7 +8,7 @@ from .serializers import ListSerializer
class ListViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
model = List
queryset = List.objects.all()
serializer_class = ListSerializer
filter_fields = ('name',)

View File

@ -8,6 +8,17 @@ from orchestra.core.validators import validate_name
from . import settings
class ListQuerySet(models.QuerySet):
def create(self, **kwargs):
""" Sets password if provided, all within a single DB operation """
password = kwargs.pop('password')
instance = self.model(**kwargs)
if password:
instance.set_password(password)
instance.save()
return instance
# TODO address and domain, perhaps allow only domain?
class List(models.Model):
@ -27,6 +38,8 @@ class List(models.Model):
"Unselect this instead of deleting accounts."))
password = None
objects = ListQuerySet.as_manager()
class Meta:
unique_together = ('address_name', 'address_domain')

View File

@ -1,9 +1,10 @@
from django.core.validators import RegexValidator
from django.forms import widgets
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 HyperlinkedModelSerializer
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from orchestra.core.validators import validate_password
@ -20,38 +21,31 @@ class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedMod
return get_object_or_404(queryset, name=data['name'])
class ListSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
class ListSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False,
widget=widgets.PasswordInput)
write_only=True, style={'widget': widgets.PasswordInput},
validators=[
validate_password,
RegexValidator(r'^[^"\'\\]+$',
_('Enter a valid password. '
'This value may contain any ascii character except for '
' \'/"/\\/ characters.'), 'invalid'),
])
address_domain = RelatedDomainSerializer(required=False)
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 """
if self.object:
if 'password' in attrs:
raise serializers.ValidationError(_("Can not set password"))
elif 'password' not in attrs:
raise serializers.ValidationError(_("Password required"))
return attrs
fields = ('url', 'name', 'password', 'address_name', 'address_domain', 'admin_email')
postonly_fields = ('name', 'password')
def validate_address_domain(self, attrs, source):
address_domain = attrs.get(source)
address_name = attrs.get('address_name')
if self.object:
address_domain = address_domain or self.object.address_domain
address_name = address_name or self.object.address_name
if self.instance:
address_domain = address_domain or self.instance.address_domain
address_name = address_name or self.instance.address_name
if address_name and not address_domain:
raise serializers.ValidationError(
_("address_domains should should be provided when providing an addres_name"))
return attrs
def save_object(self, obj, **kwargs):
if not obj.pk:
obj.set_password(self.init_data.get('password', ''))
super(ListSerializer, self).save_object(obj, **kwargs)

View File

@ -8,13 +8,13 @@ from .serializers import AddressSerializer, MailboxSerializer
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Address
queryset = Address.objects.all()
serializer_class = AddressSerializer
class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Mailbox
queryset = Mailbox.objects.all()
serializer_class = MailboxSerializer

View File

@ -54,7 +54,7 @@ class UNIXUserMaildirBackend(ServiceController):
def delete(self, mailbox):
context = self.get_context(mailbox)
self.append('mv %(home)s %(home)s.deleted' % context)
self.append('mv %(home)s %(home)s.deleted || exit_code=1' % context)
self.append(textwrap.dedent("""
{ sleep 2 && killall -u %(user)s -s KILL; } &
killall -u %(user)s || true

View File

@ -3,9 +3,8 @@ 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
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from orchestra.core.validators import validate_password
from .models import Mailbox, Address
@ -32,10 +31,7 @@ class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedMo
return get_object_or_404(queryset, name=data['name'])
class MailboxSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False,
widget=widgets.PasswordInput)
class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
addresses = RelatedAddressSerializer(many=True, read_only=True)
class Meta:
@ -43,22 +39,7 @@ class MailboxSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
fields = (
'url', 'name', 'password', 'filtering', 'custom_filtering', 'addresses', 'is_active'
)
postonly_fields = ('name',)
def validate_password(self, attrs, source):
""" POST only password """
if self.object:
if 'password' in attrs:
raise serializers.ValidationError(_("Can not set password"))
elif 'password' not in attrs:
raise serializers.ValidationError(_("Password required"))
return attrs
def save_object(self, obj, **kwargs):
# FIXME this method will be called when saving nested serializers :(
if not obj.pk:
obj.set_password(obj.password)
super(MailboxSerializer, self).save_object(obj, **kwargs)
postonly_fields = ('name', 'password')
class RelatedMailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
@ -73,13 +54,14 @@ class RelatedMailboxSerializer(AccountSerializerMixin, serializers.HyperlinkedMo
class AddressSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
domain = RelatedDomainSerializer()
mailboxes = RelatedMailboxSerializer(many=True, allow_add_remove=True, required=False)
mailboxes = RelatedMailboxSerializer(many=True, required=False) #allow_add_remove=True
class Meta:
model = Address
fields = ('url', 'name', 'domain', 'mailboxes', 'forward')
def validate(self, attrs):
attrs = super(AddressSerializer, self).validate(attrs)
if not attrs['mailboxes'] and not attrs['forward']:
raise serializers.ValidationError("A mailbox or forward address should be provided.")
return attrs

View File

@ -25,7 +25,9 @@ class Operation():
self.backend = backend
# instance should maintain any dynamic attribute until backend execution
# deep copy is prefered over copy otherwise objects will share same atributes (queryset cache)
print('aa', getattr(instance, 'password', 'NOOOO'), id(instance))
self.instance = copy.deepcopy(instance)
print('aa', getattr(self.instance, 'password', 'NOOOO'), id(self.instance))
self.action = action
self.servers = servers

View File

@ -16,6 +16,7 @@ from .models import BackendLog
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
def post_save_collector(sender, *args, **kwargs):
if sender not in [BackendLog, Operation]:
instance = kwargs.get('instance')
OperationsMiddleware.collect(Operation.SAVE, **kwargs)

View File

@ -8,7 +8,8 @@ from .serializers import OrderSerializer
class OrderViewSet(AccountApiMixin, viewsets.ModelViewSet):
model = Order
queryset = Order.objects.all()
serializer_class = OrderSerializer
router.register(r'orders', OrderViewSet)

View File

@ -8,13 +8,13 @@ from .serializers import PaymentSourceSerializer, TransactionSerializer
class PaymentSourceViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = PaymentSource
serializer_class = PaymentSourceSerializer
queryset = PaymentSource.objects.all()
class TransactionViewSet(LogApiMixin, viewsets.ModelViewSet):
model = Transaction
serializer_class = TransactionSerializer
queryset = Transaction.objects.all()
router.register(r'payment-sources', PaymentSourceViewSet)

View File

@ -28,6 +28,7 @@ class PaymentSourceSerializer(AccountSerializerMixin, serializers.HyperlinkedMod
return serializer_class().to_native(obj.data)
return obj.data
# TODO
def metadata(self):
meta = super(PaymentSourceSerializer, self).metadata()
meta['data'] = {

View File

@ -7,8 +7,8 @@ from .models import Resource, ResourceData
class ResourceSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField('get_name')
unit = serializers.Field()
name = serializers.SerializerMethodField()
unit = serializers.ReadOnlyField()
class Meta:
model = ResourceData
@ -72,19 +72,19 @@ def insert_resource_serializers():
viewset = router.get_viewset(model)
viewset.serializer_class.validate_resources = validate_resources
old_metadata = viewset.metadata
def metadata(self, request, resources=resources):
old_options = viewset.options
def options(self, request, resources=resources):
""" Provides available resources description """
ret = old_metadata(self, request)
ret['available_resources'] = [
metadata = old_options(self, request)
metadata.data['available_resources'] = [
{
'name': resource.name,
'on_demand': resource.on_demand,
'default_allocation': resource.default_allocation
} for resource in resources
]
return ret
viewset.metadata = metadata
return metadata
viewset.options = options
if database_ready():
insert_resource_serializers()

View File

@ -0,0 +1,17 @@
from rest_framework import viewsets
from orchestra.api import router, LogApiMixin
from orchestra.contrib.accounts.api import AccountApiMixin
from . import settings
from .models import SaaS
from .serializers import SaaSSerializer
class SaaSViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = SaaS.objects.all()
serializer_class = SaaSSerializer
filter_fields = ('name',)
router.register(r'saas', SaaSViewSet)

View File

@ -11,6 +11,17 @@ from .fields import VirtualDatabaseRelation
from .services import SoftwareService
class SaaSQuerySet(models.QuerySet):
def create(self, **kwargs):
""" Sets password if provided, all within a single DB operation """
password = kwargs.pop('password')
saas = SaaS(**kwargs)
if password:
saas.set_password(password)
saas.save()
return saas
class SaaS(models.Model):
service = models.CharField(_("service"), max_length=32,
choices=SoftwareService.get_choices())
@ -27,6 +38,7 @@ class SaaS(models.Model):
# Some SaaS sites may need a database, with this virtual field we tell the ORM to delete them
databases = VirtualDatabaseRelation('databases.Database')
objects = SaaSQuerySet.as_manager()
class Meta:
verbose_name = "SaaS"

View File

@ -0,0 +1,29 @@
from django.forms import widgets
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from orchestra.core import validators
from .models import SaaS
class SaaSSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
data = serializers.DictField(required=False)
password = serializers.CharField(write_only=True, required=False,
style={'widget': widgets.PasswordInput},
validators=[
validators.validate_password,
RegexValidator(r'^[^"\'\\]+$',
_('Enter a valid password. '
'This value may contain any ascii character except for '
' \'/"/\\/ characters.'), 'invalid'),
])
class Meta:
model = SaaS
fields = ('url', 'name', 'service', 'is_active', 'data', 'password')
postonly_fields = ('name', 'service', 'password')

View File

@ -19,6 +19,7 @@ class SoftwareServiceForm(PluginDataForm):
password = forms.CharField(label=_("Password"), required=False,
widget=widgets.ReadOnlyWidget('<strong>Unknown password</strong>'),
validators=[
validators.validate_password,
RegexValidator(r'^[^"\'\\]+$',
_('Enter a valid password. '
'This value may contain any ascii character except for '

View File

@ -9,7 +9,7 @@ from .serializers import SystemUserSerializer
class SystemUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
model = SystemUser
queryset = SystemUser.objects.all()
serializer_class = SystemUserSerializer
filter_fields = ('username',)

View File

@ -3,9 +3,8 @@ 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
from orchestra.api.serializers import SetPasswordHyperlinkedSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from orchestra.core.validators import validate_password
from .models import SystemUser
from .validators import validate_home
@ -21,36 +20,25 @@ class GroupSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerial
return get_object_or_404(queryset, username=data['username'])
class SystemUserSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
password = serializers.CharField(max_length=128, label=_('Password'),
validators=[validate_password], write_only=True, required=False,
widget=widgets.PasswordInput)
groups = GroupSerializer(many=True, allow_add_remove=True, required=False)
class SystemUserSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):
groups = GroupSerializer(many=True, required=False)
class Meta:
model = SystemUser
fields = (
'url', 'username', 'password', 'home', 'directory', 'shell', 'groups', 'is_active',
)
postonly_fields = ('username',)
postonly_fields = ('username', 'password')
def validate(self, attrs):
attrs = super(SystemUserSerializer, self).validate(attrs)
user = SystemUser(
username=attrs.get('username') or self.object.username,
shell=attrs.get('shell') or self.object.shell,
username=attrs.get('username') or self.instance.username,
shell=attrs.get('shell') or self.instance.shell,
)
validate_home(user, attrs, self.account)
return attrs
def validate_password(self, attrs, source):
""" POST only password """
if self.object:
if 'password' in attrs:
raise serializers.ValidationError(_("Can not set password"))
elif 'password' not in attrs:
raise serializers.ValidationError(_("Password required"))
return attrs
def validate_groups(self, attrs, source):
groups = attrs.get(source)
if groups:
@ -59,9 +47,3 @@ class SystemUserSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
raise serializers.ValidationError(
_("Do not make the user member of its group"))
return attrs
def save_object(self, obj, **kwargs):
# FIXME this method will be called when saving nested serializers :(
if not obj.pk:
obj.set_password(obj.password)
super(SystemUserSerializer, self).save_object(obj, **kwargs)

View File

@ -9,20 +9,20 @@ from .serializers import WebAppSerializer
class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = WebApp
queryset = WebApp.objects.all()
serializer_class = WebAppSerializer
filter_fields = ('name',)
def metadata(self, request):
ret = super(WebAppViewSet, self).metadata(request)
def options(self, request):
metadata = super(WebAppViewSet, self).options(request)
names = [
'WEBAPPS_BASE_DIR', 'WEBAPPS_TYPES', 'WEBAPPS_WEBAPP_OPTIONS',
'WEBAPPS_PHP_DISABLED_FUNCTIONS', 'WEBAPPS_DEFAULT_TYPE'
]
ret['settings'] = {
metadata.data['settings'] = {
name.lower(): getattr(settings, name, None) for name in names
}
return ret
return metadata
router.register(r'webapps', WebAppViewSet)

View File

@ -8,6 +8,9 @@ from .. import settings
class WebAppServiceMixin(object):
model = 'webapps.WebApp'
related_models = (
('webapps.WebAppOption', 'webapp'),
)
directive = None
def create_webapp_dir(self, context):

View File

@ -1,14 +1,55 @@
from orchestra.api.fields import OptionField
from rest_framework import serializers
from orchestra.api.serializers import HyperlinkedModelSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from .models import WebApp
from .models import WebApp, WebAppOption
class OptionSerializer(serializers.ModelSerializer):
class Meta:
model = WebAppOption
fields = ('name', 'value')
def to_representation(self, instance):
return {prop.name: prop.value for prop in instance.all()}
def to_internal_value(self, data):
return data
class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
options = OptionField(required=False)
options = OptionSerializer(required=False)
class Meta:
model = WebApp
fields = ('url', 'name', 'type', 'options')
postonly_fields = ('name', 'type')
def create(self, validated_data):
options_data = validated_data.pop('options')
webapp = super(WebAppSerializer, self).create(validated_data)
for key, value in options_data.items():
WebAppOption.objects.create(webapp=webapp, name=key, value=value)
return webap
def update(self, instance, validated_data):
options_data = validated_data.pop('options')
instance = super(WebAppSerializer, self).update(validated_data)
existing = {}
for obj in instance.options.all():
existing[obj.name] = obj
posted = set()
for key, value in options_data.items():
posted.add(key)
try:
option = existing[key]
except KeyError:
option = instance.options.create(name=key, value=value)
else:
if option.value != value:
option.value = value
option.save(update_fields=('value',))
for to_delete in set(existing.keys())-posted:
existing[to_delete].delete()
return instance

View File

@ -9,17 +9,17 @@ from .serializers import WebsiteSerializer
class WebsiteViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
model = Website
queryset = Website.objects.all()
serializer_class = WebsiteSerializer
filter_fields = ('name',)
def metadata(self, request):
ret = super(WebsiteViewSet, self).metadata(request)
def options(self, request):
metadata = super(WebsiteViewSet, self).options(request)
names = ['WEBSITES_OPTIONS', 'WEBSITES_PORT_CHOICES']
ret['settings'] = {
metadata.data['settings'] = {
name.lower(): getattr(settings, name, None) for name in names
}
return ret
return metadata
router.register(r'websites', WebsiteViewSet)

View File

@ -127,13 +127,13 @@ class Apache2Backend(ServiceController):
echo -n "$state" > /dev/shm/restart.apache2
if [[ $UPDATED == 1 ]]; then
if [[ $locked == 0 ]]; then
service apache2 satus && service apache2 reload || service apache2 start
service apache2 status && service apache2 reload || service apache2 start
else
echo "Apache2Backend RESTART" >> /dev/shm/restart.apache2
fi
elif [[ "$state" =~ .*RESTART$ ]]; then
rm /dev/shm/restart.apache2
service apache2 satus && service apache2 reload || service apache2 start
service apache2 status && service apache2 reload || service apache2 start
fi""")
)
super(Apache2Backend, self).commit()

View File

@ -2,11 +2,10 @@ from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
from rest_framework import serializers
from orchestra.api.fields import OptionField
from orchestra.api.serializers import HyperlinkedModelSerializer
from orchestra.contrib.accounts.serializers import AccountSerializerMixin
from .models import Website, Content
from .models import Website, Content, WebsiteDirective
from .validators import validate_domain_protocol
@ -22,7 +21,7 @@ class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedMod
class RelatedWebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta:
# model = Content.webapp.field.rel.to
model = Content.webapp.field.rel.to
fields = ('url', 'name', 'type')
def from_native(self, data, files=None):
@ -41,11 +40,23 @@ class ContentSerializer(serializers.HyperlinkedModelSerializer):
return '%s-%s' % (data.get('website'), data.get('path'))
class DirectiveSerializer(serializers.ModelSerializer):
class Meta:
model = WebsiteDirective
fields = ('name', 'value')
def to_representation(self, instance):
return {prop.name: prop.value for prop in instance.all()}
def to_internal_value(self, data):
return data
class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
domains = RelatedDomainSerializer(many=True, allow_add_remove=True, required=False)
contents = ContentSerializer(required=False, many=True, allow_add_remove=True,
domains = RelatedDomainSerializer(many=True, required=False) #allow_add_remove=True
contents = ContentSerializer(required=False, many=True, #allow_add_remove=True,
source='content_set')
directives = OptionField(required=False)
directives = DirectiveSerializer(required=False)
class Meta:
model = Website
@ -62,3 +73,30 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
self.add_error(None, e)
return instance
def create(self, validated_data):
options_data = validated_data.pop('options')
webapp = super(WebsiteSerializer, self).create(validated_data)
for key, value in options_data.items():
WebAppOption.objects.create(webapp=webapp, name=key, value=value)
return webap
def update(self, instance, validated_data):
options_data = validated_data.pop('options')
instance = super(WebsiteSerializer, self).update(validated_data)
existing = {}
for obj in instance.options.all():
existing[obj.name] = obj
posted = set()
for key, value in options_data.items():
posted.add(key)
try:
option = existing[key]
except KeyError:
option = instance.options.create(name=key, value=value)
else:
if option.value != value:
option.value = value
option.save(update_fields=('value',))
for to_delete in set(existing.keys())-posted:
existing[to_delete].delete()
return instance

View File

@ -59,8 +59,8 @@ def paddingCheckboxSelectMultiple(padding):
old_render = widget.render
def render(self, *args, **kwargs):
value = old_render(self, *args, **kwargs)
value = re.sub(r'^<ul id=(.*)>',
r'<ul id=\1 style="padding-left:%ipx">' % padding, value, 1)
value = re.sub(r'^<ul id=([^>]+)>',
r'<ul id=\1 style="padding-left:%ipx">' % padding, value, 1)
return mark_safe(value)
widget.render = render
return widget

View File

@ -6,11 +6,12 @@ class OrchestraPermissionBackend(DjangoModelPermissions):
""" Permissions according to each user """
def has_permission(self, request, view):
model_cls = getattr(view, 'model', None)
if not model_cls:
queryset = getattr(view, 'queryset', None)
if queryset is None:
name = resolve(request.path).url_name
return name == 'api-root'
model_cls = queryset.model
perms = self.get_required_permissions(request.method, model_cls)
if (request.user and
request.user.is_authenticated() and

View File

@ -1,11 +1,10 @@
{% extends "rest_framework/base.html" %}
{% load rest_framework utils staticfiles %}
{% block head %}
{% block meta %}
{{ block.super }}
<link rel="icon" href="{% static "orchestra/images/favicon.png" %}" type="image/png" />
{% endblock %}
{% block title %}{{ SITE_VERBOSE_NAME }} REST API{% endblock %}
{% block branding %}<a class='brand' href="{% url 'api-root' %}">{{ SITE_VERBOSE_NAME }} REST API <span class="version">{% version %}</span></a>{% endblock %}
{% block userlinks %}

View File

@ -24,12 +24,14 @@ def orchestra_version():
def rest_to_admin_url(context):
""" returns the admin equivelent url of the current REST API view """
view = context['view']
model = getattr(view, 'model', None)
queryset = getattr(view, 'queryset', None)
model = queryset.model if queryset else None
url = 'admin:index'
args = []
if model:
url = 'admin:%s_%s' % (model._meta.app_label, model._meta.module_name)
pk = view.kwargs.get(view.pk_url_kwarg)
opts = model._meta
url = 'admin:%s_%s' % (opts.app_label, opts.model_name)
pk = view.kwargs.get(view.lookup_field)
if pk:
url += '_change'
args = [pk]