Get logged user information in serializers Django Rest + Django JWT - python

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())

Related

Django Rest Framework: TypeError - Direct assignment to the forward side of a many-to-many set is prohibited

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.

DRF: how to change the value of the model fields before saving to the database

If I need to change some field values before saving to the database as I think models method clear() is suitable. But I can't call him despite all my efforts.
For example fields email I need set to lowercase and fields nda I need set as null
models.py
class Vendors(models.Model):
nda = models.DateField(blank=True, null=True)
parent = models.OneToOneField('Vendors', models.DO_NOTHING, blank=True, null=True)
def clean(self):
if self.nda == "":
self.nda = None
class VendorContacts(models.Model):
....
vendor = models.ForeignKey('Vendors', related_name='contacts', on_delete=models.CASCADE)
email = models.CharField(max_length=80, blank=True, null=True, unique=True)
def clean(self):
if self.email:
self.email = self.email.lower()
serializer.py
class VendorContactSerializer(serializers.ModelSerializer):
class Meta:
model = VendorContacts
fields = (
...
'email',)
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors.objects.create(**validated_data)
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
views.py
class VendorsCreateView(APIView):
"""Create new vendor instances from form"""
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsSerializer
def post(self, request, *args, **kwargs):
serializer = VendorsSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
else:
return Response(request.data, status=status.HTTP_200_OK)
As I learned from the documentation
Django Rest Framework serializers do not call the Model.clean when
validating model serializers
In dealing with this problem, I found two ways to solve it.
1. using the custom method at serializer. For my case, it looks like
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors.objects.create(**validated_data)
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
def validate(self, attrs):
instance = Vendors(**attrs)
instance.clean()
return attrs
Using full_clean() method. For me, it looks like
class VendorsSerializer(serializers.ModelSerializer):
contacts = VendorContactSerializer(many=True)
class Meta:
model = Vendors
fields = (...
'nda',
'contacts',
)
def create(self, validated_data):
contact_data = validated_data.pop('contacts')
vendor = Vendors(**validated_data)
vendor.full_clean()
vendor.save()
for data in contact_data:
VendorContacts.objects.create(vendor=vendor, **data)
return vendor
But in both cases, the clean() method is not called. I really don't understand what I'm doing wrong.
In my case I had the same problem but with validation feature
I used the way below and it works for me (not excludes the way found above):
class CustomViewClass(APIView):
def post(self, request, format=None):
prepared_data_variable = 'some data in needed format'
serializer = CustomSerializer(data=request.data)
if serializer.is_valid(self):
serializer.validated_data['field_name'] = prepared_data_variable
serializer.save()
return Response(data=serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
This string is key for my solution serializer.validated_data['field_name'] = prepared_data_variable
For DRF you can change your serializer before save as below...
First of all, you should check that serializer is valid or not, and if it is valid then change the required object of the serializer and then save that serializer.
if serializer.is_valid():
serializer.object.user_id = 15 # For example
serializer.save()
UPD!
views.py
class VendorsCreateView(APIView):
"""Create new vendor instances from form"""
permission_classes = (permissions.AllowAny,)
serializer_class = VendorsSerializer
def post(self, request, *args, **kwargs):
data = request.data
if data['nda'] == '':
data['nda'] = None
for contact in data['contacts']:
if contact['email']:
print(contact['email'])
contact['email'] = contact['email'].lower()
serializer = VendorsSerializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
serializer.save()
except ValidationError:
return Response({"errors": (serializer.errors,)},
status=status.HTTP_400_BAD_REQUEST)
To answer your question: just override save() method for your models as written in docs. There you can assign any values to your model instance directly before saving it in database.
Also, you should probably use models.EmailField for your email fields which will get rid of your lower() check.

Django Rest Framework: Updating subobject with new route, not partial update to model

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)

Permission based on field value that user POSTs

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.

how to update OneToOneField / ForeignKey using ViewSet? Django

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

Categories

Resources