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

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

Related

How to delete constant in subclass?

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

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 rest framework: override create() in ModelSerializer passing an extra parameter

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__'

Django Rest Framework - How to add custom field in ModelSerializer

I created a ModelSerializer and want to add a custom field which is not part of my model.
I found a description to add extra fields here and I tried the following:
customField = CharField(source='my_field')
When I add this field and call my validate() function then this field is not part of the attr dict. attr contains all model fields specified except the extra fields. So I cannot access this field in my overwritten validation, can I?
When I add this field to the field list like this:
class Meta:
model = Account
fields = ('myfield1', 'myfield2', 'customField')
then I get an error because customField is not part of my model - what is correct because I want to add it just for this serializer.
Is there any way to add a custom field?
In fact there a solution without touching at all the model. You can use SerializerMethodField which allow you to plug any method to your serializer.
class FooSerializer(ModelSerializer):
foo = serializers.SerializerMethodField()
def get_foo(self, obj):
return "Foo id: %i" % obj.pk
You're doing the right thing, except that CharField (and the other typed fields) are for writable fields.
In this case you just want a simple read-only field, so instead just use:
customField = Field(source='get_absolute_url')
...for clarity, if you have a Model Method defined in the following way:
class MyModel(models.Model):
...
def model_method(self):
return "some_calculated_result"
You can add the result of calling said method to your serializer like so:
class MyModelSerializer(serializers.ModelSerializer):
model_method_field = serializers.CharField(source='model_method')
p.s. Since the custom field isn't really a field in your model, you'll usually want to make it read-only, like so:
class Meta:
model = MyModel
read_only_fields = (
'model_method_field',
)
here answer for your question.
you should add to your model Account:
#property
def my_field(self):
return None
now you can use:
customField = CharField(source='my_field')
source: https://stackoverflow.com/a/18396622/3220916
After reading all the answers here my conclusion is that it is impossible to do this cleanly. You have to play dirty and do something hadkish like creating a write_only field and then override the validate and to_representation methods. This is what worked for me:
class FooSerializer(ModelSerializer):
foo = CharField(write_only=True)
class Meta:
model = Foo
fields = ["foo", ...]
def validate(self, data):
foo = data.pop("foo", None)
# Do what you want with your value
return super().validate(data)
def to_representation(self, instance):
data = super().to_representation(instance)
data["foo"] = whatever_you_want
return data
To show self.author.full_name, I got an error with Field. It worked with ReadOnlyField:
class CommentSerializer(serializers.HyperlinkedModelSerializer):
author_name = ReadOnlyField(source="author.full_name")
class Meta:
model = Comment
fields = ('url', 'content', 'author_name', 'author')
I was looking for a solution for adding a writable custom field to a model serializer. I found this one, which has not been covered in the answers to this question.
It seems like you do indeed need to write your own simple Serializer.
class PassThroughSerializer(serializers.Field):
def to_representation(self, instance):
# This function is for the direction: Instance -> Dict
# If you only need this, use a ReadOnlyField, or SerializerField
return None
def to_internal_value(self, data):
# This function is for the direction: Dict -> Instance
# Here you can manipulate the data if you need to.
return data
Now you can use this Serializer to add custom fields to a ModelSerializer
class MyModelSerializer(serializers.ModelSerializer)
my_custom_field = PassThroughSerializer()
def create(self, validated_data):
# now the key 'my_custom_field' is available in validated_data
...
return instance
This also works, if the Model MyModel actually has a property called my_custom_field but you want to ignore its validators.
With the last version of Django Rest Framework, you need to create a method in your model with the name of the field you want to add.
class Foo(models.Model):
. . .
def foo(self):
return 'stuff'
. . .
class FooSerializer(ModelSerializer):
foo = serializers.ReadOnlyField()
class Meta:
model = Foo
fields = ('foo',)

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