Is there a way to set the id of an existing instance as the value of a nested serializer in DRF? - json

I'm developing a chat application. I have a serializer like this:
class PersonalChatRoomSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalChatRoom
fields = '__all__'
user_1 = UserSerializer(read_only=True)
user_2 = UserSerializer()
the user_1 field is auto-populated but the client should provide the user_2 field in order to create a personal chat room with another user.
My problem is, when creating a new chat room, the serializer tries to create a new user object from the input data thus giving me validation errors. What I really want it to do is to accept a user id and set the value of user_2 field to an existing user instance that is currently available in the database and if the user is not found, simply return a validation error. (the exact behavior of PrimaryKeyRelatedField when creating a new object)
I want my input data to look like this:
{
'user_2': 1 // id of the user
}
And when I retrieve my PersonalChatRoom object, I want the serialized form of the user object for my user_2 field:
{
...,
'user_2': {
'username': ...,
'the_rest_of_the_fields': ...
}
}
How can I achieve this?
views.py
class GroupChatRoomViewSet(viewsets.ModelViewSet):
permission_classes = [IsUserVerified, IsGroupOrIsAdminOrReadOnly]
serializer_class = GroupChatRoomSerializer
def get_queryset(self):
return self.request.user.group_chat_rooms.all()
def perform_create(self, serializer):
return serializer.save(owner=self.request.user)

I finally figured out how to do it. I just needed to override the to_representation method and serialize the object there. Here is the code I ended up with:
class PersonalChatRoomSerializer(serializers.ModelSerializer):
class Meta:
model = PersonalChatRoom
fields = '__all__'
read_only_fields = ['user_1']
def to_representation(self, chat_room):
""" Serialize user instances when outputing the results """
obj = super().to_representation(chat_room)
for field in obj.keys():
if field.startswith('user_'):
obj[field] = UserSerializer(User.objects.get(pk=obj[field])).data
return obj

Related

SQLALchemy update ARRAY column [duplicate]

I'm working on a project using Flask and a PostgreSQL database, with SQLAlchemy.
I have Group objects which have a list of User IDs who are members of the group. For some reason, when I try to add an ID to a group, it will not save properly.
If I try members.append(user_id), it doesn't seem to work at all. However, if I try members += [user_id], the id will show up in the view listing all the groups, but if I restart the server, the added value(s) is (are) not there. The initial values, however, are.
Related code:
Adding group to the database initially:
db = SQLAlchemy(app)
# ...
g = Group(request.form['name'], user_id)
db.session.add(g)
db.session.commit()
The Group class:
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.postgresql import ARRAY
class Group(db.Model):
__tablename__ = "groups"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
leader = db.Column(db.Integer)
# list of the members in the group based on user id
members = db.Column(ARRAY(db.Integer))
def __init__(self, name, leader):
self.name = name
self.leader = leader
self.members = [leader]
def __repr__(self):
return "Name: {}, Leader: {}, Members: {}".format(self.name, self.leader, self.members)
def add_user(self, user_id):
self.members += [user_id]
My test function for updating the Group:
def add_2_to_group():
g = Group.query.all()[0]
g.add_user(2)
db.session.commit()
return redirect(url_for('show_groups'))
Thanks for any help!
As you have mentioned, the ARRAY datatype in sqlalchemy is immutable. This means it isn’t possible to add new data into array once it has been initialised.
To solve this, create class MutableList.
from sqlalchemy.ext.mutable import Mutable
class MutableList(Mutable, list):
def append(self, value):
list.append(self, value)
self.changed()
#classmethod
def coerce(cls, key, value):
if not isinstance(value, MutableList):
if isinstance(value, list):
return MutableList(value)
return Mutable.coerce(key, value)
else:
return value
This snippet allows you to extend a list to add mutability to it. So, now you can use the class above to create a mutable array type like:
class Group(db.Model):
...
members = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
...
You can use the flag_modified function to mark the property as having changed. In this example, you could change your add_user method to:
from sqlalchemy.orm.attributes import flag_modified
# ~~~
def add_user(self, user_id):
self.members += [user_id]
flag_modified(self, 'members')
To anyone in the future: so it turns out that arrays through SQLAlchemy are immutable. So, once they're initialized in the database, they can't change size. There's probably a way to do this, but there are better ways to do what we're trying to do.
This is a hacky solution, but what you can do is:
Store the existing array temporarily
Set the column value to None
Set the column value to the existing temporary array
For example:
g = Group.query.all()[0]
temp_array = g.members
g.members = None
db.session.commit()
db.session.refresh(g)
g.members = temp_array
db.session.commit()
In my case it was solved by using the new reference for storing a object variable and assiging that new created variable in object variable.so, Instead of updating the existing objects variable it will create a new reference address which reflect the changes.
Here in Model,
Table: question
optional_id = sa.Column(sa.ARRAY(sa.Integer), nullable=True)
In views,
option_list=list(question.optional_id if question.optional_id else [])
if option_list:
question.optional_id.clear()
option_list.append(obj.id)
question.optional_id=option_list
else:
question.optional_id=[obj.id]

Adding Additional Data to a Serialize Response in Django

Updated
I changed my simplified question into a real example.
I've created a working post response of data from the model using ModelSerialzer, which I call from a post method in a view class. I would like to add additional data to the response. This is the pertinent code from my CBV:
def post(self, request, format=None):
user_profile = UserProfiles.objects.get(user=request.user.id)
service_id = user_profile.service_id
rec_filter = Recommendations.objects.values_list('resource')
if service_id > 0:
service_name = Services.objects.get(pk=service_id)
programs = Programs.objects.filter(services=service_id)
resources_filtered = Resources.objects.filter(program__in=programs).exclude(id__in=rec_filter)
else:
service_name = 'All Services'
resources_filtered = Resources.objects.exclude(id__in=rec_filter)
serializer = ResourceSerializer(resources_filtered, many=True)
#serializer.data["service"] = service_name
return Response(serializer.data)
The commented out line was my attempt to add data base on a similar post here. I get a 500 response in my API call. What is the correct way to do it? The response data is JSON if that's necessary to mention.
This is the ModelSerializer:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail'
]
Test of the answer
Heres the updated code based on the answer with a correction to fix and error:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
service = serializers.SerializerMethodField()
def get_service(self, obj):
return "All Services"
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail',
'service'
]
The problem with this approach is that the value "All Services" is repeated in every row serialized. It's only needed once. I'd also like to keep the data transmitted minimized.
The problem with the original attempt is that serializer.data is immutable. It's necessary to make a copy and add to it.
serializer = ResourceSerializer(resources_filtered, many=True)
augmented_serializer_data = list(serializer.data)
augmented_serializer_data.append({'service': 'All Services'})
return Response(augmented_serializer_data)
This answer is based on one given by #andre-machado in this question.
This code here is an example to coincide with the other answer given.
You can do it in serializer itself. Define the new field required and add it in fields. Mark all the fields in serializer from resource model.
class ResourceSerializer(serializers.ModelSerializer):
service = serializers.SerializerMethodField()
def get_service(self):
return "All Services"
class Meta :
model = Resources
fields = ('service') #Mark all the fields required here from resource model
You can do it from the serilaizer. In this case i was adding the field isOpen to the response and this is how i did it .timeDifference is the name of the function that was to generate data for the extra field . I hope it helps
class ShopSearchSerializer(serializers.ModelSerializer):
isOpen = serializers.SerializerMethodField('timeDifference')
def timeDifference(self,*args):
requestTime = datetime.now()
return requestTime
class Meta:
model = Shop
fields =['name','city','street','house','opening_time','closing_time','isOpen']

How to modify a many-to-many collection using django rest framework

I am trying to create an endpoint where, having a User entity, I can add / remove existing Group entities to user.groups many-to-many field. But when I try to do it, django-rest-framework tries to create new group objects instead of finding existing ones.
I have defined two serializers where UserSerializer has a nested GroupSerializer:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'groups']
groups = GroupSerializer(many=True)
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.groups.clear()
for group in groups:
instance.groups.add(group)
return instance
def create(self, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
instance = self.Meta.model.objects.create(**data)
for group in groups:
instance.groups.add(group)
return instance
When I send a JSON through a PUT REST call (from django-rest-framework web interface):
{
"id": 6,
"username": "user5#example.com",
"email": "user5#example.com",
"groups": [
{
"id": 1,
"name": "AAA"
}
]
}
I expect serializer to find the Group with given id and add it to User. But instead, it tries to create a new user group and fails with duplicate key error:
{
"groups": [
{
"name": [
"group with this name already exists."
]
}
]
}
I searched over the internet and debugged myself and found no solution to this use case.
The create and update methods inside UserSerializerclass are never reached.
Edit: as asked, here are my views and urls:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
Urls:
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
This seems to be the validation error due to a nested serializer model that contains unique constraint, see this post. According to the article, DRF did not handle this condition since it's hard to realize if the serializer is a nested serializer within another one. And that's why the create() and update() never been reached since the validation is done before calling them.
The way to work around this is to remove the uniqueness validator manually in GroupSerializer as follow:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
extra_kwargs = {
'name': {'validators': []},
}
BTW, there are some points that can be improved or should be corrected in your update() and create() code. Firstly, you didn't do instance.save() so the instance won't be update after the whole process done. Second, the groups are just a list of dictionary, and you should not add object that way. The following are the modification based on your OP code:
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.save() # This will indeed update DB values
group_ids = [g['id'] for g in groups]
instance.groups.clear()
instance.groups.add(*group_ids) # Add all groups once. Also you can replace these two lines with
# instance.groups.set(group_ids)
return instance
Though am late, here is how I did it, adding to Tsang-Yi Shen answer. However this worked for me because I was using django-role-permissions https://django-role-permissions.readthedocs.io/en/stable/setup.html
For the group serializer am only interested in the name.
from rolepermissions.roles import assign_role
class GroupModelSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ['name']
extra_kwargs = {
'name': {'validators': []},
}
def get_name(self, obj):
"""
This method modifies the way the name field is returned in a get request.
"""
return [group.name for group in obj.objects.all()]
In the UserSerializer, I modify the groups field to be returned as a list of groups a user belongs to, and received as a list instead of an OrderedDict in the JSON payload for creating a User, which can look something like this:
{
"email": "testuser#example.com",
"username": "testuser",
"name": "Test User",
"password": "password123",
"groups": ["doctor", "nurse", ]
}
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
groups = ListField(required=False, default=[], write_only=True)
user_groups = serializers.SerializerMethodField(read_only=True)
class Meta:
model = User
fields = ["id", "email", "username", "name", "password", "groups", "user_groups"]
depth = 2
def get_user_groups(self, obj):
"""
This method modifies the way the `groups` field is returned in a get request.
"""
return [group.name for group in obj.groups.all()]
def create(self, validated_data):
password = validated_data.pop('password')
gropus_to_add_user = validated_data.pop("groups")
user = User(**validated_data)
user.set_password(password)
user.save()
for group_name in groups_to_add_user:
# The assign_role function adds the user to a group.
# In this case group and roles may mean the same thing.
assign_role(user, group_name)
return user
You will have something like this when creating a user, here am using Swagger:

Django add new field on model if new object is created from another model

I have two models:
class Exam(models.Model):
name = models.CharField(max_length=100,default="test")
user = models.ForeignKey(User)
class UserScore(models.Model):
user = models.OneToOneField(User,primary_key=True,on_delete=models.CASCADE)
resultat = models.IntegerField(default=0)
When I create a new exam create dynamically I want a new field on my userscore model for all user.
you can use Django signals for that
eg.
from django.db.models.signals import post_save
def create_userscore(sender, instance, **kwargs):
if created:
UserScore.objects.create()
post_save.connect(create_userscore(), sender=Exam)
this will create a UserScore object everytime Exam objects get created

How to do an update method for a nested django rest framework APi Boolean ? [OnetoOneField]

So i have been researching about how to update the nested serializer with onetoonefield. However it has not been able to solve my problem. As i am still new to django rest framework, i am still inexperience about what is the problem as i never done an API before.
models.py
class Membership(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
membership = models.BooleanField(default=False)
serializers.py
class MembershipSerializer(serializers.ModelSerializer):
class Meta:
model = Membership
fields = ('membership',)
class UserSerializer(serializers.ModelSerializer):
membership = MembershipSerializer(many=False)
class Meta:
model = User
fields = ('id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_staff', 'membership',)
read_only_fields = ('id',)
def create(self, validated_data):
membership_data = validated_data.pop('membership')
user = User.objects.create(**validated_data)
Membership.objects.create(user=user, **membership_data)
return user
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
instance.email = validated_data.get('email', instance.email)
instance.password = validated_data.get('password', instance.password)
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)
instance.is_staff = validated_data.get('is_staff', instance.is_staff)
instance.save()
membership_data = validated_data.get('membership')
membership_id = membership_data.get('id', None)
if membership_id:
membership_item = Membership.objects.get(id=membership_id, membership=instance)
membership_item.membership = membership_data.get('membership', membership_item.name)
membership_item.user = membership_data.get('user', membership_item.user)
membership_item.save()
return instance
views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_permissions(self):
# allow non-authenticated user to create
return (AllowAny() if self.request.method == 'POST'
else permissions.IsStaffOrTargetUser()),
screenshot of api
https://i.imgur.com/dDqthRu.png
As you can see above, my membership is null, i have no idea why so i tested with is_staff to check and it is using false like a normal Booleanfield. This has make me wonder what is wrong with my models for the membership boolean field.
Main problem
As i am using a boolean field, i was trying to get the user membership to be updated. So i try to use PUT method and the result is nothing has change after i check the membership box and click on PUT.
And if i just want to update the username, i have to check on the membership box else it will give me this:
https://i.imgur.com/KpzHIsE.png
I have been checking online for several solution and none of them has work for me with the update method. I am also puzzle by the null value in the api for the booleanfield membership.