How to get required response from DRF - django

I have database which consists of configuration of application. Application can have various configuration keys and basically I don't have information about how much keys I have and name of keys are not known too. I need to have response as {key: value, }. But I have response {key_field: key, value_field: value}. What should I do basically in this case? Does using MongoDB instead of PostgreSQL or SQlite help me? Or any other ideas?
Model looks like:
class Service(models.Model):
name = models.TextField()
version = models.IntegerField()
class ServiceKey(models.Model):
service = models.ForeignKey(
Service,
on_delete=models.CASCADE
)
service_key = models.TextField()
service_value = models.TextField()
views:
#api_view(['GET', ])
def hello(request):
name = request.query_params.get('service')
try:
service = Service.objects.get(name=name)
service_key_instance = ServiceKey.objects.filter(service=service)
serializer = KeySerializer(instance=service_key_instance, many=True)
return Response(data=serializer.data, status=status.HTTP_200_OK)
except:
Response(data='record not found', status=status.HTTP_400_BAD_REQUEST)

on your serializer, you should overwrite to_representation method for example:
class ServiceSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = [
"name",
"version",
]
def to_representation(self, instance):
response = super().to_representation(instance)
response["keys"] = {
key.service_key: key.service_value for key in instance.service_key_set.all()
}
return response
and just use this serializer in your API view

Related

Using Django-Taggit in Django Rest Framework 3.13.1 with Django 4.0.2 with Error "Invalid json list" in Serializing Tags Model Attribute

Older solutions uses django-taggit-serializer which is now been deprecated and doesn't work with the version I have.
I have already done what the documentation says regarding the integration with DRF.
However, I am getting this error:
b'{"tags":["Invalid json list. A tag list submitted in string form must be valid json."]}'
image error in drf browsable api
Here is my code
viewset.py
class AnnouncementViewSet(viewsets.ModelViewSet):
"""Announcement ViewSet."""
queryset = Announcement.objects.all()
serializer_class = AnnouncementSerializer
lookup_field = "slug"
permission_classes = [IsAdminUser, IsAuthorOrReadOnly]
def get_permissions(self):
"""Return permissions."""
if self.action in ["create", "update", "partial_update", "destroy"]:
self.permission_classes = [IsAdminUser]
else:
self.permission_classes = [IsAuthorOrReadOnly]
return super().get_permissions()
models.py
class Announcement(models.Model):
"""Announcement model."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(_("title"), max_length=255, unique=True)
content = models.TextField(_("content"))
is_featured = models.BooleanField(default=False)
thumbnail = models.ImageField(
upload_to="announcement/thumbnail", default="announcement/thumbnail/default.jpg"
)
tags = TaggableManager(_("tags"))
slug = models.SlugField(_("Slug"), max_length=255, unique=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
serializers.py
class AnnouncementSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField()
class Meta:
model = Announcement
exclude = ["slug"]
extra_kwargs = {
"url": {"view_name": "api:announcement-detail", "lookup_field": "slug"}
}
What I have tried so far:
tags = "my-tag"
tags = ["tag1", "tag2"]
I even wrote a test:
tests.py
def test_announcement_create_url(
self, api_client: Client, admin_user
):
api_client.force_login(admin_user)
response = api_client.post(
reverse("api:announcement-list"),
{
"title": "This is a new announcement",
"content": "This is the content of the new announcement",
"is_featured": "True",
"tags": ["tag1", "tag2"],
},
)
print(response.content)
assert response.status_code == status.HTTP_201_CREATED
But it produces the same error.
I also found this GitHub issue but again, django-taggit-serializer is deprecated and is not being maintained anymore.
Found my own answer. The cause of the error is my tags Model Attribute.
If your Model Attribute is named tags then you need to do this in order to submit or tests your API query:
Browsable API
{
"tags": ["tag1", "tag2"]
}
Python Code
{
"tags": '["tag1", "tag2"]'
}
Some fields are omitted for brevity. Make sure to include the required fields needed by your serializer.

Can i make retrieve request in django from model parameter?

I am using Django rest framework for my api. In my views.py file i am using Viewset.ModelViewset.
class SchemaViewSet(viewsets.ModelViewSet):
queryset = models.Schema.objects.all()
serializer_class = serializers.SchemaSerializer
My URL for this is : http://127.0.0.1:7000/api/schema/
THis is giving me GET and POST option. The response is something like this:
{
"id": 1,
"name": "yatharth",
"version": "1.1"
},
To Delete/Put/Patch i have to pass id which is 1 like this: http://127.0.0.1:7000/api/schema/1/.
Can i do this by name like this: http://127.0.0.1:7000/api/schema/yatharth/ instead of id.
My model.py (I can Set name to unique = True)
class Schema(models.Model):
"""Database model for Schema """
name= models.TextField()
version = models.TextField()
def __str__(self):
return self.name
I need id parameter also, so removing it isn't an option but instead of making query by id, i need to make it by name (both are unique)
what i found on django rest framework documentation
def retrieve(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)
But don't know how to change primary key?
You can set do this by setting lookup_field = 'name' in your SchemaViewSet.
eg
class SchemaViewSet(viewsets.ModelViewSet):
queryset = models.Schema.objects.all()
serializer_class = serializers.SchemaSerializer
lookup_field = 'name'

This QueryDict instance is immutable

I have a Branch model with a foreign key to account (the owner of the branch):
class Branch(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE_CASCADE
name = models.CharField(max_length=100)
account = models.ForeignKey(Account, null=True, on_delete=models.CASCADE)
location = models.TextField()
phone = models.CharField(max_length=20, blank=True,
null=True, default=None)
create_at = models.DateTimeField(auto_now_add=True, null=True)
update_at = models.DateTimeField(auto_now=True, null=True)
def __str__(self):
return self.name
class Meta:
unique_together = (('name','account'),)
...
I have a Account model with a foreign key to user (one to one field):
class Account(models.Model):
_safedelete_policy = SOFT_DELETE_CASCADE
name = models.CharField(max_length=100)
user = models.OneToOneField(User)
create_at = models.DateTimeField(auto_now_add=True)
update_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name + ' - ' + self.create_at.strftime('%Y-%m-%d %H:%M:%S')
I've created a ModelViewSet for Branch which shows the branch owned by the logged in user:
class BranchViewSet(viewsets.ModelViewSet):
serializer_class = BranchSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
queryset = Branch.objects.all().filter(account=self.request.user.account)
return queryset
Now to create a new branch, I want to save account field with request.user.account, not with data sent from the rest client (for more security). for example:
def create(self, request, *args, **kwargs):
if request.user.user_type == User.ADMIN:
request.data['account'] = request.user.account
return super(BranchViewSet, self).create(request, *args, **kwargs)
def perform_create(self, serializer):
'''
Associate branch with account
'''
serializer.save(account=self.request.user.account)
In branch serializer
class BranchSerializer(serializers.ModelSerializer):
account = serializers.CharField(source='account.id', read_only=True)
class Meta:
model = Branch
fields = ('id', 'name', 'branch_alias',
'location', 'phone', 'account')
validators = [
UniqueTogetherValidator(
queryset=Branch.objects.all(),
fields=('name', 'account')
)
]
but I got this error:
This QueryDict instance is immutable. (means request.data is a immutable QueryDict and can't be changed)
Do you know any better way to add additional fields when creating an object with django rest framework?
As you can see in the Django documentation:
The QueryDicts at request.POST and request.GET will be immutable when accessed in a normal request/response cycle.
so you can use the recommendation from the same documentation:
To get a mutable version you need to use QueryDict.copy()
or ... use a little trick, for example, if you need to keep a reference to an object for some reason or leave the object the same:
# remember old state
_mutable = data._mutable
# set to mutable
data._mutable = True
# сhange the values you want
data['param_name'] = 'new value'
# set mutable flag back
data._mutable = _mutable
where data it is your QueryDicts
Do Simple:
#views.py
from rest_framework import generics
class Login(generics.CreateAPIView):
serializer_class = MySerializerClass
def create(self, request, *args, **kwargs):
request.data._mutable = True
request.data['username'] = "example#mail.com"
request.data._mutable = False
#serializes.py
from rest_framework import serializers
class MySerializerClass(serializers.Serializer):
username = serializers.CharField(required=False)
password = serializers.CharField(required=False)
class Meta:
fields = ('username', 'password')
request.data._mutable=True
Make mutable true to enable editing in querydict or the request.
I personally think it would be more elegant to write code like this.
def create(self, request, *args, **kwargs):
data = OrderedDict()
data.update(request.data)
data['account'] = request.user.account
serializer = self.get_serializer(data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Do you know any better way to add additional fields when creating an object with django rest framework?
The official way to provide extra data when creating/updating an object is to pass them to the serializer.save() as shown here
https://docs.djangoproject.com/en/2.0/ref/request-response/#querydict-objects
The QueryDicts at request.POST and request.GET will be immutable when accessed in a normal request/response cycle. To get a mutable version you need to use QueryDict.copy().
You can use request=request.copy() at the first line of your function.

Using Django-taggit with django-rest-framework, i'm not able to save my tags

I'm trying to figure it out why when i submit my form, my tags are not saved in my db. Pretty new with the django-rest-framework and Django-taggit too, i think i'm doing something wrong :)
First, before making my API with the rest-framework, i was using a generic view (CreateView and UpdateView) to register/validate my event. It was working fine but i decided to go further and try to build an API since i'm using Angularjs now.
Now my model event is created but without my tag and i have some errors. I put some code and i'll describe my errors after.
events/models.py
class Event(models.Model):
[...]
title = models.CharField(max_length=245, blank=False)
description = models.TextField(max_length=750, null=True, blank=True)
start = models.DateTimeField()
end = models.DateTimeField()
created_at = models.DateTimeField(editable=False)
updated_at = models.DateTimeField(editable=False)
slug = AutoSlugField(populate_from='title', unique=True, editable=False)
expert = models.BooleanField(choices=MODE_EXPERT, default=0)
home = models.BooleanField(choices=HOME, default=0)
nb_participant = models.PositiveSmallIntegerField(default=1)
price = models.PositiveSmallIntegerField(default=0)
cancelled = models.BooleanField(default=0)
user = models.ForeignKey(User, editable=False, related_name='author')
address = models.ForeignKey('Address', editable=False, related_name='events')
participants = models.ManyToManyField(User, related_name='participants', blank=True, editable=False,
through='Participants')
theme_category = models.ForeignKey('EventThemeCategory', unique=True, editable=False)
tags = TaggableManager(blank=True)
class Meta:
db_table = 'event'
def save(self, *args, **kwargs):
if not self.pk:
self.created_at = timezone.now()
self.updated_at = timezone.now()
super(Event, self).save(*args, **kwargs)
[...]
i'm using the serializers.HyperlinkedModelSerializer.
api/serializer.py
from taggit.models import Tag
class TagListSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Tag
fields = ('url', 'id', 'name')
class EventSerializer(serializers.HyperlinkedModelSerializer):
address = AddressSerializer()
user = UserSerializer(required=False)
tags = TagListSerializer(blank=True)
class Meta:
model = Event
fields = ('url', 'id', 'title', 'description', 'start', 'end', 'created_at', 'updated_at', 'slug', 'expert','home', 'nb_participant', 'price', 'address', 'user', 'theme_category', 'tags')
depth = 1
api/views/tags_views.py
from rest_framework import generics
from api.serializers import TagListSerializer
from taggit.models import Tag
class TagsListAPIView(generics.ListCreateAPIView):
queryset = Tag.objects.all()
model = Tag
serializer_class = TagListSerializer
class TagsDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Tag.objects.all()
model = Tag
serializer_class = TagListSerializer
api/views/events_views.py
class EventListAPIView(generics.ListCreateAPIView):
queryset = Event.objects.all()
model = Event
serializer_class = EventSerializer
paginate_by = 100
def pre_save(self, obj):
"""
Set the object's owner, based on the incoming request.
"""
obj.user = self.request.user
return super(EventListAPIView, self).pre_save(obj)
api/urls.py
url(r'^events/(?P<slug>[0-9a-zA-Z_-]+)/$', EventDetailAPIView.as_view(), name='event-detail'),
So first when i call /api/events/name-of-my-event the API send me the good resource with my tags on it. The GET method is working fine.
I was thinking that rest-framework follow the query set. So if i can get the resource with with all my tags why when i use POST my tags are not register ?
Actually i have two problems with the POST method:
first one if i send a tag which i have already created, he send me an error saying that the tag must be unique. I understand that, i don't want to create a new one, i just want it to be linked with my object. I don't have this problem when i use the generic view (it's done by magic :) and all is working fine)
Secondly, when i try to create a new tag, my new event is saved but without my tags.
You can see the response received by angularjs for my tag... He send me the name of the tag but without id, url (hyperlinked). When i checked my db the tag has not been created.
I think i have to make a custom get_queryset(self) in my tags_views but i'm not sure.
I'll will continue to investigate. If someone have already to that and have some advise, i'll be very API. Thanks.
meet the same question. But I just want to save the tag list directly by TaggableManager (without TagListSerializer and TagsListAPIView). My solution is:
class MyModel(models.Model):
...
tags = TaggableManager(blank=True)
def get_tags_display(self):
return self.tags.values_list('name', flat=True)
class MyModelSerializer(serializers.HyperlinkedModelSerializer):
...
tags = serializers.Field(source='get_tags_display') # more about: http://www.django-rest-framework.org/api-guide/fields#generic-fields
...
class MyModelViewSet(viewsets.ModelViewSet):
...
def post_save(self, *args, **kwargs):
if 'tags' in self.request.DATA:
self.object.tags.set(*self.request.DATA['tags']) # type(self.object.tags) == <taggit.managers._TaggableManager>
return super(MyModelViewSet, self).post_save(*args, **kwargs)
The post data of tags data will be ['tagA', 'tagB',...], the TaggableManager will handle it. Thx.
For DRF>3.1, you just need to override create and update in your ModelSerializer class:
class StringListField(serializers.ListField): # get from http://www.django-rest-framework.org/api-guide/fields/#listfield
child = serializers.CharField()
def to_representation(self, data):
return ' '.join(data.values_list('name', flat=True)) # you change the representation style here.
class MyModelSerializer(serializers.ModelSerializer):
tags = StringListField()
class Meta:
model = models.MyModel
def create(self, validated_data):
tags = validated_data.pop('tags')
instance = super(MyModelSerializer, self).create(validated_data)
instance.tags.set(*tags)
return instance
def update(self, instance, validated_data):
# looks same as create method
I used to follow the following ways to serialize taggit objects but currently django-taggit provide a built in serializer https://github.com/jazzband/django-taggit/blob/master/taggit/serializers.py and it was vendor from the package I mentioned previously.
"""
Django-taggit serializer support
Originally vendored from https://github.com/glemmaPaul/django-taggit-serializer
"""
import json
# Third party
from django.utils.translation import gettext_lazy
from rest_framework import serializers
class TagList(list):
def __init__(self, *args, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
super().__init__(*args, **kwargs)
self.pretty_print = pretty_print
def __add__(self, rhs):
return TagList(super().__add__(rhs))
def __getitem__(self, item):
result = super().__getitem__(item)
try:
return TagList(result)
except TypeError:
return result
def __str__(self):
if self.pretty_print:
return json.dumps(self, sort_keys=True, indent=4, separators=(",", ": "))
else:
return json.dumps(self)
class TagListSerializerField(serializers.Field):
child = serializers.CharField()
default_error_messages = {
"not_a_list": gettext_lazy(
'Expected a list of items but got type "{input_type}".'
),
"invalid_json": gettext_lazy(
"Invalid json list. A tag list submitted in string"
" form must be valid json."
),
"not_a_str": gettext_lazy("All list items must be of string type."),
}
order_by = None
def __init__(self, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
style = kwargs.pop("style", {})
kwargs["style"] = {"base_template": "textarea.html"}
kwargs["style"].update(style)
super().__init__(**kwargs)
self.pretty_print = pretty_print
def to_internal_value(self, value):
if isinstance(value, str):
if not value:
value = "[]"
try:
value = json.loads(value)
except ValueError:
self.fail("invalid_json")
if not isinstance(value, list):
self.fail("not_a_list", input_type=type(value).__name__)
for s in value:
if not isinstance(s, str):
self.fail("not_a_str")
self.child.run_validation(s)
return value
def to_representation(self, value):
if not isinstance(value, TagList):
if not isinstance(value, list):
if self.order_by:
tags = value.all().order_by(*self.order_by)
else:
tags = value.all()
value = [tag.name for tag in tags]
value = TagList(value, pretty_print=self.pretty_print)
return value
class TaggitSerializer(serializers.Serializer):
def create(self, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super().create(validated_data)
return self._save_tags(tag_object, to_be_tagged)
def update(self, instance, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super().update(instance, validated_data)
return self._save_tags(tag_object, to_be_tagged)
def _save_tags(self, tag_object, tags):
for key in tags.keys():
tag_values = tags.get(key)
getattr(tag_object, key).set(tag_values)
return tag_object
def _pop_tags(self, validated_data):
to_be_tagged = {}
for key in self.fields.keys():
field = self.fields[key]
if isinstance(field, TagListSerializerField):
if key in validated_data:
to_be_tagged[key] = validated_data.pop(key)
return (to_be_tagged, validated_data)
http://blog.pedesen.de/2013/07/06/Using-django-rest-framework-with-tagged-items-django-taggit/
With the release of the Django Rest Framework 3.0, the code for the TagListSerializer has changed slightly. The serializers.WritableField was depreciated in favour for serializers.Field for the creation of custom serializer fields such as this. Below is the corrected code for Django Rest Framework 3.0.
class TagListSerializer(serializers.Field):
def to_internal_value(self, data):
if type(data) is not list:
raise ParseError("expected a list of data")
return data
def to_representation(self, obj):
if type(obj) is not list:
return [tag.name for tag in obj.all()]
return obj
I now use the bulit in taggit serializer which was taken from https://github.com/glemmaPaul/django-taggit-serializer library.
I had a bunch of errors but i found a way to resolve my problem. Maybe not the best as i'm pretty new with all of this but for now it works.
I'll try to describe all my errors maybe it'll help someone.
First my angularjs send a json which match exatly the queryset
So for example with my model events below, angularjs send to the API:
Now let's begin with all my errors:
"A tag with this name already exist"
When i re-use a tag i have this error. Don't know why because with a classic validation without the API, all is working fine.
With a new tag nothing is saved too.
When i try to use a new tag on my event event model nothing is saved on the database. Angularjs received a response with the tag name but with an id of null (see the pitcure on my original question)
"AttributeError: 'RelationsList' object has no attribute 'add'"
Now i'm trying to think that to register my tags i need to have an instance of event already created. Thanks to that i will be able to add my tag on it like it's describe in the doc.
apple.tags.add("red", "green", "fruit")
So i decided to add a post_save in my events_views.py:
class EventListAPIView(generics.ListCreateAPIView):
queryset = Event.objects.all()
model = Event
serializer_class = EventSerializer
paginate_by = 100
def pre_save(self, obj):
"""
Set the object's owner, based on the incoming request.
"""
obj.user = self.request.user
return super(EventListAPIView, self).pre_save(obj)
def post_save(self, obj, created=False):
print 'tags', self.request.DATA
obj.tags.add(self.request.DATA['tags'])
return super(EventListAPIView, self).post_save(obj)
But now as is said i have this error AttributeError: 'RelationsList' object has no attribute 'add'.
Actually, it's obvious since obj.tags is a list of object and not the TaggableManager anymore.
So i decided to start over and send my tags not in 'tags' but in another custom property 'tagged' to avoid conflit with the TaggableManager.
"TypeError: unhashable type: 'list'"
New error :) I found the solution with this django-taggit-unhashable-type-list
def post_save(self, obj, created=False):
map(obj.tags.add, self.request.DATA['tagged'])
return super(EventListAPIView, self).post_save(obj)
"TypeError: unhashable type: 'dict'"
Now, i figured it out that the tags i sent are not well formatted. I changed it (on the angularjs side) to send an array like this ['jazz','rock'] instead of [object, object]. Stupid mistake from a beginner.
Now the magic happen, response received by angularjs is good:
Sorry for my english. I know it may not be the best solution and i will try to update it when i'll find another solution.

Per Field Permission in Django REST Framework

I am using Django REST Framework to serialize a Django model. I have a ListCreateAPIView view to list the objects and a RetrieveUpdateDestroyAPIView view to retrieve/update/delete individual objects. The model stores information that the users submit themselves. The information they submit contains some private information and some public information. I want all users to be able to list and retrieve the public information but I want only the owner to list/retrieve/update/delete the private information. Therefore, I need per-field permissions and not object permissions.
The closest suggestion I found was https://groups.google.com/forum/#!topic/django-rest-framework/FUd27n_k3U0 which changes the serializer based on the request type. This won't work for my situation because I don't have the queryset or object at that point to determine if it is owned by the user or not.
Of course, I have my frontend hiding the private information but smart people can still snoop the API requests to get the full objects. If code is needed, I can provide it but my request applies to vanilla Django REST Framework designs.
How about switching serializer class based on user?
In documentation:
http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself
def get_serializer_class(self):
if self.request.user.is_staff:
return FullAccountSerializer
return BasicAccountSerializer
I had a similar problem the other day. Here is my approach:
This is a DRF 2.4 solution.
class PrivateField(serializers.Field):
def field_to_native(self, obj, field_name):
"""
Return null value if request has no access to that field
"""
if obj.created_by == self.context.get('request').user:
return super(PrivateField, self).field_to_native(obj, field_name)
return None
#Usage
class UserInfoSerializer(serializers.ModelSerializer):
private_field1 = PrivateField()
private_field2 = PrivateField()
class Meta:
model = UserInfo
And a DRF 3.x solution:
class PrivateField(serializers.ReadOnlyField):
def get_attribute(self, instance):
"""
Given the *outgoing* object instance, return the primitive value
that should be used for this field.
"""
if instance.created_by == self.context['request'].user:
return super(PrivateField, self).get_attribute(instance)
return None
This time we extend ReadOnlyField only because to_representation is not implemented in the serializers.Field class.
I figured out a way to do it. In the serializer, I have access to both the object and the user making the API request. I can therefore check if the requestor is the owner of the object and return the private information. If they are not, the serializer will return an empty string.
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = serializers.SerializerMethodField('get_private_field1')
class Meta:
model = UserInfo
fields = (
'id',
'public_field1',
'public_field2',
'private_field1',
)
read_only_fields = ('id')
def get_private_field1(self, obj):
# obj.created_by is the foreign key to the user model
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
Here:
-- models.py:
class Article(models.Model):
name = models.CharField(max_length=50, blank=False)
author = models.CharField(max_length=50, blank=True)
def __str__(self):
return u"%s" % self.name
class Meta:
permissions = (
# name
('read_name_article', "Read article's name"),
('change_name_article', "Change article's name"),
# author
('read_author_article', "Read article's author"),
('change_author_article', "Change article's author"),
)
-- serializers.py:
class ArticleSerializer(serializers.ModelSerializer):
class Meta(object):
model = Article
fields = "__all__"
def to_representation(self, request_data):
# get the original representation
ret = super(ArticleSerializer, self).to_representation(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if not current_user.has_perm(
'app_name.read_{}_article'.format(field_name)
):
ret.pop(field_name) # remove field if it's not permitted
return ret
def to_internal_value(self, request_data):
errors = {}
# get the original representation
ret = super(ArticleSerializer, self).to_internal_value(request_data)
current_user = self.context['request'].user
for field_name, field_value in sorted(ret.items()):
if field_value and not current_user.has_perm(
'app_name.change_{}_article'.format(field_name)
):
errors[field_name] = ["Field not allowed to change"] # throw error if it's not permitted
if errors:
raise ValidationError(errors)
return ret
For a solution that allows both reading and writing, do this:
class PrivateField(serializers.Field):
def get_attribute(self, obj):
# We pass the object instance onto `to_representation`,
# not just the field attribute.
return obj
def to_representation(self, obj):
# for read functionality
if obj.created_by != self.context['request'].user:
return ""
else:
return obj.private_field1
def to_internal_value(self, data):
# for write functionality
# check if data is valid and if not raise ValidationError
class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
private_field1 = PrivateField()
...
See the docs for an example.
This is an old question, but the topic is still relevant.
DRF recommends to create different serializers for different permission. But this approach only works, if you have only a few permissions or groups.
restframework-serializer-permissions is a drop in replacement for drf serializers.
Instead of importing the serializers and fields from drf, you are importing them from serializer_permissions.
Installation:
$ pip install restframework-serializer-permissions
Example Serializers:
# import permissions from rest_framework
from rest_framework.permissions import AllowAny, IsAuthenticated
# import serializers from serializer_permissions instead of rest_framework
from serializer_permissions import serializers
# import you models
from myproject.models import ShoppingItem, ShoppingList
class ShoppingItemSerializer(serializers.ModelSerializer):
item_name = serializers.CharField()
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('item_name', )
class ShoppingListSerializer(serializers.ModelSerializer):
# Allow all users to list name
list_name = serializers.CharField(permission_classes=(AllowAny, ))
# Only allow authenticated users to retrieve the comment
list_comment = serializers.CharField(permissions=(IsAuthenticated, ))
# show owner only, when the current user has 'auth.view_user' permission
owner = serializers.CharField(permissions=('auth.view_user', ), hide=True)
# serializer which is only available, when the user is authenticated
items = ShoppingItemSerializer(many=True, permissions=(IsAuthenticated, ), hide=True)
class Meta:
# metaclass as described in drf docs
model = ShoppingItem
fields = ('list_name', 'list_comment', 'owner', 'items', )
Disclosure: I'm the author of this extension
In case you are performing only READ operations, you can just pop the fields in to_representation method of the serializer.
def to_representation(self,instance):
ret = super(YourSerializer,self).to_representation(instance)
fields_to_pop = ['field1','field2','field3']
if instance.created_by != self.context['request'].user.id:
[ret.pop(field,'') for field in fields_to_pop]
return ret
This should be enough to hide sensitive fields.
Just share another possible solution
For example, to make email only show for oneself.
On UserSerializer, add:
email = serializers.SerializerMethodField('get_user_email')
Then implement get_user_email like this:
def get_user_email(self, obj):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
return obj.email if user.id == obj.pk else 'HIDDEN'
I solved it using a serializer Mixin:
class FieldPermissionModelSerializerMixin(serializers.ModelSerializer):
"""
A mixin that allows you to specify what fields will be returned based on field level permissions
"""
permission_fields = []
def get_field_names(self, declared_fields, info) -> List:
"""Determine the fields to apply."""
fields = getattr(self.Meta, "fields", [])
for permission_field in self.permission_fields:
app_name = getattr(self.Meta, "model", None)._meta.app_label
permission_name = f"can_view_field_{permission_field}"
full_permission_name = f"{app_name}.{permission_name}"
if self.context["request"].user.has_perm(full_permission_name):
fields.append(permission_field)
return fields
Then you can use this serializer with base fields and permissionable fields.
POSITION_BASE_FIELDS = [
"id",
"name",
"level",
"role",
"sort",
]
POSITION_PERMISSION_FIELDS = ["market_salary", "recommended_rate_per_hour"]
class PositionListSerializer(FieldPermissionModelSerializerMixin):
permission_fields = POSITION_PERMISSION_FIELDS
class Meta:
model = Position
fields = POSITION_BASE_FIELDS + []
This is then based on field level permissions defined on the model.
class Position(models.Model):
name = models.CharField(max_length=255, db_index=True)
level = models.CharField(max_length=255, null=True, blank=True)
sort = models.IntegerField(blank=True, default=0)
market_salary = models.DecimalField(max_digits=19, decimal_places=2, default=0.00)
recommended_rate_per_hour = models.DecimalField(
max_digits=7, decimal_places=2, null=True, blank=True
)
class Meta:
ordering = ["name", "sort"]
unique_together = ("name", "level")
permissions = (
("can_view_field_market_salary", "Can view field: market_salary"),
(
"can_view_field_recommended_rate_per_hour",
"Can view field: recommended_rate_per_hour",
),
)