Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
peterthomassen committed Feb 9, 2022
1 parent 8db36a9 commit 0b9997b
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 73 deletions.
75 changes: 7 additions & 68 deletions api/desecapi/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.',
Expand Down
52 changes: 51 additions & 1 deletion api/desecapi/validators.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
6 changes: 2 additions & 4 deletions docs/dns/rrsets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``[]``.
Expand Down

0 comments on commit 0b9997b

Please sign in to comment.