diff --git a/api/desecapi/serializers.py b/api/desecapi/serializers.py index dd8e8e073..bdc80cfcf 100644 --- a/api/desecapi/serializers.py +++ b/api/desecapi/serializers.py @@ -9,15 +9,15 @@ from captcha.image import ImageCaptcha from django.contrib.auth.password_validation import validate_password from django.core.validators import MinValueValidator -from django.db.models import Model, Q +from django.db.models import Q from django.utils import timezone from netfields import rest_framework as netfields_rf from rest_framework import fields, serializers from rest_framework.settings import api_settings -from rest_framework.validators import UniqueTogetherValidator, UniqueValidator, qs_filter +from rest_framework.validators import UniqueTogetherValidator from api import settings -from desecapi import crypto, metrics, models, validators +from desecapi import crypto, models, validators class CaptchaSerializer(serializers.ModelSerializer): @@ -99,46 +99,6 @@ def save(self, **kwargs): raise serializers.ValidationError(exc.message_dict, code='precedence') -class RequiredOnPartialUpdateCharField(serializers.CharField): - """ - This field is always required, even for partial updates (e.g. using PATCH). - """ - def validate_empty_values(self, data): - if data is serializers.empty: - self.fail('required') - - return super().validate_empty_values(data) - - -class Validator: - - message = 'This field did not pass validation.' - - def __init__(self, message=None): - self.field_name = None - self.message = message or self.message - self.instance = None - - def __call__(self, value): - raise NotImplementedError - - def __repr__(self): - return '<%s>' % self.__class__.__name__ - - -class ReadOnlyOnUpdateValidator(Validator): - - message = 'Can only be written on create.' - requires_context = True - - def __call__(self, value, serializer_field): - instance = getattr(serializer_field.parent, 'instance', None) - if isinstance(instance, Model): - field_name = serializer_field.source_attrs[-1] - if isinstance(instance, Model) and value != getattr(instance, field_name): - raise serializers.ValidationError(self.message, code='read-only-on-update') - - class ConditionalExistenceModelSerializer(serializers.ModelSerializer): """ Only considers data with certain condition as existing data. @@ -464,8 +424,8 @@ def __init__(self, *args, **kwargs): def get_fields(self): fields = super().get_fields() - fields['subname'].validators.append(ReadOnlyOnUpdateValidator()) - fields['type'].validators.append(ReadOnlyOnUpdateValidator()) + fields['subname'].validators.append(validators.ReadOnlyOnUpdateValidator()) + fields['type'].validators.append(validators.ReadOnlyOnUpdateValidator()) fields['ttl'].validators.append(MinValueValidator(limit_value=self.minimum_ttl)) return fields @@ -620,7 +580,7 @@ def get_fields(self): fields = super().get_fields() if not self.include_keys: fields.pop('keys') - fields['name'].validators.append(ReadOnlyOnUpdateValidator()) + fields['name'].validators.append(validators.ReadOnlyOnUpdateValidator()) return fields def validate_name(self, value): @@ -723,27 +683,6 @@ class ResetPasswordSerializer(EmailSerializer): captcha = CaptchaSolutionSerializer(required=True) -class CustomFieldNameUniqueValidator(UniqueValidator): - """ - Does exactly what rest_framework's UniqueValidator does, however allows to further customize the - query that is used to determine the uniqueness. - More specifically, we allow that the field name the value is queried against is passed when initializing - this validator. (At the time of writing, UniqueValidator insists that the field's name is used for the - database query field; only how the lookup must match is allowed to be changed.) - """ - - def __init__(self, queryset, message=None, lookup='exact', lookup_field=None): - self.lookup_field = lookup_field - super().__init__(queryset, message, lookup) - - def filter_queryset(self, value, queryset, field_name): - """ - Filter the queryset to all instances matching the given value on the specified lookup field. - """ - filter_kwargs = {'%s__%s' % (self.lookup_field or field_name, self.lookup): value} - return qs_filter(queryset, **filter_kwargs) - - class AuthenticatedActionSerializer(serializers.ModelSerializer): state = serializers.CharField() # serializer read-write, but model read-only field validity_period = settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE @@ -845,7 +784,7 @@ def validate(self, attrs): class AuthenticatedChangeEmailUserActionSerializer(AuthenticatedBasicUserActionSerializer): new_email = serializers.EmailField( validators=[ - CustomFieldNameUniqueValidator( + validators.CustomFieldNameUniqueValidator( queryset=models.User.objects.all(), lookup_field='email', message='You already have another account with this email address.', diff --git a/api/desecapi/validators.py b/api/desecapi/validators.py index 5c65d71cd..8f97e63c1 100644 --- a/api/desecapi/validators.py +++ b/api/desecapi/validators.py @@ -1,6 +1,56 @@ from django.db import DataError +from django.db.models import Model from rest_framework.exceptions import ValidationError -from rest_framework.validators import qs_exists, qs_filter, UniqueTogetherValidator +from rest_framework.validators import qs_exists, qs_filter, UniqueTogetherValidator, UniqueValidator + +class Validator: + + message = 'This field did not pass validation.' + + def __init__(self, message=None): + self.field_name = None + self.message = message or self.message + self.instance = None + + def __call__(self, value): + raise NotImplementedError + + def __repr__(self): + return '<%s>' % self.__class__.__name__ + + +class ReadOnlyOnUpdateValidator(Validator): + + message = 'Can only be written on create.' + requires_context = True + + def __call__(self, value, serializer_field): + instance = getattr(serializer_field.parent, 'instance', None) + if isinstance(instance, Model): + field_name = serializer_field.source_attrs[-1] + if isinstance(instance, Model) and value != getattr(instance, field_name): + raise ValidationError(self.message, code='read-only-on-update') + + +class CustomFieldNameUniqueValidator(UniqueValidator): + """ + Does exactly what rest_framework's UniqueValidator does, however allows to further customize the + query that is used to determine the uniqueness. + More specifically, we allow that the field name the value is queried against is passed when initializing + this validator. (At the time of writing, UniqueValidator insists that the field's name is used for the + database query field; only how the lookup must match is allowed to be changed.) + """ + + def __init__(self, queryset, message=None, lookup='exact', lookup_field=None): + self.lookup_field = lookup_field + super().__init__(queryset, message, lookup) + + def filter_queryset(self, value, queryset, field_name): + """ + Filter the queryset to all instances matching the given value on the specified lookup field. + """ + filter_kwargs = {'%s__%s' % (self.lookup_field or field_name, self.lookup): value} + return qs_filter(queryset, **filter_kwargs) def qs_exclude(queryset, **kwargs): diff --git a/docs/dns/rrsets.rst b/docs/dns/rrsets.rst index bae1b8b6e..c8524a311 100644 --- a/docs/dns/rrsets.rst +++ b/docs/dns/rrsets.rst @@ -486,10 +486,8 @@ RRset. With ``POST``, only new RRsets are acceptable (i.e. the domain must not yet have an RRset with the same subname and type), while ``PUT`` allows both creating new RRsets and modifying existing ones. -For the ``PATCH`` method, only ``subname `` and ``type`` is required; if you -want to modify only ``ttl`` or ``records``, you can skip the other field. To -create a new RRset using ``PATCH``, all fields but ``subname`` must be -specified. +For the ``PATCH`` method, only ``subname`` and ``type`` is required; if you +want to modify only ``ttl`` or ``records``, you can skip the other field. To delete an RRset during a bulk operation, use ``PATCH`` or ``PUT`` and set ``records`` to ``[]``.