Package: drf-ext
DRF Ext (Django REST Framework Extensions)Extensions for the DRFInstallation:pip install drf-extFeatures/Extensions:Nested model serializer saving (create/update)Declaration of non-required fieldsAdd multiple common parameters to a set of fieldsCheck fields' existence on de-serialization (create/update)Check any field's existence among a set of fields on de-serialization (create/update)Several frequently used utilitiesAvailable objects:Metaclasses:NestedCreateUpdateMetaclass: provides nested serializer writes oncreateandupdate.FieldOptionsMetaclass: provides various field declaration options for different scenarios.ExtendedSerializerMetaclass: contains bothNestedCreateUpdateMetaclassandFieldOptionsMetaclass.InheritableExtendedSerializerMetaclass:ExtendedSerializerMetaclasswith inheritance support.Mixins:NestedCreateUpdateMixin: provides nested serializer writes oncreateandupdate(used byNestedCreateUpdateMetaclass), see example below.Utilities:update_error_dict: allows updating aValidationErrorerror dict with provided key/value.exc_dict_has_keys: tests whether given key(s) are in the exception error dict (e.g.ValidationError).get_request_user_on_serializer: gets the current user object from inside the serializer.NOTE:All of the above areimport-able fromdrf_exte.g.:fromdrf_extimportNestedCreateUpdateMetaclass,update_error_dictExamples:Assuming the followingmodels.py:fromdjango.dbimportmodelsfromdjango.contrib.auth.modelsimportUserclassTag(models.Model):name=models.CharField(max_length=12)classAddress(models.Model):user=models.OneToOneField(User,on_delete=models.CASCADE,related_name="address",null=True,blank=True,)tags=models.ManyToManyField(Tag,related_name="addresses",blank=True)state=models.CharField(max_length=2)zip_code=models.CharField(max_length=12)classClient(models.Model):user=models.OneToOneField(User,on_delete=models.CASCADE,related_name="client",null=False,blank=False,)Metaclasses/mixins:NestedCreateUpdateMetaclass/NestedCreateUpdateMixin:serializers.py:fromrest_frameworkimportserializersfromdrf_extimportNestedCreateUpdateMetaclassclassAddressSerializer(serializers.ModelSerializer):classMeta:model=Addressfields="__all__"classUserSerializer(serializers.ModelSerializer,metaclass=NestedCreateUpdateMetaclass):address=AddressSerializer()classMeta:model=Userfields="__all__"SamplePOSTrequest:data={"username":"my_username","password":"my_password","address":{"state":"CA","zip_code":"12345","tags":[1,3,7]# `pk` of `Tag` objects}}client.post("/users/",data=data)SamplePUT/PATCHrequest:data={"address":{"_pk":2,# `pk` of the related `address`"state":"CA","zip_code":"12345","tags":[9,24,56]}}client.patch("/users/1/",data=data)NOTE:drf_extuses the existence of_pkfield to track
whether it's a new nested object creation or an update. So if_pkis omitted it would taken as new nested object creation
request.This_pkwrite-onlyfield is automatically
injected to all nested serializers by the metaclass. But if
one is using theNestedCreateUpdateMixin, they need to
explicitly define the field on the nested serializer e.g.:classAddressSerializer(serializers.ModelSerializer):_pk=serializers.IntegerField(write_only=True,required=False)# hereclassMeta:model=Addressfields=("pk","_pk","state","zip_code")read_only_fields=("pk",)classUserSerializer(NestedCreateUpdateMixin,serializers.ModelSerializer):address=AddressSerializer()classMeta:model=Userfields="__all__"Everything else remains the same asNestedCreateUpdateMetaclass.FieldOptionsMetaclass:required_fields_on_create,required_fields_on_update,required_fields_on_create_any,required_fields_on_update_any:classAddressSerializer(serializers.ModelSerializer,metaclass=FieldOptionsMetaclass):classMeta:model=Addressfields=("pk","state","zip_code")read_only_fields=("pk",)# These fields are required on POST request i.e. on creation of objectrequired_fields_on_create=("state","zip_code")# These fields are required on PUT/PATCH request i.e. on update of objectrequired_fields_on_update=("zip_code",)classUserSerializer(serializers.ModelSerializer,metaclass=FieldOptionsMetaclass):address=AddressSerializer()classMeta:model=Userfields=("pk","address","username","email","password","first_name","last_name",)read_only_fields=("pk",)# At least one of these fields are required on POST requestrequired_fields_on_create_any=("first_name","last_name")# At least one of these fields are required on PUT/PATCH requestrequired_fields_on_update_any=("address","username","email")non_required_fields:classAddressSerializer(serializers.ModelSerializer,metaclass=FieldOptionsMetaclass):classMeta:model=Addressfields=("pk","state","zip_code")read_only_fields=("pk",)# The mentioned fields are made *non-required*, like providing# the `required=False` parameter on them. All fields are taken# as required, unless model has `blank=True` on the field, or# explicitly mentioned with `required=False`. This will allow# to control that from a single place. Also, this would make# working with `required_fields_on_create` and related options# (see above) easier to follow as users can decide to make a# field mandatory in POST but not in PUT/PATCH and vice versa,# which allows for a finer control over fields.non_required_fields=("state","zip_code")NOTE:Ifnon_required_fieldsis not provided, all fields mentioned
infields(withoutexclude-ed ones) are madenon-required. To use
the default option of DRF, one can setnon_required_fieldsto an empty
iterable e.g.:classAddressSerializer(serializers.ModelSerializer,metaclass=FieldOptionsMetaclass):classMeta:model=Addressfields=("pk","state","zip_code")non_required_fields=()common_field_params:classAddressSerializer(serializers.ModelSerializer,metaclass=FieldOptionsMetaclass):classMeta:model=Addressfields=("pk","state","zip_code")read_only_fields=("pk",)# `common_field_params` allows to add some common parameters# to a set of fields. This must be a `dict` with the keys# being an (hashable) iterable e.g. `tuple` and values being# a `dict` of parameter-values.common_field_params={("state",zip_code"): {"allow_blank":False,"trim_whitespace":True,},# Using a single field is also fine (this works similar# to the default `extra_kwargs` in that case).("state",):{"max_length":2,},}ExtendedSerializerMetaclass:If you want to use all features fromNestedCreateUpdateMetaclassandFieldOptionsMetaclassmentioned above, use this metaclass:classUserSerializer(serializers.ModelSerializer,metaclass=ExtendedSerializerMetaclass):address=AddressSerializer()classMeta:model=Addressfields="__all__"required_fields_on_create=("username, "password",)required_fields_on_update_any=("first_name","last_name","email")SamplePOSTrequest:data={"username":"my_username","password":"my_password","address":{"state":"CA","zip_code":"12345"}}client.post("/users/",data=data)InheritableExtendedSerializerMetaclass:Works exactly likeExtendedSerializerMetaclass. This one should be
used to include all the attributes defined in superclasses (ignoring
the dunder andMetaattributes, and callables).This is designed to be used instead ofExtendedSerializerMetaclasswhen e.g. a (common) base class contains field definitions that are
to be inherited by all child classes. For example:classCommon:field=serializers.IntegerField()classSerializer(serializers.ModelSerializer,metaclass=InheritableExtendedSerializerMetaclass):# `field` will be injected here like it were defined# on this class body....Utilities:update_error_dict:errors={}if...:# Following will result in `errors` being:# `{"field": ["Error message"]}`update_error_dict(errors,"field","Error message")if...:# Following will result in `errors` being:# `{"field": ["Error message", "New error message"]}`update_error_dict(errors,"field","New error message")iferrors:raiseValidationError(errors)exc_dict_has_keys:exc=ValidationError({"field_1":["msg","new msg"],"field_2":["msg"],})exc_dict_has_keys(exc,("field_1","field_2"))# returns `True`exc_dict_has_keys(exc,"field_1")# returns `True`exc_dict_has_keys(exc,("field_1","field_2","field_3"))# returns `False`exc_dict_has_keys(exc,"field_3")# returns `False`get_request_user_on_serializer:classMySerializer(serializers.Serializer):......defcreate(self,validated_data):# Get the user sending this requestuser=get_request_user_on_serializer(self)Development:Installdevdependencies:pip install drf-ext[dev]Run tests:drf_ext/tests$ PYTHONPATH=.. pytestLicense:MIT