A simplified view of my models:
# models.py
class User(models.Model):
first_name = models.CharField()
last_name = models.CharField()
team = models.ForeignKey('Team')
...
class Team(models.Model):
name = models.CharField()
class ToDo(models.Model):
task = models.CharField()
description = models.TextField()
owner = models.ForeignKey('User')
# serializers.py
class ToDoSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = ToDo
fields = '__all__'
I want to create a POST endpoint to add a new ToDo object based on the following logic:
Users can create ToDo items for themselves
Users can create ToDo items for others in their Team
Users cannot create ToDo items for others who aren't in their team
Question: Where do is write this logic
I attempted this by using Permission classes but I don't know if that is the best place to do this
# views.py
class ToDoViewSet(viewsets.ModelViewSet):
serializer_class = ToDoSerializer
permission_classes = (CanAddToDo,)
# permissions.py
class CanAddToDo(BasePermission):
def has_permission(self, request, view):
owner_id = request.data.get('owner', None)
# owner_id must be set
if not owner_id:
return False
# User can create items if owner is themselves or someone in their team
if User.objects.get(pk=owner_id).team == request.user.team:
return True
return False
def has_object_permission(self, request, view, obj):
"""
Checks if the user owns the todo to edit
"""
return obj.owner == request.user
What's bugging me about that is I'm not using the serialized data and instead, getting the raw owner's id from the request and making a query in the permissions object to do my validation/permission
Other options could be to do this validation in the views' def perform_create(self, serializer): function or in the serializer its self.
There is another way to look at this; validation for owner
In your serializer for ToDo you can write a validation for owner field
class ToDoSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = ToDo
fields = '__all__'
def validate_owner(self, val):
owner = val
request = self.context.get('request', None)
# it is possible you are using serializer outside an api view
# in which case reqeust will not be present in the serializer context
if request:
if owner.team != request.user.team:
raise serializers.ValidationError('you can only create todos for'
'yourself or your team members')
return val
This is works well for creation. But for update or delete you need to check the current owner on the todo object. Which can be done in a permission class. You can use the id from the request url to get the todo object in the permission class.
Related
I have a custom User model and a Group model that are linked by a UserGroup through model (Many to Many relationship):
models.py
class User(models.Model):
username = models.CharField(primary_key=True, max_length=32, unique=True)
user_email = models.EmailField(max_length=32, unique=False) # Validates an email through predefined regex which checks ‘#’ and a ‘.’
user_password = models.CharField(max_length=32)
user_avatar_path = models.CharField(max_length=64)
class Group(models.Model):
group_id = models.AutoField(primary_key=True)
group_name = models.CharField(max_length=32, unique=False)
group_admin = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='my_groups'
)
members = models.ManyToManyField(
User,
related_name='groups', # The name to use for the relation from the related object back to this one.
through='UserGroup' # Attaches a Junction table to the Many to Many relationship.
)
class UserGroup(models.Model): # Manually specified Junction table for User and Group
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='user_groups'
)
group = models.ForeignKey(
Group,
on_delete=models.CASCADE,
related_name='user_groups'
)
I'm trying to associate multiple users with a group, using a PATCH request to update the members attribute of a group. Using the following GroupSerializer, I'm able to associate a user as a member of the group when the group is created, by overriding the create function of the serializer:
serializers.py
class GroupSerializer(serializers.ModelSerializer):
members = MemberSerializer(many=True, required=False)
group_admin = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all()) # A Group object is related to a User object by username
class Meta:
model = Group
fields = ['group_id', 'group_name', 'group_admin', 'members']
def create(self, validated_data): # Overriden so that when a group is created, the group admin is automatically declared as a member.
group = Group.objects.create(**validated_data)
group_admin_data = validated_data.pop('group_admin')
group.members.add(group_admin_data)
return group
def update(self, instance, validated_data):
members_data = validated_data.pop('members') # Comes from the request body, gets the members list
#print('output: ' + str(members_data[0].items()))
add_remove = self.context['add_remove'] # Comes from the View
if members_data is not None:
if add_remove == 'add':
for member in members_data:
instance.members.add(member['username'])
elif add_remove == 'remove':
for member in members_data:
instance.members.remove(member['username'])
return super().update(instance, validated_data)
I'm not able to update the members associated with a group when overriding the update function of the serializer. The serializer is called from the following GroupUpdate view:
views.py
class GroupUpdate(generics.UpdateAPIView):
serializer_class = GroupSerializer
def get_object(self):
queryset = Group.objects.all()
group_id = self.kwargs['group_id']
if group_id is not None:
queryset = queryset.filter(group_id=group_id).first()
return queryset
def get_serializer_context(self): # Passes the URL paramters to the GroupSerializer (serializer doesn't have kwargs).
context = super().get_serializer_context()
context['add_remove'] = self.kwargs['add_remove']
print(self.request.data)
return context
def perform_update(self, serializer):
serializer=GroupSerializer(data=self.request.data, partial=True)
serializer.is_valid(raise_exception=True)
return super().perform_update(serializer)
Within the perform_update function of GroupUpdate, I receive the following: TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use members.set() instead. but I am unsure as to why this error would be raised, considering I was able to associate a user with a group in the create function in pretty much the same way.
This is what a PATCH request would have as the JSON body:
{
"members": [
{
"username": "small_man"
}
]
}
The output of self.request.data is {'members': [{'username': 'small_man'}]}.
You should specify instance of updated object when you create serializer otherwise serializer's save method will call create not update:
def perform_update(self, serializer):
instance = self.get_object()
serializer=GroupSerializer(instance, data=self.request.data, partial=True)
serializer.is_valid(raise_exception=True)
return super().perform_update(serializer)
BTW looks like perform_update is redundant and you can remove it since serializer validation should work without additional modifications.
I'm trying to create an API object, Roster, which has a list of Members as a subobject on it. However, I do not want to update the subobject by partially updating the Roster object -- instead, I want a route for "add member" and "remove member".
Goal:
GET /Roster/{ROSTERID}
response body:
{
id: {roster id},
members: # members sub object is read only
[
{member subobject},
{member subobject},
...
],
}
POST /Roster/{RosterID}/AddMember
{
{member id},
{member id}, ...
}
and then a similar thing for removing a member.
Note: I want to be able to pass a existing member id in. I don't want to create new members here.
What should I be looking for in the docs to be able to add a route to update the member list with a user id, instead of having to pass in the whole user object?
serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'email', 'groups']
class RosterSerializer(serializers.ModelSerializer):
members = serializers.ListField(
child=UserSerializer()
)
class Meta:
model = Roster
fields = ('id', 'name', 'members')
depth = 2
app/models.py
class Members(User):
on_pto = models.BooleanField(default=False)
class Roster(models.Model):
objects = models.Manager()
name = models.CharField(max_length=80, blank=True, default='', unique=True, null='')
members = models.ForeignKey(
Members,
limit_choices_to={'on_pto': False},
blank=True,
null=True,
related_name='members',
on_delete=models.CASCADE
)
views.py
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all().order_by('-id')
serializer_class = GroupSerializer
class RosterViewSet(viewsets.ModelViewSet):
""""""
queryset = Roster.objects.all().order_by('-id')
serializer_class = RosterSerializer
You probably need to change your models to allow multiple members for a roster, either do a many-to-many for roster or put the FK relationship on the user. So you can then add multiple members for a roster.
To do that you can use a custom route like this. Showing below for add_member and then similarly for remove_member, modify to delete from members_set for roster object.
class RosterViewSet(viewsets.ModelViewSet):
queryset = Roster.objects.all().order_by('-id')
serializer_class = RosterSerializer
#action(detail=True, methods=['post'])
def add_member(self, request, pk=None):
errors = []
response = {}
roster = self.get_object()
members_dict = request.data['members']
if not isinstance(members_dict, list):
errors.append("Invalid request format")
else:
for id in members_dict:
try:
member = User.objects.get(pk=id)
roster.members.add(member)
roster.save()
status_code = status.HTTP_200_OK
except Member.DoesNotExist:
errors.append("Member id {} not found".format(id))
if errors:
response['errors'] = errors
status_code = status.HTTP_400_BAD_REQUEST
return response.Response(response, status=status_code)
These are simplified versions of my models (the user model is just an id and name)
class Convo(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='convo_owner')
users = models.ManyToManyField(User, through='Convo_user')
class Convo_user (models.Model):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
class Comments(models.Model):
name = models.CharField(max_length=255)
content = models.TextField(max_length=1024)
convo = models.ForeignKey(Convo, on_delete=models.CASCADE)
This is my view
class ConvoViewSet(viewsets.ModelViewSet):
serializer_class = serializers.ConvoSerializer
def get_queryset(self):
return None
def list(self, request):
curr_user = request.user.id
# Collecting the list of conversations
conversations = models.Conversation.object.filter(ConvoUser__user_id=request.user.id)
#Getting list of conversation id's
conv_ids = list(conversations.values_list('id', flat=True).order_by('id'))
#Getting list of relevant comments
comments = models.Comments.objects.filter(conversation_id__in=conv_ids)
return Response(self.get_serializer(conversations, many=True).data)
And my current serializer
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
# access = AccessSerializer(many=True)
# model = models.Comments
# fields = ('id', 'name', 'content', 'convo_id')
class Meta:
model = models.Convo
fields = ('id', 'owner_id')
The current response I get is of the form
[
{
"id": 1,
"owner_id": 32
}, ...
]
But I would like to add a comments field that shows all the properties of comments into the response, so basically everything in the second queryset (called comments) and I'm not sure how to go about this at all. (I retrieve the comments in the way I do because I'm trying to minimize the calls to the database). Would I need to create a new view for comments, make its own serializer and then somehow combine them into the serializer for the convo?
The way you've set up your models, you can access the comments of each Convo through Django's ORM by using convo_object.comments_set.all(), so you could set up your ConvoSerializer to access that instance's comments, like this:
class ConvoSerializer(serializers.ModelSerializer):
"""A serializer for messaging objects"""
comments_set = CommentSerializer(many=True)
class Meta:
model = models.Convo
fields = ('id', 'owner_id', 'comments_set')
and then you define your CommentSerializer like:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comments
fields = ('id', 'name', 'content')
No data appears because my serializers are using the default database, not sure why but a step forward
EDIT:
Django: Database used for prefetch_related is not the same that the parent query Provided me the correct answer, I was able to choose the database with this method because for some reason inner queries use the default DB
I need to get a FK info in logged User on ModelSerializer to add a new models.
In this case User->Business and Client->Business.
When post client I need to set Business id using the logged user Business.
It's important to say all other models have the same behavior. I'm looking for some generic solution for this problem.
Client Model
class Client(SoftDeletionModel):
object = ClientManager
business = models.ForeignKey(Business, related_name='business_clients', on_delete=models.CASCADE)
company_name = models.CharField(max_length=511, verbose_name=_('Company Name'))
cnpj = models.CharField(max_length=14, verbose_name=_('CNPJ'))
User Model
class User(AbstractUser):
"""User model."""
username = None
email = models.EmailField(_('email address'), unique=True)
business = models.ForeignKey(Business, related_name='business', on_delete=models.CASCADE, null=True)
ClientSerializer
class ClientSerializer(serializers.ModelSerializer):
business = serializers.IntegerField() # here how can I get user.business?
deleted_at = serializers.HiddenField(default=None)
active = serializers.BooleanField(read_only=True)
password = serializers.CharField(write_only=True, required=False, allow_blank=True)
password_contract = Base64PDFFileField()
class Meta:
model = Client
fields = '__all__'
validators = [
UniqueTogetherValidator2(
queryset=Client.objects.all(),
fields=('cnpj', 'business'),
message=_("CNPJ already exists"),
key_field_name='cnpj'
),
UniqueTogetherValidator2(
queryset=Client.objects.all(),
fields=('email', 'business'),
message=_("Email already exists"),
key_field_name='email'
)
]
Access request inside a serializer
Within the serializer you have access to the serializer context that can include the request instance
class ClientSerializer(serializers.ModelSerializer):
...
def create(self, validated_data):
return Client.objects.create(
business=self.context['request'].user.business,
**validated_data
)
Request is only acessible if you pass it when instantiate the serializer
Pass extra arguments to a serializer via save()
It is also possible to pass extra arguments to a serializer during the save() method call
def create(self, request, **kwargs)
serializer = ClientSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(business=request.user.business)
...
Create a mixin to set business
Finally, a more reusable way is create a mixin for views that provides create and/or update actions, then overwrite perform_create() and perform_update() methods
class BusinessMixin:
def perform_create(self, serializer):
serializer.save(business=self.request.user.business)
def perform_update(self, serializer):
serializer.save(business=self.request.user.business)
class ClientViewSet(BusinessMixin, ModelViewSet):
serializer_class = ClientSerializer
queryset = Client.objects.all()
...
ModelViewSet (basicallyCreateModelMixin and UpdateModelMixin) use these methods to call the save() method from serializer when executing its actions (create(), update() and partial_update(), i.e. POST, PUT and PATCH)
Inspired by serializers.CurrentUserDefault() magic I wrote CurrenUserBusinessDefault but set_context with current user business.
class CurrentUserBusinessDefault(object):
def set_context(self, serializer_field):
self.business = serializer_field.context['request'].user.business
def __call__(self):
return self.business
def __repr__(self):
return unicode_to_repr('%s()' % self.__class__.__name__)
So it's accessible like the default method
class ClientSerializer(serializers.ModelSerializer):
business = BusinessSerializer(default=CurrentUserBusinessDefault())
I am currently using restful and serializers to create and update my user.
Somehow I am not able to update some of the fields if the field has to do with OneToOneField / ForeignKey.
in my models.py, my Student is actually connected to the django build in user model which includes the user's email and connected to the school model which has the name of the school
class Student(Model):
user = OneToOneField(settings.AUTH_USER_MODEL, on_delete=CASCADE)
date_of_birth = DateField(blank=True, null=True)
student_name = CharField(max_length=256)
school = ForeignKey(School,
on_delete=CASCADE,
related_name="%(class)ss",
related_query_name="%(class)s",
blank=True,
null=True)
in serializer.py I have
class StudentSerializer(ModelSerializer):
user_email = SerializerMethodField()
school_name = SerializerMethodField()
class Meta:
model = Student
fields = (
'user_email', 'student_name', 'phone', 'school_name')
def get_user_email(self, obj):
return obj.user.email
def get_school_name(self, obj):
return obj.school.school_name
def create(self, validated_data):
return Student.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.user.email = validated_data.get('user_email', instance.user.email)
instance.student_name = validated_data.get('student_name', instance.student_name)
instance.phone = validated_data.get('phone', instance.phone)
instance.school.school_name = validated_data.get('school_name', instance.school.school_name)
instance.save()
return instance
in my view.py update function
class UserViewSet(ViewSet):
queryset = Student.objects.all()
def update(self, request, pk=None):
student = get_object_or_404(self.queryset, pk=pk)
serializer = StudentSerializer(student, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({'status': True})
return Response({'status': False, 'message': serializer.errors})
I am able to use the API view to pass in json and update the student_name and phone but as for the other two, user_email and school_name I am not able to update it. I don't get any error output when I submit the json though.
I realized the two fields that I am not able to update are because they OneToOneField / ForeignKey.
Can someone please give me a hand what I am missing here or what I can do to check?
Thanks in advance
I think your serializer isn't completed... the field of user and school is instance model, you need specific field in your serializer to implement the instance model, eg: with source='...' argument.
and example:
class VoteSerializer(serializers.ModelSerializer):
# by `username`
user = serializers.CharField(
source='user.username',
read_only=True
)
# by `pk/id`
candidate = serializers.IntegerField(
source='candidate.pk',
read_only=True
)
class Meta:
model = Vote
fields = ('user', 'candidate', 'score')
def create(self, validated_data):
return Vote.objects.create(**validated_data)
and in your case, perhaps is like this;
class StudentSerializer(ModelSerializer):
# by `pk/id` from the user
user = serializers.IntegerField(
source='user.pk',
read_only=True
)
school = serializers.IntegerField(
source='school.pk',
read_only=True
)
Since you are using SerializerMethodField which is readonly field (docs) for user_email and school_name so they won't be available in the validated_data.
Have you check the data you are receiving in validated_data
def update(self, instance, validated_data):
print('++'*22, validated_data)
return instance
The nested seriailzer / model / presentation actually helped me get the work done and pretty helpful.
An example is also provided here.
http://www.django-rest-framework.org/api-guide/serializers/#writing-update-methods-for-nested-representations
the above is continued from
http://www.django-rest-framework.org/api-guide/serializers/#writing-create-methods-for-nested-representations which contained how the nested serializer is being setup in the class and meta's fields