Django rest framework: override create() in ModelSerializer passing an extra parameter - python

I am looking for a way to properly ovverride the default .create() method of a ModelSerializer serializer in Django Rest Framework for dealing with an extra parameter.
In my original Django model I have just overridden the default.save() method for managing an extra param. Now .save() can be called also in this way: .save(extra = 'foo').
I have to create a ModelSerializer mapping on that original Django model:
from OriginalModels.models import OriginalModel
from rest_framework import serializers
class OriginalModelSerializer(serializers.ModelSerializer):
# model fields
class Meta:
model = OriginalModel
But in this way I can't pass the extra param to the model .save() method.
How can I properly override the .create() method of my OriginalModelSerializer class to take (eventually) this extra param into account?

Hmm. this might not be the perfect answer given I don't know how you want to pass this "extra" in (ie. is it an extra field in a form normally, etc)
What you'd probably want to do is just represent foo as a field on the serializer. Then it will be present in validated_data in create, then you can make create do something like the following
def create(self, validated_data):
obj = OriginalModel.objects.create(**validated_data)
obj.save(foo=validated_data['foo'])
return obj
You'd probably want to look at the default implementation of create for some of the other things it does though (like remove many-to-many relationships, etc.).

You can now do this in the view set (threw in user as a bonus ;) ):
class OriginalModelViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows OriginalModel classes to be viewed or edited.
"""
serializer_class = OriginalModelSerializer
queryset = OriginalModel.objects.all()
def perform_create(self, serializer):
user = None
if self.request and hasattr(self.request, "user"):
user = self.request.user
serializer.save(user=user, foo='foo')
That way the Serializer can stay generic, i.e.:
class OriginalModelSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = OriginalModel
fields = '__all__'

Related

Django REST API: Make field read-only for certain permission level

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

Can to_representation() in Django Rest Framework access the normal fields

The docs on using to_representation is somewhat short. This method is used by Django Rest Framework 3.0+ to change the representation of your data in an API.
Here' the documentation link:
http://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior
Here is my current code:
from django.forms.models import model_to_dict
class PersonListSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('foo', 'bar',)
def to_representation(self, instance):
return model_to_dict(instance)
When I do this code, it returns all fields in the model instead of the fields that I have specified above in class Meta: fields.
Is it possible to reference the class Meta: fields within the to_representation method?
DRF's ModelSerializer already has all the logic to handle that. In your case you shouldn't even need to customize to_representation. If you need to customize it, I would recommend to first call super and then customize the output:
class PersonListSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ('foo', 'bar',)
def to_representation(self, instance):
data = super(PersonListSerializer, self).to_representation(instance)
data.update(...)
return data
P.S. if you are interested to know how it works, the magic actually does not happen in ModelSerializer.to_representation. As a matter of fact, it does not even implement that method. Its implemented on regular Serializer. All the magic with Django models actually happens in get_fields which calls get_field_names which then considers the Meta.fields parameters...
def to_representation(self, instance):
data = super(ResultLogSerializer, self).to_representation(instance)
data['username'] = instance.job_result.user.username
data['status'] = instance.job_result.status
data['created'] = instance.job_result.created
data['completed'] = instance.job_result.completed
return data

Django admin post-save method - how to do?

I've got an admin form with a couple of inlines for displaying m2m fields, like so:
class ArticleAdmin(admin.ModelAdmin):
form = ArticleCustomAdminForm
inlines = (SpecificGemInline, SuiteInline,)
Base class looks something like that:
class Article(models.Model):
article_code = models.CharField(max_length=15)
gems = models.ManyToManyField(Gem, through='SpecificGem')
Model has a special field article_code that should aggregate some data from m2m fields represented in both inlines, so I've written a function create_code(instance) that does so by accessing model instance fields directly, something like that:
def create_code(instance):
article_code_part1 = SpecificGem.objects.filter(article=instance)
article_code_part2 = instance.suite_set.all()
instance.article_code = #do something with both parts
The problem is, when I call this function from overriden ModelAdmin's save_model() or model's save() functions, following instance m2m fields produces outdated results. Even retarded example below wouldn't help:
class ArticleAdmin(admin.ModelAdmin):
#...
def save_model(self, request, obj, form, change):
obj.save()
create_code(obj)
obj.save()
When I get into InlineFormset's clean() method, I have access to its forms' data so I could figure out a part of article_code even without actual saving... but I have two inlines.
So how do I find the topmost save method, so I could call my aggregation function after all models are validated and saved to db?
In order to catch changes to a ManyToManyField, you need to hook up the m2m_changed signal. You might want to have a look at the documentation for signals in general and the m2m_changed signal in particular.

Django customize AdminModel view

In my Django application I have Guest user accounts that are created for all unregistered users (they all have email='guest#mysite.com'). At the same time I create some demo objects related to the Guest account.
These objects live in the same table (have the same model) as objects for registered users. And I have more that one type (model) of these objects like:
class Object1(models.Model):
user = ForeignKey(...)
...
class Object2(models.Model):
user = ForeignKey(...)
...
And what I would like to achieve is to filter out all objects related to guest accounts when I view them in django admin.
Right now I subclass django.contrib.admin.views.main.ChangeList and override get_query_set method to do the required exclude, and I redefine get_changelist method of django's ModelAdmin class at runtime:
class FilteredChangeList(ChangeList):
def get_query_set(self):
qs = super(FilteredChangeList, self).get_query_set()
if is_related_to(self.model, Profile):
qs = qs.exclude(user__email='guest#mysite.com')
return qs
def my_getchangelist(self, request, **kwargs):
return FilteredChangeList
ModelAdmin.get_changelist = my_getchangelist
I suppose redefining django's methods at runtime is a bad practice, so is there any correct solution for the problem?
Guess you are doing a lot more work than necessary. You could also create your own ModelAdmin class and overwrite its queryset method, no need to construct your own ChangeList class:
class MyFilteredAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyFilteredAdmin, self).queryset(request)
if is_related_to(self.model, Profile):
qs = qs.exclude(user__email='guest#mysite.com')
return qs
You could then either register your models directly with this new admin class - admin.site.register(Model, MyFilteredAdmin) - or create subclasses that inherit from MyFilteredAdmin instead from django's ModelAdmin.
You can also have proxy models. You could have one that has 'real' users, and one that has 'guest' users.
You can subclass ModelAdmin and override the get_changelist method instead of redefining at runtime.
class FilteredModelAdmin(ModelAdmin):
def get_changelist(self, request, **kwargs):
return FilteredChangeList
Then register your models with FilteredModelAdmin instead of ModelAdmin.
admin.site.register(Object1, FilteredModelAdmin)
admin.site.register(Object2, FilteredModelAdmin)
If you need to customize any other settings for your models subclass FilteredModelAdmin instead of ModelAdmin.
class Object1ModelAdmin(FilteredModelAdmin)
# other customizations here
admin.site.register(Object1, Object1ModelAdmin)

Django ModelForm with User data in Generic View

I have a model with a foreign key to group (the other fields don't matter):
class Project(models.Model) :
group = models.ForeignKey(Group)
...
I have a model form for this model:
class AddProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = ["group","another"]
In my urls, I am using this in a generic view:
(r'^$', create_object, {'form_class':AddProjectForm, 'template_name':"form.html", 'login_required':True, 'extra_context':{'title':'Add a Project'}}),
That all works, but I want to have the group field display only the groups that the current user belongs to, not all of the groups available. I'd normally do this by passing in the user to the model form and overriding init if I wasn't in a generic view. Is there any way to do this with the generic view or do I need to go with a regular view to pass in that value?
This is gonna look dirty, since the generic view instantiates the form_class with no parameters. If you really want to use the generic_view you're gonna have to generate the class dynamically :S
def FormForUser(user):
class TmpClass(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(TmpClass, self).__init__(*args, **kwargs)
self.fields['group'].queryset = user.group_set.all()
class Meta:
model = Project
fields = ['group', 'another']
Then wrap the create object view
#login_required # Only logged users right?
def create_project(request):
user = request.user
form_class = FormForUser(user)
return create_object(request, form_class=form_class, ..... )
My recommendation is to write your own view, it will give you more control on the long term and it's a trivial view.
No, you'll need to make a regular view. As can be seen by looking at the source code for create_object(), there's no functionality to pass in extra parameters to the modelform (in django 1.2):
http://code.djangoproject.com/svn/django/branches/releases/1.2.X/django/views/generic/create_update.py

Categories