diff --git a/TODO.md b/TODO.md index 8e9f3a4c..d8d7ab82 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/orchestra/api/actions.py b/orchestra/api/actions.py index 4eed0b37..55d6e176 100644 --- a/orchestra/api/actions.py +++ b/orchestra/api/actions.py @@ -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 diff --git a/orchestra/api/fields.py b/orchestra/api/fields.py deleted file mode 100644 index 18ff0236..00000000 --- a/orchestra/api/fields.py +++ /dev/null @@ -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 diff --git a/orchestra/api/helpers.py b/orchestra/api/helpers.py index a364dec1..6bc8f749 100644 --- a/orchestra/api/helpers.py +++ b/orchestra/api/helpers.py @@ -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: diff --git a/orchestra/api/options.py b/orchestra/api/options.py index 070a42bb..1dc864db 100644 --- a/orchestra/api/options.py +++ b/orchestra/api/options.py @@ -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) diff --git a/orchestra/api/root.py b/orchestra/api/root.py index 7fef2313..12551571 100644 --- a/orchestra/api/root.py +++ b/orchestra/api/root.py @@ -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 diff --git a/orchestra/api/serializers.py b/orchestra/api/serializers.py index 9096383b..55cfaa9d 100644 --- a/orchestra/api/serializers.py +++ b/orchestra/api/serializers.py @@ -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 diff --git a/orchestra/bin/orchestra-admin b/orchestra/bin/orchestra-admin index d4daee0b..0e9129cb 100755 --- a/orchestra/bin/orchestra-admin +++ b/orchestra/bin/orchestra-admin @@ -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 \ diff --git a/orchestra/contrib/accounts/api.py b/orchestra/contrib/accounts/api.py index edd205ee..bef27d27 100644 --- a/orchestra/contrib/accounts/api.py +++ b/orchestra/contrib/accounts/api.py @@ -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 diff --git a/orchestra/contrib/accounts/serializers.py b/orchestra/contrib/accounts/serializers.py index 8fb621e6..3b95a59d 100644 --- a/orchestra/contrib/accounts/serializers.py +++ b/orchestra/contrib/accounts/serializers.py @@ -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) diff --git a/orchestra/contrib/bills/api.py b/orchestra/contrib/bills/api.py index d5d4bfb4..6bb16a85 100644 --- a/orchestra/contrib/bills/api.py +++ b/orchestra/contrib/bills/api.py @@ -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']) diff --git a/orchestra/contrib/contacts/api.py b/orchestra/contrib/contacts/api.py index d2575602..96cd6ce9 100644 --- a/orchestra/contrib/contacts/api.py +++ b/orchestra/contrib/contacts/api.py @@ -8,7 +8,7 @@ from .serializers import ContactSerializer class ContactViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): - model = Contact + queryset = Contact.objects.all() serializer_class = ContactSerializer diff --git a/orchestra/contrib/contacts/filters.py b/orchestra/contrib/contacts/filters.py index 76783932..2ffae3ed 100644 --- a/orchestra/contrib/contacts/filters.py +++ b/orchestra/contrib/contacts/filters.py @@ -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() diff --git a/orchestra/contrib/contacts/migrations/0001_initial.py b/orchestra/contrib/contacts/migrations/0001_initial.py deleted file mode 100644 index 627f9834..00000000 --- a/orchestra/contrib/contacts/migrations/0001_initial.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import django.core.validators -import orchestra.models.fields -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Contact', - fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('short_name', models.CharField(max_length=128, verbose_name='short name')), - ('full_name', models.CharField(max_length=256, verbose_name='full name', blank=True)), - ('email', models.EmailField(max_length=254)), - ('email_usage', orchestra.models.fields.MultiSelectField(max_length=256, verbose_name='email usage', choices=[('SUPPORT', 'Support tickets'), ('ADMIN', 'Administrative'), ('BILLING', 'Billing'), ('TECH', 'Technical'), ('ADDS', 'Announcements'), ('EMERGENCY', 'Emergency contact')], default=('SUPPORT', 'ADMIN', 'BILLING', 'TECH', 'ADDS', 'EMERGENCY'), blank=True)), - ('phone', models.CharField(max_length=32, verbose_name='phone', blank=True)), - ('phone2', models.CharField(max_length=32, verbose_name='alternative phone', blank=True)), - ('address', models.TextField(verbose_name='address', blank=True)), - ('city', models.CharField(max_length=128, verbose_name='city', blank=True)), - ('zipcode', models.CharField(max_length=10, verbose_name='zip code', validators=[django.core.validators.RegexValidator('^[0-9,A-Z]{3,10}$', 'Enter a valid zipcode.', 'invalid')], blank=True)), - ('country', models.CharField(max_length=20, verbose_name='country', choices=[('BI', 'Burundi'), ('MW', 'Malawi'), ('UZ', 'Uzbekistan'), ('UA', 'Ukraine'), ('CR', 'Costa Rica'), ('TG', 'Togo'), ('VA', 'Holy See'), ('SB', 'Solomon Islands'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('SV', 'El Salvador'), ('VI', 'Virgin Islands (U.S.)'), ('FJ', 'Fiji'), ('LS', 'Lesotho'), ('NG', 'Nigeria'), ('EC', 'Ecuador'), ('WS', 'Samoa'), ('BZ', 'Belize'), ('ZM', 'Zambia'), ('TL', 'Timor-Leste'), ('SO', 'Somalia'), ('VC', 'Saint Vincent and the Grenadines'), ('KM', 'Comoros'), ('JE', 'Jersey'), ('SC', 'Seychelles'), ('GG', 'Guernsey'), ('MC', 'Monaco'), ('SM', 'San Marino'), ('AE', 'United Arab Emirates'), ('MG', 'Madagascar'), ('PE', 'Peru'), ('NR', 'Nauru'), ('MA', 'Morocco'), ('MM', 'Myanmar'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('PN', 'Pitcairn'), ('AW', 'Aruba'), ('FI', 'Finland'), ('TT', 'Trinidad and Tobago'), ('BO', 'Bolivia (Plurinational State of)'), ('ET', 'Ethiopia'), ('PM', 'Saint Pierre and Miquelon'), ('PK', 'Pakistan'), ('TR', 'Turkey'), ('CV', 'Cabo Verde'), ('SZ', 'Swaziland'), ('GT', 'Guatemala'), ('RW', 'Rwanda'), ('AL', 'Albania'), ('TK', 'Tokelau'), ('AS', 'American Samoa'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('CH', 'Switzerland'), ('ME', 'Montenegro'), ('KP', "Korea (the Democratic People's Republic of)"), ('HM', 'Heard Island and McDonald Islands'), ('EG', 'Egypt'), ('SR', 'Suriname'), ('IT', 'Italy'), ('RO', 'Romania'), ('CO', 'Colombia'), ('MN', 'Mongolia'), ('AD', 'Andorra'), ('PH', 'Philippines'), ('IS', 'Iceland'), ('MF', 'Saint Martin (French part)'), ('KE', 'Kenya'), ('BN', 'Brunei Darussalam'), ('SD', 'Sudan'), ('GI', 'Gibraltar'), ('WF', 'Wallis and Futuna'), ('KR', 'Korea (the Republic of)'), ('AT', 'Austria'), ('ES', 'Spain'), ('DJ', 'Djibouti'), ('TV', 'Tuvalu'), ('JO', 'Jordan'), ('YE', 'Yemen'), ('IO', 'British Indian Ocean Territory'), ('HU', 'Hungary'), ('JM', 'Jamaica'), ('KH', 'Cambodia'), ('LR', 'Liberia'), ('SG', 'Singapore'), ('NZ', 'New Zealand'), ('ID', 'Indonesia'), ('SI', 'Slovenia'), ('PF', 'French Polynesia'), ('SJ', 'Svalbard and Jan Mayen'), ('GW', 'Guinea-Bissau'), ('BE', 'Belgium'), ('MV', 'Maldives'), ('IQ', 'Iraq'), ('SS', 'South Sudan'), ('IR', 'Iran (Islamic Republic of)'), ('PR', 'Puerto Rico'), ('KY', 'Cayman Islands'), ('BY', 'Belarus'), ('AI', 'Anguilla'), ('EE', 'Estonia'), ('SL', 'Sierra Leone'), ('AG', 'Antigua and Barbuda'), ('CL', 'Chile'), ('VG', 'Virgin Islands (British)'), ('GY', 'Guyana'), ('NE', 'Niger'), ('TH', 'Thailand'), ('RE', 'Réunion'), ('MU', 'Mauritius'), ('BF', 'Burkina Faso'), ('NU', 'Niue'), ('NP', 'Nepal'), ('CK', 'Cook Islands'), ('TC', 'Turks and Caicos Islands'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('GS', 'South Georgia and the South Sandwich Islands'), ('HT', 'Haiti'), ('CD', 'Congo (the Democratic Republic of the)'), ('DM', 'Dominica'), ('CW', 'Curaçao'), ('CG', 'Congo'), ('BR', 'Brazil'), ('GQ', 'Equatorial Guinea'), ('NA', 'Namibia'), ('AF', 'Afghanistan'), ('MH', 'Marshall Islands'), ('LV', 'Latvia'), ('DE', 'Germany'), ('ZW', 'Zimbabwe'), ('CN', 'China'), ('KI', 'Kiribati'), ('SN', 'Senegal'), ('NI', 'Nicaragua'), ('PT', 'Portugal'), ('FO', 'Faroe Islands'), ('NF', 'Norfolk Island'), ('HK', 'Hong Kong'), ('FM', 'Micronesia (Federated States of)'), ('TN', 'Tunisia'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('KZ', 'Kazakhstan'), ('PS', 'Palestine, State of'), ('BH', 'Bahrain'), ('DZ', 'Algeria'), ('BM', 'Bermuda'), ('DK', 'Denmark'), ('TF', 'French Southern Territories'), ('KN', 'Saint Kitts and Nevis'), ('HN', 'Honduras'), ('MD', 'Moldova (the Republic of)'), ('BW', 'Botswana'), ('LU', 'Luxembourg'), ('QA', 'Qatar'), ('AZ', 'Azerbaijan'), ('AU', 'Australia'), ('ML', 'Mali'), ('MR', 'Mauritania'), ('MX', 'Mexico'), ('MS', 'Montserrat'), ('VN', 'Viet Nam'), ('IN', 'India'), ('SX', 'Sint Maarten (Dutch part)'), ('TZ', 'Tanzania, United Republic of'), ('SA', 'Saudi Arabia'), ('CY', 'Cyprus'), ('PA', 'Panama'), ('SY', 'Syrian Arab Republic'), ('VU', 'Vanuatu'), ('PL', 'Poland'), ('BA', 'Bosnia and Herzegovina'), ('LK', 'Sri Lanka'), ('CI', "Côte d'Ivoire"), ('AQ', 'Antarctica'), ('AO', 'Angola'), ('LT', 'Lithuania'), ('MT', 'Malta'), ('GU', 'Guam'), ('IL', 'Israel'), ('MP', 'Northern Mariana Islands'), ('TM', 'Turkmenistan'), ('FK', 'Falkland Islands [Malvinas]'), ('LC', 'Saint Lucia'), ('TO', 'Tonga'), ('UG', 'Uganda'), ('GF', 'French Guiana'), ('TD', 'Chad'), ('AX', 'Åland Islands'), ('CF', 'Central African Republic'), ('PW', 'Palau'), ('CU', 'Cuba'), ('EH', 'Western Sahara'), ('HR', 'Croatia'), ('GN', 'Guinea'), ('BV', 'Bouvet Island'), ('BL', 'Saint Barthélemy'), ('MO', 'Macao'), ('SE', 'Sweden'), ('IM', 'Isle of Man'), ('CM', 'Cameroon'), ('BG', 'Bulgaria'), ('BS', 'Bahamas'), ('NO', 'Norway'), ('AM', 'Armenia'), ('PY', 'Paraguay'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('JP', 'Japan'), ('LI', 'Liechtenstein'), ('BJ', 'Benin'), ('SK', 'Slovakia'), ('GD', 'Grenada'), ('RU', 'Russian Federation'), ('ER', 'Eritrea'), ('RS', 'Serbia'), ('MQ', 'Martinique'), ('ZA', 'South Africa'), ('CA', 'Canada'), ('ST', 'Sao Tome and Principe'), ('LY', 'Libya'), ('CX', 'Christmas Island'), ('MZ', 'Mozambique'), ('GL', 'Greenland'), ('BT', 'Bhutan'), ('PG', 'Papua New Guinea'), ('BD', 'Bangladesh'), ('NL', 'Netherlands'), ('LB', 'Lebanon'), ('CC', 'Cocos (Keeling) Islands'), ('FR', 'France'), ('GH', 'Ghana'), ('KG', 'Kyrgyzstan'), ('GM', 'Gambia'), ('YT', 'Mayotte'), ('MY', 'Malaysia'), ('CZ', 'Czech Republic'), ('LA', "Lao People's Democratic Republic"), ('AR', 'Argentina'), ('IE', 'Ireland'), ('TW', 'Taiwan (Province of China)'), ('GR', 'Greece'), ('NC', 'New Caledonia'), ('OM', 'Oman'), ('GA', 'Gabon'), ('DO', 'Dominican Republic'), ('GE', 'Georgia'), ('KW', 'Kuwait'), ('BB', 'Barbados'), ('GP', 'Guadeloupe'), ('TJ', 'Tajikistan'), ('US', 'United States of America')], default='ES', blank=True)), - ('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, verbose_name='Account', related_name='contacts')), - ], - ), - ] diff --git a/orchestra/contrib/contacts/migrations/0002_auto_20150408_1411.py b/orchestra/contrib/contacts/migrations/0002_auto_20150408_1411.py deleted file mode 100644 index 8c62826e..00000000 --- a/orchestra/contrib/contacts/migrations/0002_auto_20150408_1411.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('contacts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='country', - field=models.CharField(max_length=20, verbose_name='country', default='ES', blank=True, choices=[('EE', 'Estonia'), ('AI', 'Anguilla'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('LB', 'Lebanon'), ('MO', 'Macao'), ('PF', 'French Polynesia'), ('CI', "Côte d'Ivoire"), ('VC', 'Saint Vincent and the Grenadines'), ('PR', 'Puerto Rico'), ('PW', 'Palau'), ('PA', 'Panama'), ('TL', 'Timor-Leste'), ('SD', 'Sudan'), ('TT', 'Trinidad and Tobago'), ('CA', 'Canada'), ('MS', 'Montserrat'), ('DE', 'Germany'), ('HR', 'Croatia'), ('KN', 'Saint Kitts and Nevis'), ('SC', 'Seychelles'), ('TC', 'Turks and Caicos Islands'), ('AR', 'Argentina'), ('VN', 'Viet Nam'), ('NO', 'Norway'), ('MY', 'Malaysia'), ('IO', 'British Indian Ocean Territory'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('PL', 'Poland'), ('BW', 'Botswana'), ('CZ', 'Czech Republic'), ('AG', 'Antigua and Barbuda'), ('TR', 'Turkey'), ('NC', 'New Caledonia'), ('ZM', 'Zambia'), ('MV', 'Maldives'), ('BO', 'Bolivia (Plurinational State of)'), ('CM', 'Cameroon'), ('BZ', 'Belize'), ('SO', 'Somalia'), ('AE', 'United Arab Emirates'), ('YE', 'Yemen'), ('UY', 'Uruguay'), ('GG', 'Guernsey'), ('QA', 'Qatar'), ('IN', 'India'), ('FJ', 'Fiji'), ('MZ', 'Mozambique'), ('CO', 'Colombia'), ('EC', 'Ecuador'), ('SK', 'Slovakia'), ('UM', 'United States Minor Outlying Islands'), ('CR', 'Costa Rica'), ('TV', 'Tuvalu'), ('AZ', 'Azerbaijan'), ('IE', 'Ireland'), ('EG', 'Egypt'), ('GL', 'Greenland'), ('US', 'United States of America'), ('BT', 'Bhutan'), ('ZA', 'South Africa'), ('NP', 'Nepal'), ('TO', 'Tonga'), ('TJ', 'Tajikistan'), ('VG', 'Virgin Islands (British)'), ('RE', 'Réunion'), ('NR', 'Nauru'), ('DK', 'Denmark'), ('LT', 'Lithuania'), ('ET', 'Ethiopia'), ('TZ', 'Tanzania, United Republic of'), ('SB', 'Solomon Islands'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('SG', 'Singapore'), ('MF', 'Saint Martin (French part)'), ('FI', 'Finland'), ('LK', 'Sri Lanka'), ('NU', 'Niue'), ('TM', 'Turkmenistan'), ('SJ', 'Svalbard and Jan Mayen'), ('GN', 'Guinea'), ('BB', 'Barbados'), ('VU', 'Vanuatu'), ('AL', 'Albania'), ('JM', 'Jamaica'), ('ID', 'Indonesia'), ('CL', 'Chile'), ('IT', 'Italy'), ('PT', 'Portugal'), ('KP', "Korea (the Democratic People's Republic of)"), ('MU', 'Mauritius'), ('PY', 'Paraguay'), ('MH', 'Marshall Islands'), ('TH', 'Thailand'), ('IQ', 'Iraq'), ('LR', 'Liberia'), ('NA', 'Namibia'), ('NL', 'Netherlands'), ('AX', 'Åland Islands'), ('MX', 'Mexico'), ('KW', 'Kuwait'), ('ZW', 'Zimbabwe'), ('GQ', 'Equatorial Guinea'), ('UA', 'Ukraine'), ('OM', 'Oman'), ('TD', 'Chad'), ('SR', 'Suriname'), ('KM', 'Comoros'), ('CF', 'Central African Republic'), ('GP', 'Guadeloupe'), ('LU', 'Luxembourg'), ('LC', 'Saint Lucia'), ('SY', 'Syrian Arab Republic'), ('HU', 'Hungary'), ('VA', 'Holy See'), ('KY', 'Cayman Islands'), ('CN', 'China'), ('KR', 'Korea (the Republic of)'), ('DZ', 'Algeria'), ('GE', 'Georgia'), ('CY', 'Cyprus'), ('LA', "Lao People's Democratic Republic"), ('GH', 'Ghana'), ('AT', 'Austria'), ('DO', 'Dominican Republic'), ('MN', 'Mongolia'), ('SI', 'Slovenia'), ('NG', 'Nigeria'), ('GM', 'Gambia'), ('AF', 'Afghanistan'), ('NE', 'Niger'), ('AO', 'Angola'), ('MW', 'Malawi'), ('PN', 'Pitcairn'), ('NZ', 'New Zealand'), ('ST', 'Sao Tome and Principe'), ('SA', 'Saudi Arabia'), ('RW', 'Rwanda'), ('AU', 'Australia'), ('CK', 'Cook Islands'), ('BJ', 'Benin'), ('SS', 'South Sudan'), ('GU', 'Guam'), ('LI', 'Liechtenstein'), ('MR', 'Mauritania'), ('FM', 'Micronesia (Federated States of)'), ('BY', 'Belarus'), ('PH', 'Philippines'), ('ER', 'Eritrea'), ('RU', 'Russian Federation'), ('YT', 'Mayotte'), ('BI', 'Burundi'), ('AS', 'American Samoa'), ('MA', 'Morocco'), ('BD', 'Bangladesh'), ('TN', 'Tunisia'), ('NI', 'Nicaragua'), ('HT', 'Haiti'), ('AQ', 'Antarctica'), ('BR', 'Brazil'), ('FK', 'Falkland Islands [Malvinas]'), ('AM', 'Armenia'), ('MG', 'Madagascar'), ('MC', 'Monaco'), ('IM', 'Isle of Man'), ('KI', 'Kiribati'), ('FO', 'Faroe Islands'), ('SZ', 'Swaziland'), ('GR', 'Greece'), ('PS', 'Palestine, State of'), ('GY', 'Guyana'), ('MQ', 'Martinique'), ('ML', 'Mali'), ('BM', 'Bermuda'), ('CU', 'Cuba'), ('MD', 'Moldova (the Republic of)'), ('CX', 'Christmas Island'), ('TF', 'French Southern Territories'), ('KE', 'Kenya'), ('CH', 'Switzerland'), ('SM', 'San Marino'), ('TK', 'Tokelau'), ('EH', 'Western Sahara'), ('IS', 'Iceland'), ('DM', 'Dominica'), ('SE', 'Sweden'), ('NF', 'Norfolk Island'), ('MM', 'Myanmar'), ('CD', 'Congo (the Democratic Republic of the)'), ('JP', 'Japan'), ('BS', 'Bahamas'), ('ES', 'Spain'), ('GI', 'Gibraltar'), ('AD', 'Andorra'), ('RO', 'Romania'), ('CV', 'Cabo Verde'), ('HN', 'Honduras'), ('GT', 'Guatemala'), ('KG', 'Kyrgyzstan'), ('WS', 'Samoa'), ('PG', 'Papua New Guinea'), ('VI', 'Virgin Islands (U.S.)'), ('BG', 'Bulgaria'), ('RS', 'Serbia'), ('LS', 'Lesotho'), ('GA', 'Gabon'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('BN', 'Brunei Darussalam'), ('CC', 'Cocos (Keeling) Islands'), ('SN', 'Senegal'), ('BE', 'Belgium'), ('BA', 'Bosnia and Herzegovina'), ('HK', 'Hong Kong'), ('DJ', 'Djibouti'), ('GF', 'French Guiana'), ('UZ', 'Uzbekistan'), ('HM', 'Heard Island and McDonald Islands'), ('IL', 'Israel'), ('MP', 'Northern Mariana Islands'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('SX', 'Sint Maarten (Dutch part)'), ('CG', 'Congo'), ('SV', 'El Salvador'), ('AW', 'Aruba'), ('BL', 'Saint Barthélemy'), ('MT', 'Malta'), ('LV', 'Latvia'), ('UG', 'Uganda'), ('PE', 'Peru'), ('KZ', 'Kazakhstan'), ('PK', 'Pakistan'), ('TG', 'Togo'), ('WF', 'Wallis and Futuna'), ('GS', 'South Georgia and the South Sandwich Islands'), ('GD', 'Grenada'), ('PM', 'Saint Pierre and Miquelon'), ('BV', 'Bouvet Island'), ('KH', 'Cambodia'), ('BH', 'Bahrain'), ('GW', 'Guinea-Bissau'), ('ME', 'Montenegro'), ('SL', 'Sierra Leone'), ('IR', 'Iran (Islamic Republic of)'), ('JO', 'Jordan'), ('BF', 'Burkina Faso'), ('LY', 'Libya'), ('FR', 'France'), ('TW', 'Taiwan (Province of China)'), ('CW', 'Curaçao'), ('JE', 'Jersey')]), - ), - ] diff --git a/orchestra/contrib/contacts/migrations/0003_auto_20150408_1412.py b/orchestra/contrib/contacts/migrations/0003_auto_20150408_1412.py deleted file mode 100644 index 2d989100..00000000 --- a/orchestra/contrib/contacts/migrations/0003_auto_20150408_1412.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -import orchestra.contrib.contacts.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contacts', '0002_auto_20150408_1411'), - ] - - operations = [ - migrations.AlterField( - model_name='contact', - name='country', - field=models.CharField(max_length=20, choices=[('CH', 'Switzerland'), ('ES', 'Spain'), ('CV', 'Cabo Verde'), ('GN', 'Guinea'), ('NI', 'Nicaragua'), ('IO', 'British Indian Ocean Territory'), ('FR', 'France'), ('AG', 'Antigua and Barbuda'), ('TW', 'Taiwan (Province of China)'), ('IT', 'Italy'), ('GI', 'Gibraltar'), ('AX', 'Åland Islands'), ('PY', 'Paraguay'), ('AS', 'American Samoa'), ('TD', 'Chad'), ('OM', 'Oman'), ('MU', 'Mauritius'), ('NP', 'Nepal'), ('SG', 'Singapore'), ('US', 'United States of America'), ('NE', 'Niger'), ('HT', 'Haiti'), ('JE', 'Jersey'), ('MZ', 'Mozambique'), ('BT', 'Bhutan'), ('LB', 'Lebanon'), ('CY', 'Cyprus'), ('HM', 'Heard Island and McDonald Islands'), ('IS', 'Iceland'), ('UY', 'Uruguay'), ('MP', 'Northern Mariana Islands'), ('CR', 'Costa Rica'), ('HK', 'Hong Kong'), ('BD', 'Bangladesh'), ('CL', 'Chile'), ('NZ', 'New Zealand'), ('LA', "Lao People's Democratic Republic"), ('ZW', 'Zimbabwe'), ('UZ', 'Uzbekistan'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('BO', 'Bolivia (Plurinational State of)'), ('ET', 'Ethiopia'), ('DM', 'Dominica'), ('ER', 'Eritrea'), ('TL', 'Timor-Leste'), ('TO', 'Tonga'), ('PK', 'Pakistan'), ('AM', 'Armenia'), ('AO', 'Angola'), ('WF', 'Wallis and Futuna'), ('CU', 'Cuba'), ('UA', 'Ukraine'), ('SX', 'Sint Maarten (Dutch part)'), ('EH', 'Western Sahara'), ('SA', 'Saudi Arabia'), ('MQ', 'Martinique'), ('TM', 'Turkmenistan'), ('SR', 'Suriname'), ('PG', 'Papua New Guinea'), ('BN', 'Brunei Darussalam'), ('KG', 'Kyrgyzstan'), ('BL', 'Saint Barthélemy'), ('HN', 'Honduras'), ('MN', 'Mongolia'), ('YE', 'Yemen'), ('TJ', 'Tajikistan'), ('PR', 'Puerto Rico'), ('GS', 'South Georgia and the South Sandwich Islands'), ('TR', 'Turkey'), ('SJ', 'Svalbard and Jan Mayen'), ('AW', 'Aruba'), ('KP', "Korea (the Democratic People's Republic of)"), ('PA', 'Panama'), ('PN', 'Pitcairn'), ('IR', 'Iran (Islamic Republic of)'), ('YT', 'Mayotte'), ('MM', 'Myanmar'), ('LY', 'Libya'), ('SE', 'Sweden'), ('BW', 'Botswana'), ('ME', 'Montenegro'), ('FO', 'Faroe Islands'), ('EC', 'Ecuador'), ('KW', 'Kuwait'), ('GY', 'Guyana'), ('KR', 'Korea (the Republic of)'), ('BE', 'Belgium'), ('SO', 'Somalia'), ('KY', 'Cayman Islands'), ('NC', 'New Caledonia'), ('AQ', 'Antarctica'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('GG', 'Guernsey'), ('FJ', 'Fiji'), ('MR', 'Mauritania'), ('BG', 'Bulgaria'), ('DK', 'Denmark'), ('IN', 'India'), ('EE', 'Estonia'), ('TK', 'Tokelau'), ('TN', 'Tunisia'), ('MH', 'Marshall Islands'), ('BR', 'Brazil'), ('LT', 'Lithuania'), ('GA', 'Gabon'), ('MC', 'Monaco'), ('CZ', 'Czech Republic'), ('KZ', 'Kazakhstan'), ('BY', 'Belarus'), ('SC', 'Seychelles'), ('DZ', 'Algeria'), ('RW', 'Rwanda'), ('SY', 'Syrian Arab Republic'), ('AU', 'Australia'), ('MD', 'Moldova (the Republic of)'), ('ID', 'Indonesia'), ('SD', 'Sudan'), ('GR', 'Greece'), ('LS', 'Lesotho'), ('MA', 'Morocco'), ('SS', 'South Sudan'), ('IL', 'Israel'), ('LR', 'Liberia'), ('CI', "Côte d'Ivoire"), ('AL', 'Albania'), ('MV', 'Maldives'), ('NF', 'Norfolk Island'), ('PE', 'Peru'), ('RO', 'Romania'), ('AI', 'Anguilla'), ('CX', 'Christmas Island'), ('IQ', 'Iraq'), ('PM', 'Saint Pierre and Miquelon'), ('AF', 'Afghanistan'), ('LI', 'Liechtenstein'), ('BF', 'Burkina Faso'), ('EG', 'Egypt'), ('KH', 'Cambodia'), ('MW', 'Malawi'), ('ST', 'Sao Tome and Principe'), ('NG', 'Nigeria'), ('CO', 'Colombia'), ('CW', 'Curaçao'), ('PW', 'Palau'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('HR', 'Croatia'), ('LC', 'Saint Lucia'), ('FI', 'Finland'), ('GL', 'Greenland'), ('SM', 'San Marino'), ('MG', 'Madagascar'), ('VI', 'Virgin Islands (U.S.)'), ('TF', 'French Southern Territories'), ('KI', 'Kiribati'), ('NR', 'Nauru'), ('GP', 'Guadeloupe'), ('DO', 'Dominican Republic'), ('SB', 'Solomon Islands'), ('SI', 'Slovenia'), ('MX', 'Mexico'), ('FK', 'Falkland Islands [Malvinas]'), ('BI', 'Burundi'), ('RU', 'Russian Federation'), ('AR', 'Argentina'), ('VC', 'Saint Vincent and the Grenadines'), ('VA', 'Holy See'), ('TV', 'Tuvalu'), ('MF', 'Saint Martin (French part)'), ('CA', 'Canada'), ('MS', 'Montserrat'), ('CG', 'Congo'), ('PS', 'Palestine, State of'), ('DE', 'Germany'), ('ZM', 'Zambia'), ('GE', 'Georgia'), ('QA', 'Qatar'), ('GW', 'Guinea-Bissau'), ('VG', 'Virgin Islands (British)'), ('PH', 'Philippines'), ('GF', 'French Guiana'), ('DJ', 'Djibouti'), ('SK', 'Slovakia'), ('TH', 'Thailand'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('RE', 'Réunion'), ('SL', 'Sierra Leone'), ('TT', 'Trinidad and Tobago'), ('BH', 'Bahrain'), ('AT', 'Austria'), ('CF', 'Central African Republic'), ('VN', 'Viet Nam'), ('PF', 'French Polynesia'), ('UM', 'United States Minor Outlying Islands'), ('AE', 'United Arab Emirates'), ('MY', 'Malaysia'), ('BZ', 'Belize'), ('PT', 'Portugal'), ('BB', 'Barbados'), ('ML', 'Mali'), ('CM', 'Cameroon'), ('GT', 'Guatemala'), ('BS', 'Bahamas'), ('MO', 'Macao'), ('GM', 'Gambia'), ('WS', 'Samoa'), ('ZA', 'South Africa'), ('RS', 'Serbia'), ('TG', 'Togo'), ('NU', 'Niue'), ('VU', 'Vanuatu'), ('JP', 'Japan'), ('KM', 'Comoros'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('SN', 'Senegal'), ('JM', 'Jamaica'), ('KE', 'Kenya'), ('HU', 'Hungary'), ('NO', 'Norway'), ('LK', 'Sri Lanka'), ('KN', 'Saint Kitts and Nevis'), ('GQ', 'Equatorial Guinea'), ('GH', 'Ghana'), ('CN', 'China'), ('NA', 'Namibia'), ('TC', 'Turks and Caicos Islands'), ('JO', 'Jordan'), ('LU', 'Luxembourg'), ('FM', 'Micronesia (Federated States of)'), ('CD', 'Congo (the Democratic Republic of the)'), ('IE', 'Ireland'), ('SZ', 'Swaziland'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('CK', 'Cook Islands'), ('IM', 'Isle of Man'), ('BV', 'Bouvet Island'), ('PL', 'Poland'), ('SV', 'El Salvador'), ('BA', 'Bosnia and Herzegovina'), ('TZ', 'Tanzania, United Republic of'), ('MT', 'Malta'), ('NL', 'Netherlands'), ('AZ', 'Azerbaijan'), ('UG', 'Uganda'), ('CC', 'Cocos (Keeling) Islands'), ('LV', 'Latvia'), ('GD', 'Grenada'), ('AD', 'Andorra'), ('GU', 'Guam')], blank=True, default='ES', verbose_name='country'), - ), - migrations.AlterField( - model_name='contact', - name='phone', - field=models.CharField(validators=[orchestra.contrib.contacts.models.validate_phone], max_length=32, blank=True, verbose_name='phone'), - ), - migrations.AlterField( - model_name='contact', - name='phone2', - field=models.CharField(validators=[orchestra.contrib.contacts.models.validate_phone], max_length=32, blank=True, verbose_name='alternative phone'), - ), - ] diff --git a/orchestra/contrib/contacts/migrations/__init__.py b/orchestra/contrib/contacts/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/orchestra/contrib/contacts/serializers.py b/orchestra/contrib/contacts/serializers.py index 3d25d911..cd478a6d 100644 --- a/orchestra/contrib/contacts/serializers.py +++ b/orchestra/contrib/contacts/serializers.py @@ -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 = ( diff --git a/orchestra/contrib/databases/api.py b/orchestra/contrib/databases/api.py index e226ee5a..3bc92cee 100644 --- a/orchestra/contrib/databases/api.py +++ b/orchestra/contrib/databases/api.py @@ -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',) diff --git a/orchestra/contrib/databases/serializers.py b/orchestra/contrib/databases/serializers.py index e07d2d7b..fbc9ede6 100644 --- a/orchestra/contrib/databases/serializers.py +++ b/orchestra/contrib/databases/serializers.py @@ -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) diff --git a/orchestra/contrib/domains/api.py b/orchestra/contrib/domains/api.py index d9728994..38ab2e40 100644 --- a/orchestra/contrib/domains/api.py +++ b/orchestra/contrib/domains/api.py @@ -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) diff --git a/orchestra/contrib/domains/serializers.py b/orchestra/contrib/domains/serializers.py index 8be6de8d..228016c1 100644 --- a/orchestra/contrib/domains/serializers.py +++ b/orchestra/contrib/domains/serializers.py @@ -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 diff --git a/orchestra/contrib/issues/api.py b/orchestra/contrib/issues/api.py index ec89d48d..15974a11 100644 --- a/orchestra/contrib/issues/api.py +++ b/orchestra/contrib/issues/api.py @@ -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 diff --git a/orchestra/contrib/issues/serializers.py b/orchestra/contrib/issues/serializers.py index cf411a12..3a2062a9 100644 --- a/orchestra/contrib/issues/serializers.py +++ b/orchestra/contrib/issues/serializers.py @@ -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 diff --git a/orchestra/contrib/lists/api.py b/orchestra/contrib/lists/api.py index 7205cc66..96c9fd68 100644 --- a/orchestra/contrib/lists/api.py +++ b/orchestra/contrib/lists/api.py @@ -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',) diff --git a/orchestra/contrib/lists/models.py b/orchestra/contrib/lists/models.py index 3f442814..8d1ba5ed 100644 --- a/orchestra/contrib/lists/models.py +++ b/orchestra/contrib/lists/models.py @@ -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') diff --git a/orchestra/contrib/lists/serializers.py b/orchestra/contrib/lists/serializers.py index 296ef1f7..bbd5781d 100644 --- a/orchestra/contrib/lists/serializers.py +++ b/orchestra/contrib/lists/serializers.py @@ -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) diff --git a/orchestra/contrib/mailboxes/api.py b/orchestra/contrib/mailboxes/api.py index f9731727..d03a892e 100644 --- a/orchestra/contrib/mailboxes/api.py +++ b/orchestra/contrib/mailboxes/api.py @@ -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 diff --git a/orchestra/contrib/mailboxes/backends.py b/orchestra/contrib/mailboxes/backends.py index b37324cf..bed6ab45 100644 --- a/orchestra/contrib/mailboxes/backends.py +++ b/orchestra/contrib/mailboxes/backends.py @@ -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 diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index c7086b27..23cf2e7c 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -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 diff --git a/orchestra/contrib/orchestration/__init__.py b/orchestra/contrib/orchestration/__init__.py index c4d3945d..e9769bd5 100644 --- a/orchestra/contrib/orchestration/__init__.py +++ b/orchestra/contrib/orchestration/__init__.py @@ -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 diff --git a/orchestra/contrib/orchestration/middlewares.py b/orchestra/contrib/orchestration/middlewares.py index 3b1a96d2..156d931f 100644 --- a/orchestra/contrib/orchestration/middlewares.py +++ b/orchestra/contrib/orchestration/middlewares.py @@ -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) diff --git a/orchestra/contrib/orders/api.py b/orchestra/contrib/orders/api.py index 61bc5f02..922632eb 100644 --- a/orchestra/contrib/orders/api.py +++ b/orchestra/contrib/orders/api.py @@ -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) diff --git a/orchestra/contrib/payments/api.py b/orchestra/contrib/payments/api.py index 03effe6a..7fd65f27 100644 --- a/orchestra/contrib/payments/api.py +++ b/orchestra/contrib/payments/api.py @@ -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) diff --git a/orchestra/contrib/payments/serializers.py b/orchestra/contrib/payments/serializers.py index 6af8f629..7287cd78 100644 --- a/orchestra/contrib/payments/serializers.py +++ b/orchestra/contrib/payments/serializers.py @@ -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'] = { diff --git a/orchestra/contrib/resources/serializers.py b/orchestra/contrib/resources/serializers.py index 07e4ff94..5819692f 100644 --- a/orchestra/contrib/resources/serializers.py +++ b/orchestra/contrib/resources/serializers.py @@ -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() diff --git a/orchestra/contrib/saas/api.py b/orchestra/contrib/saas/api.py new file mode 100644 index 00000000..2d968524 --- /dev/null +++ b/orchestra/contrib/saas/api.py @@ -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) diff --git a/orchestra/contrib/saas/models.py b/orchestra/contrib/saas/models.py index 94088071..ce64d7bf 100644 --- a/orchestra/contrib/saas/models.py +++ b/orchestra/contrib/saas/models.py @@ -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" diff --git a/orchestra/contrib/saas/serializers.py b/orchestra/contrib/saas/serializers.py new file mode 100644 index 00000000..06887ab1 --- /dev/null +++ b/orchestra/contrib/saas/serializers.py @@ -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') diff --git a/orchestra/contrib/saas/services/options.py b/orchestra/contrib/saas/services/options.py index e3ab9283..da5b3287 100644 --- a/orchestra/contrib/saas/services/options.py +++ b/orchestra/contrib/saas/services/options.py @@ -19,6 +19,7 @@ class SoftwareServiceForm(PluginDataForm): password = forms.CharField(label=_("Password"), required=False, widget=widgets.ReadOnlyWidget('Unknown password'), validators=[ + validators.validate_password, RegexValidator(r'^[^"\'\\]+$', _('Enter a valid password. ' 'This value may contain any ascii character except for ' diff --git a/orchestra/contrib/systemusers/api.py b/orchestra/contrib/systemusers/api.py index f7ddae18..044048e4 100644 --- a/orchestra/contrib/systemusers/api.py +++ b/orchestra/contrib/systemusers/api.py @@ -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',) diff --git a/orchestra/contrib/systemusers/serializers.py b/orchestra/contrib/systemusers/serializers.py index f0ae242b..f6f54b66 100644 --- a/orchestra/contrib/systemusers/serializers.py +++ b/orchestra/contrib/systemusers/serializers.py @@ -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) diff --git a/orchestra/contrib/webapps/api.py b/orchestra/contrib/webapps/api.py index 4681413d..283a8fb3 100644 --- a/orchestra/contrib/webapps/api.py +++ b/orchestra/contrib/webapps/api.py @@ -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) diff --git a/orchestra/contrib/webapps/backends/__init__.py b/orchestra/contrib/webapps/backends/__init__.py index d6e88dd3..30796a3d 100644 --- a/orchestra/contrib/webapps/backends/__init__.py +++ b/orchestra/contrib/webapps/backends/__init__.py @@ -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): diff --git a/orchestra/contrib/webapps/serializers.py b/orchestra/contrib/webapps/serializers.py index 60e2aef6..bed4100d 100644 --- a/orchestra/contrib/webapps/serializers.py +++ b/orchestra/contrib/webapps/serializers.py @@ -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 diff --git a/orchestra/contrib/websites/api.py b/orchestra/contrib/websites/api.py index 8cf8e130..54be3c66 100644 --- a/orchestra/contrib/websites/api.py +++ b/orchestra/contrib/websites/api.py @@ -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) diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index e012f540..f8f2d286 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -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() diff --git a/orchestra/contrib/websites/serializers.py b/orchestra/contrib/websites/serializers.py index 6fdc42c9..70776571 100644 --- a/orchestra/contrib/websites/serializers.py +++ b/orchestra/contrib/websites/serializers.py @@ -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 @@ -61,4 +72,31 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): # TODO not sure about this one 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 diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index 8b94bee9..edb70980 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -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'^