Let's say I have a class something like the following:
class PostSerializer(serializers.HyperlinkedModelSerializer):
updated_at = serializers.DateTimeField()
def __init__(self, *args, **kwargs):
init = super().__init__(*args, **kwargs)
return init
I want to create a subclass of the PostSerializer class and I'd like to remove the updated_at constant property from the subclass-ed class.
class PostWithoutUpdatedAtSerializer(serializers.HyperlinkedModelSerializer):
# something to remove the updated_at property ?
def somefunc(self);
pass
I use a framework for example django so generally I cannot simply remove the property from the parent class, I need to subclass them. And of course obviously I need to "delete" the property, I cannot do updated_at = None, it's not a deleting.
How is it possible? Thanks.
It's not directly possible, since the attribute doesn't exist on your derived class at all (it does on the superclass), so there's nothing to remove or reassign.
Instead, the framework you're using (Django REST Framework, my magic ball tells me), uses a metaclass that inspects the class definition for field objects and puts them into cls._declared_fields on the class (along with any fields from the superclass(es)).
The real fields for your serializer instance are acquired by get_fields(), which by default just copies _declared_fields.
In other words, if your Django REST Framework serializer subclass should not serialize that field, customize get_fields():
def get_fields(self):
fields = super().get_fields()
fields.pop("updated_at", None) # remove field if it's there
return fields
Related
I have an abstract mixin class that adds a Django model field to any concrete class that inherits from it.
At class initialisation - when makemigrations is run - I'd like the inheriting class to define whether an inherited field is required or optional via the blank= True or False property.
I've tried various Meta and __new__ approaches, but can't figure out how the abstract mixin class can get the information from the inheriting class.
Here's a naive attempt:
from django.db import models
class DescriptionMixin(models.Model):
class Meta:
abstract = True
description = models.TetxField(
# how to get value here?
blank=inheriting_class.description_required
)
class OptionalDescription(DescriptionMixin, SomeOtherClass):
class Meta:
verbose_name = 'Optional description'
description_required = False
class RequiredDescription(DescriptionMixin, SomeOtherClass):
class Meta:
verbose_name = 'Required description'
description_required = True
Thanks in advance for any help offered.
You can't do this at the database level. makemigrations doesn't actually initialise your models to create the migration files.
But since you're trying to enforce this on a TextField, which cannot be enforced at the database level anyway (blank is only used when validating a model through the full_clean() method), you could just override the clean() method on the DescriptionMixin, checking the value of self.blank and raising a ValidationError appropriately.
Solved using this (it's actually Wagtail on top of Django):
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)
self._meta.get_field('description').blank = not getattr(self, 'description_required', False)
Some Context
I have the following django/python snippet:
from rest_framework import serializers
from .models import Profile, Task
class Serializable():
types = {}
def __init__(self, objectid):
self.object = self.types[objectid][0]
self.serializer = self.types[objectid][1]
def serialized(self):
instances = self.object.objects.all()
serialized = self.serializer(instances, many=True)
return serialized
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
oid = 'profile'
model = Profile
fields = ['login', 'status']
Serializable.types[oid] = [model, <class-reference>]
class TaskSerializer(serializers.ModelSerializer):
class Meta:
oid = 'task'
model = Task
fields = ['description', 'date', 'owner']
Serializable.types[oid] = [model, <class-reference>]
I am using Django with the rest_framework library installed. One of the interesting features I am using is ModelSerializers (ModelSerializers Documentation), which save quite a lot of code repetition. I want Serializable.types variable to be populated on runtime (when all the serializer classes are declared). The whole point of this is that I will not have to update my views whens a new type of model is included. For example, I would print the json representation of my model instances like this:
class QueryObject(APIView):
permission_classes = (AllowAny,)
def get(self, request, *args, **kwargs):
oid = request.GET['oid']
serializable= Serializable(oid)
json = serializable.serialized
return JsonResponse(json)
The Problem
The major problem is in the last line of each Serializer class.
Serializable.types[oid] = [model, <class-reference>]
I've tried putting the name of the class, ProfileSerializer for example, to no avail. I've tried doing the same outside of the Meta class, such as:
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
oid = 'profile'
model = Profile
fields = ['login', 'status']
Serializable.types[Meta.oid] = [Meta.model, ProfileSerializer]
also not successful. Not sure what else to do, which is why I'm hoping the community can help me on this one.
This is actually a case for defining a metaclass.
I've never actually found a source of information which gives a complete, clear and satisfactory explanation as to what metaclasses are or how they work. I will try to enhance this answer with such information if required but for the time being I am going to stick to a solution for your present problem. I am assuming python 3.
Define an additional class, thus:
class ModelSerializerMeta(serializers.SerializerMetaclass):
def __init__(cls, class_name, base_classes, attributes):
super(ModelSerialiserMeta, cls).__init__(class_name, base_classes, attributes)
Serializer.types[cls.Meta.oid] = [cls.Meta.model, cls]
Then use this as the metaclass of your Serializers, e.g.
class ProfileSerializer(serializers.ModelSerializer, metaclass=ModelSerializerMeta):
class Meta:
oid = 'profile'
model = Profile
fields = ['login', 'status']
Better yet, create some superclass for all your model serializers, assign the metaclass there, make all of your serializers inherit from that superclass which will then use the metaclass throughout.
Metaclasses are definitely the right answer unless your code can require python >= 3.6. Starting with 3.6 there is a new feature called the __init_subclass__ hook.
So you can do something like
class foo:
#classmethod
def __init_subclass__(cls, *args, **kwargs):
Serializers.register_class(cls)
Whenever a child of Foo is defined, the __init_subclass__ method on Foo will be called, passing in the child class reference as cls.
How make some fields read-only for particular user permission level?
There is a Django REST API project. There is an Foo serializer with 2 fields - foo and bar. There are 2 permissions - USER and ADMIN.
Serializer is defined as:
class FooSerializer(serializers.ModelSerializer):
...
class Meta:
model = FooModel
fields = ['foo', 'bar']
How does one makes sure 'bar' field is read-only for USER and writable for ADMIN?
I would use smth like:
class FooSerializer(serializers.ModelSerializer):
...
class Meta:
model = FooModel
fields = ['foo', 'bar']
read_only_fields = ['bar']
But how to make it conditional (depending on permission)?
You can use get_serializer_class() method of the view to use different serializers for different users:
class ForUserSerializer(serializers.ModelSerializer):
class Meta:
model = ExampleModel
fields = ('id', 'name', 'bar')
read_only_fields = ('bar',)
class ForAdminSerializer(serializers.ModelSerializer):
class Meta:
model = ExampleModel
fields = ('id', 'name', 'bar', 'for_admin_only_field')
class ExampleView(viewsets.ModelViewSet):
...
def get_serializer_class(self):
if self.request.user.is_admin:
return ForAdminSerializer
return ForUserSerializer
Although Fian's answer does seem to be the most obviously documented way there is an alternative that draws on other documented code and which enables passing arguments to the serializer as it is instantiated.
The first piece of the puzzle is the documentation on dynamically modifying a serializer at the point of instantiation. That documentation doesn't explain how to call this code from a viewset or how to modify the readonly status of fields after they've been initated - but that's not very hard.
The second piece - the get_serializer method is also documented - (just a bit further down the page from get_serializer_class under 'other methods') so it should be safe to rely on (and the source is very simple, which hopefully means less chance of unintended side effects resulting from modification). Check the source under the GenericAPIView (the ModelViewSet - and all the other built in viewset classes it seems - inherit from the GenericAPIView which, defines get_serializer.
Putting the two together you could do something like this:
In a serializers file (for me base_serializers.py):
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Adding this next line to the documented example
read_only_fields = kwargs.pop('read_only_fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# another bit we're adding to documented example, to take care of readonly fields
if read_only_fields is not None:
for f in read_only_fields:
try:
self.fields[f].read_only = True
exceptKeyError:
#not in fields anyway
pass
Then in your viewset you might do something like this:
class MyUserViewSet(viewsets.ModelViewSet):
# ...permissions and all that stuff
def get_serializer(self, *args, **kwargs):
# the next line is taken from the source
kwargs['context'] = self.get_serializer_context()
# ... then whatever logic you want for this class e.g:
if self.request.user.is_staff and self.action == "list":
rofs = ('field_a', 'field_b')
fs = ('field_a', 'field_c')
# add all your further elses, elifs, drawing on info re the actions,
# the user, the instance, anything passed to the method to define your read only fields and fields ...
# and finally instantiate the specific class you want (or you could just
# use get_serializer_class if you've defined it).
# Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
kwargs['read_only_fields'] = rofs
kwargs['fields'] = fs
return MyUserSerializer(*args, **kwargs)
And that should be it! Using MyUserViewSet should now instantiate your UserSerializer with the arguments you'd like - and assuming your user serializer inherits from your DynamicFieldsModelSerializer, it should know just what to do.
Perhaps its worth mentioning that of course the DynamicFieldsModelSerializer could easily be adapted to do things like take in a read_only_exceptions list and use it to whitelist rather than blacklist fields (which I tend to do). I also find it useful to set the fields to an empty tuple if its not passed and then just remove the check for None ... and I set my fields definitions on my inheriting Serializers to 'all'. This means no fields that aren't passed when instantiating the serializer survive by accident and I also don't have to compare the serializer invocation with the inheriting serializer class definition to know what's been included...e.g within the init of the DynamicFieldsModelSerializer:
# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....
NB If I just wanted two or three classes that mapped to distinct user types and/or I didn't want any specially dynamic serializer behaviour, I might well probably stick with the approach mentioned by Fian.
However, in a case of mine I wanted to adjust the fields based both on the action as well as the admin level of the user making the request, which led to many long and annoying serializer class names. It began to feel ugly creating many serializer classes simply to tweak the list of fields and readonly fields. That approach also meant that the list of fields was separated from the relevant business logic in the view. It might be debatable whether thats a good thing but when the logic gets a tad more involved, I thought it would make the code less, rather than more, maintainable. Of course it makes even more sense to use the approach I've outlined above if you also want to do other 'dynamic' things on the initiation of the serializer.
You can extend the get_fields method in the serializer class. In your case it would look like this:
class FooSerializer(serializers.ModelSerializer):
...
class Meta:
model = FooModel
fields = ["foo", "bar"]
def get_fields(self):
fields = super().get_fields() # Python 3 syntax
request = self.context.get("request", None)
if request and request.user and request.user.is_superuser is False:
fields["bar"].read_only = True
return fields
When fields need to be filled programmatically in Django Rest Framework, the pre_save method may be overridden in the APIView, and the needed fields can be populated there, like:
def pre_save(self, obj):
obj.owner = self.request.user
This works great for flat objects, but in case of nested situations, the nested object cannot be accessed in the pre_save method. The only solution I found so far is to override the save_object method, and check if the object is an instance of the nested class, and if so, populate that field there. Although this works, I don't like the solution, and would like to know if anyone found a better way?
Demonstrating the situation:
class Notebook(models.Model):
owner = models.ForeignKey(User)
class Note(models.Model):
owner = models.ForeignKey(User)
notebook = models.ForeignKey(Notebook)
note = models.TextField()
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
class Meta:
model = Note
fields = ('note', 'owner')
class NotebookSerializer(serializers.ModelSerializer):
notes = NoteSerializer(many=True)
owner = serializers.Field(source='owner.username')
class Meta:
model = Notebook
fields = ('notes', 'owner')
def save_object(self, obj, **kwargs):
if isinstance(obj, Note):
obj.owner = obj.notebook.owner
return super(NotebookSerializer, self).save_object(obj, **kwargs)
class NotebookCreateAPIView(CreateAPIView):
model = Notebook
permission_classes = (IsAuthenticated,)
serializer_class = NotebookSerializer
def pre_save(self, obj):
obj.owner = self.request.user
Before asking why don't I use different endpoints for creating notebooks and notes separately, let me say that I do that, but I also need a functionality to provide initial notes on creation of the notebook, so that's why I need this kind of endpoint as well.
Also, before I figured out this hackish solution, I actually expected that I will have to override the save_object method of the NoteSerializer class itself, but it turned out in case of nested objects, it won't even be called, only the root object's save_objects method, for all the nested objects, but I guess it was a design decision.
So once again, is this solvable in a more idiomatic way?
You can access the request in your serializer context.
So my approach to this would be:
class NoteSerializer(serializers.ModelSerializer):
owner = serializers.Field(source='owner.username')
def restore_object(self, attrs, instance=None):
instance = super(NoteSerializer, self).restore_object(attrs, instance)
instance.owner = self.context['request'].user
return instance
class Meta:
model = Note
fields = ('note', 'owner')
And the same on the NotebookSerializer.
The Serializer context will be made available to all used serializers in the ViewSet.
I have a base class that is a Django form. The child class then inherits from the parent including all the fields, but I need to make changes to one of the parent's field option such as label, required, and etc.
Example:
class BaseForm(forms.Form):
userid = forms.CharField(required=True)
class ChildForm(BaseForm):
# I need to change the parent field option
userid = forms.CharField(required=False)
Any suggestions?
You're doing exactly what you should do.
It's particularly fitting in this case because that's the exact pattern for overriding ModelForm fields.
If you need to retain properties you don't know about / are outside your control (or what have you), you could override the __init__ method and access the form fields via self.fields['myfield']
class ChildForm(BaseForm):
def __init__(self, *args, **kwargs):
super(ChildForm, self).__init__(*args, **kwargs)
self.fields['userid'].required = False