Correct way to use DRF and viewsets - django

I have a model:
class Definition(Model):
definition_id = BigAutoField(primary_key=True)
definition_name = CharField(max_length=50)
is_active, created_by, created_datetime, last_modified_by, last_modified_datetime = default_model_attrs()
class Meta:
db_table = 'definitions'
def __str__(self: Self) -> str:
return self.definition_name
With this serializer:
class DefinitionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Definition
fields = [
'url',
'definition_id',
'definition_name',
'attributes',
'created_by',
'created_datetime',
'last_modified_by',
'last_modified_datetime',
'is_active',
]
read_only_fields = [
'url',
'definition_id',
'attributes',
'created_by',
'created_datetime',
'last_modified_by',
'last_modified_datetime',
'is_active',
]
I tried making a view set for a couple of custom actions on create:
class DefinitionViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows definitions to be viewed or edited.
"""
queryset = Definition.objects.all().order_by('definition_id')
serializer_class = DefinitionSerializer
permission_classes = [permissions.IsAuthenticated]
def create(self: Self, request: Request, **kwargs: dict[str, Any]) -> Response:
if not is_valid_db_object_name(request.data['definition_name']):
return Response(
{
'message': f"Invalid definition name -> {request.data['definition_name']}",
},
status=status.HTTP_400_BAD_REQUEST)
definition = Definition(
definition_name=request.data['definition_name'],
created_by=request.user,
last_modified_by=request.user
)
serializer = self.serializer_class(definition,
context={'request': request},
partial=True)
definition.save()
return Response(
data=serializer.data,
status=status.HTTP_201_CREATED
)
But this seems overly complicated for validating a field (is_valid_db_object_name is basically a regex) and to add created_by and last_modified_by. Also, trying to figure out how to make definition_name the only possible field that should be passed for creating and editing. Am I on the right path?
Also, how would the edit method look like if I needed to do the same validation? At this point I just want to make sure I am on track here.
I'm using DRF, because I only need an API, no interface.

The recommended track is to handle most of business logic in serializers, and use the viewsets to map view actions to serializers.
For example, a single field validation can be handled in validate_<field> method in serializer:
class DefinitionSerializer(serializers.HyperlinkedModelSerializer):
# Meta class declarations
def validate_definition_name(self, value):
if value is not <condition>:
raise serializers.ValidationError("Invalid definition name")
return value
# Additional fields can be set in 'create' method
def create(self, validated_data):
instance = Definition(
**validated_data,
created_by=self.context['request']['user'],
last_modified_by=self.context['request']['user']
)
return instance
Whith this setup the view can be simplified to:
def create(self: Self, request: Request, **kwargs: dict[str, Any]) -> Response:
serializer = self.serializer_class(data=request.data,
context={'request': request},
partial=True)
serializer.is_valid()
serializer.save()
return Response(
data=serializer.data,
status=status.HTTP_201_CREATED
)

Related

Configure Django Rest Framework to populate user field with logged in user before serializer validation

I have an API endpoint that supports the POST method which works if I include the user in the request payload.
I would like to eliminate this from the request payload and have DRF just use the logged in user.
I am getting the following error when I omit the user from the request body:
HTTP 400 Bad Request
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"user": [
"This field is required."
]
}
This appears to be coming from when the framework calls serializer.is_valid().
How do I configure DRF to populate the user from request.user so that serializer validation doesn't fail?
models.py
class Task(models.Model):
created = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
domain = models.CharField(max_length=settings.MAX_CHAR_COUNT)
serializers
class TaskSerializer(serializers.HyperlinkedModelSerializer):
user = serializers.SlugRelatedField(queryset=CustomUser.objects.all(), slug_field='email')
class Meta:
model = Task
fields = ['id', 'created', 'domain', 'user']
views.py
class TaskApiViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated,]
serializer_class = TaskSerializer
task_service = TaskService()
"""
GET /api/tasks
"""
def get_queryset(self):
user = self.request.user
if user.is_superuser:
return Task.objects.all()
return Task.objects.filter(user=self.request.user)
"""
POST /api/tasks
"""
def create(self, request):
domain = request.data['domain']
can_create_task = self.task_service.can_create_task(user=request.user, domain_name=domain)
if not can_create_task:
raise PermissionDenied(code=None, detail=None)
return super().create(request)
The correct method of doing this, according to Django Rest Framework, is to override the perform_create() method in your ViewSet, which is responsible of creating an instance of a model using a serializer. You can pass additional data to the serializer:
def perform_create(self, serializer):
serializer.save(user=self.request.user)
The original definition of the perform_create() method can be found here.
You might need to completely override your ViewSet's create() method to be able to pass a modified dict to the serializer. You can just copy the original definition
Afterwards you can add the user to the request data by making a copy, modifying it, and passing it to the serializer:
def create(self, request, *args, **kwargs):
user_id = self.request.user.id
domain = request.data['domain']
new_data = request.data.copy()
new_data.update({"user": user_id})
can_create_task = self.task_service.can_create_task(user=request.user, domain_name=domain)
if not can_create_task:
raise PermissionDenied(code=None, detail=None)
# original definition of create() with new_data
serializer = self.get_serializer(data=new_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)
A simple solution to your problem would be using the "extra_kwargs" field inside your Meta class,for your case it would be something like this:
class Meta:
model = Task
fields = ['id', 'created', 'domain']
extra_kwargs = {'user': {'default': serializers.CurrentUserDefault()}}

Django Rest Framework - How to use "UniqueTogetherValidator" if one of the fields is provided as a URL variable [duplicate]

I want to save a simple model with Django REST Framework. The only requirement is that UserVote.created_by is set automatically within the perform_create() method. This fails with this exception:
{
"created_by": [
"This field is required."
]
}
I guess it is because of the unique_together index.
models.py:
class UserVote(models.Model):
created_by = models.ForeignKey(User, related_name='uservotes')
rating = models.ForeignKey(Rating)
class Meta:
unique_together = ('created_by', 'rating')
serializers.py
class UserVoteSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
created_by = UserSerializer(read_only=True)
class Meta:
model = UserVote
fields = ('id', 'rating', 'created_by')
views.py
class UserVoteViewSet(viewsets.ModelViewSet):
queryset = UserVote.objects.all()
serializer_class = UserVoteSerializer
permission_classes = (IsCreatedByOrReadOnly, )
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)
How can I save my model in DRF without having the user to supply created_by and instead set this field automatically in code?
Thanks in advance!
I had a similar problem and I solved it by explicitly creating and passing a new instance to the serializer. In the UserVoteViewSet you have to substitute perform_create with create:
def create(self, request, *args, **kwargs):
uv = UserVote(created_by=self.request.user)
serializer = self.serializer_class(uv, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
I was able to solve this with one-liner in views.py
def create(self, request, *args, **kwargs):
request.data.update({'created_by': request.user.id})
return super(UserVoteViewSet, self).create(request, *args, **kwargs)
Since this view expects user to be authenticated, don't forget to extend permission_classes for rest_framework.permissions.IsAuthenticated
The other weird way you can do is use signals like this
#receiver(pre_save, sender=UserVote)
def intercept_UserVote(sender, instance, *args, **kwargs):
import inspect
for frame_record in inspect.stack():
if frame_record[3]=='get_response':
request = frame_record[0].f_locals['request']
break
else:
request = None
instance.pre_save(request)
Then basically you can define pre_save in your model
def pre_save(self, request):
# do some other stuff
# Although it shouldn't happen but handle the case if request is None
self.created_by = request.user
The advantage of this system is you can use same bit of code for every model. If you need to change anything just change in pre_save(). You can add more stuff as well
Add the following to the ViewSet:
def perform_create(self, serializer):
serializer.save(user=self.request.user)
And the following on the Serializer:
class Meta:
extra_kwargs = {
'user': {
'required': False,
},
}
Below code worked for me.
Even I was facing same error after many experiments found something, so added all fields in serializer.py in class meta, as shown below -
class Emp_UniSerializer( serializers.ModelSerializer ):
class Meta:
model = table
fields = '__all__' # To fetch For All Fields
extra_kwargs = {'std_code': {'required': False},'uni_code': {'required': False},'last_name': {'required': False},'first_name': {'required': False}}
Here, we can update any field which are in "extra_kwargs", it wont show error ["This field is required."]

Why DRF is skipping validation?

I have a Validator like this: (Yes it's empty and raises Expception)
class AlphanumericValidator(object):
def __init__(self):
pass
def __call__(self, value):
raise serializers.ValidationError(
'Value should contain only letters, numbers, - and _ characters.'
)
And the serializer:
class CampaignSerializer(CreatorModelSerializer):
name = serializers.CharField(max_length=32, validators=[
AlphanumericValidator()
])
class Meta:
model = Campaign
fields = ('id', 'name')
validators = [
UniqueTogetherValidator(
queryset=Campaign.objects.all(),
fields=['account', 'name'],
message='A campaign with this name is already exists.',
),
]
My Payload:
id: 5
name: "aaq???asdas"
My View:
class CampaignDetailView(APIView):
permission_classes = (IsOwner, )
def get_object(self, request, campaign_pk):
campaign = get_object_or_404(Campaign, pk=campaign_pk)
self.check_object_permissions(request, campaign)
return campaign
def put(self, request, campaign_pk):
campaign = self.get_object(request, campaign_pk)
serializer = CampaignSerializer(
campaign,
data=request.data,
context={
'request': request,
}
)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(
serializer.data,
)
Permission IsOwner:
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return request.user.profile.account == obj.account
Serializer is validating the name successfully.
Where is my mistake?
Edit: I forgot to add end of the CampaignDetailSerializer.put method. I added them.
My wildest guess, is the way you are using the custom validator is meant to be used along with ModelViewSet, to trigger these validators.
If you are doing something custom, try overriding the validate method on the ModelSerializer
like the following
# I guess that CreatorModelSerializer is a sub-class of ModelSerializer.
class CampaignSerializer(CreatorModelSerializer):
def validate(self, attrs):
# Do Validations
if True:
raise serializers.ValidationError(
'ABBBBALIIIZA'
)
# Otherwise return the attributes
return attrs
class Meta:
model = Campaign
fields = ('id', 'name')
validators = [
UniqueTogetherValidator(
queryset=Campaign.objects.all(),
fields=['account', 'name'],
message='A campaign with this name is already exists.',
),
]
The validation should work properly after this change.

Update data using RetrieveUpdateAPIView - Getting validated data from a serializer

I would like to update certain properties of a user (say first_name and last_name)
my json object through a PUT request would look like this
{
"user" : {
"first_name": "Jack",
"last_name": "shnider",
"password":"admin123"
"email" : "foo#google.com"
},
"employee_zip" : 12345
}
This is what my view looks like (I would like to update the existing fields to these new fields).
These are the serializer
class Serializer_UpdateUser(ModelSerializer):
class Meta:
model = User
fields = ('first_name','last_name','password')
class Serializer_UpdateEmployer(ModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
This is the view :
class UpdateProfile_RetrieveUpdateAPIView(RetrieveUpdateAPIView):
queryset = modelEmployer.objects.all()
serializer_class = Serializer_UpdateEmployer
lookup_field = 'user__email'
permission_classes = [permissions.AllowAny]
def update(self, request, *args, **kwargs):
instance = self.get_object() #------>I have the object that I would like to update
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True) #--->Success
Now I would like to get a validated fields (The json only contains the fields that have been updated). I know if I do something like this
serializer.save
I would get back a modelEmployer but instead I get back this error
AssertionError at /api/employer/update_profile/employerA#gmail.com/ The `.update()` method does not support writable nested fields by default. Write an explicit `.update()` method for serializer `Employer.api.serializers.Serializer_ListEmployer`, or set `read_only=True` on nested serializer fields. Request Method:
I have two questions
1-Why is save failing ?
2-How can I get the validated data from the above serializer ?
The save is failing because django-rest-framework doesn't deal with nested serializers by default.
from the django-rest-framework docs:
By default nested serializers are
read-only. If you want to support write-operations to a nested
serializer field you'll need to create create() and/or update()
methods in order to explicitly specify how the child relationships
should be saved.
You have to override the update method in the serializer to allow that behavior:
class Serializer_UpdateEmployer(ModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
def update(self, instance, validated_data):
user_data = validated_data.pop('user', {})
user_serializer = Serializer_UpdateUser(instance.user, data=user_data)
user_serializer.save()
return instance
Another solution is to use drf-writable-nested. It automatically makes your nested serializers updatable.
from drf_writable_nested import WritableNestedModelSerializer
class Serializer_UpdateEmployer(WritableNestedModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
I think drf-writable-nested can help you to update nested data.
In you case:
from django.contrib.auth import password_validation
class Serializer_UpdateUser(ModelSerializer):
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
super(Serializer_UpdateUser, self).update(instance, validated_data)
if password is not None:
instance.set_password(password)
instance.save()
return instance
def validate_password(self, value):
password_validation.validate_password(value)
return value
class Meta:
model = User
fields = ('first_name','last_name','password')
class Serializer_UpdateEmployer(WritableNestedModelSerializer):
user = Serializer_UpdateUser()
class Meta:
model = modelEmployer
fields = [
'user',
'employer_zip',
]
Note you need special handling password field.

Django Rest Framework return nested object using PrimaryKeyRelatedField

I am using DRF to expose some API endpoints.
# models.py
class Project(models.Model):
...
assigned_to = models.ManyToManyField(
User, default=None, blank=True, null=True
)
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
assigned_to = serializers.PrimaryKeyRelatedField(
queryset=User.objects.all(), required=False, many=True)
class Meta:
model = Project
fields = ('id', 'title', 'created_by', 'assigned_to')
# view.py
class ProjectList(generics.ListCreateAPIView):
mode = Project
serializer_class = ProjectSerializer
filter_fields = ('title',)
def post(self, request, format=None):
# get a list of user.id of assigned_to users
assigned_to = [x.get('id') for x in request.DATA.get('assigned_to')]
# create a new project serilaizer
serializer = ProjectSerializer(data={
"title": request.DATA.get('title'),
"created_by": request.user.pk,
"assigned_to": assigned_to,
})
if serializer.is_valid():
serializer.save()
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data, status=status.HTTP_201_CREATED)
This all works fine, and I can POST a list of ids for the assigned to field. However, to make this function I had to use PrimaryKeyRelatedField instead of RelatedField. This means that when I do a GET then I only receive the primary keys of the user in the assigned_to field. Is there some way to maintain the current behavior for POST but return the serialized User details for the assigned_to field?
I recently solved this with a subclassed PrimaryKeyRelatedField() which uses the id for input to set the value, but returns a nested value using serializers. Now this may not be 100% what was requested here. The POST, PUT, and PATCH responses will also include the nested representation whereas the question does specify that POST behave exactly as it does with a PrimaryKeyRelatedField.
https://gist.github.com/jmichalicek/f841110a9aa6dbb6f781
class PrimaryKeyInObjectOutRelatedField(PrimaryKeyRelatedField):
"""
Django Rest Framework RelatedField which takes the primary key as input to allow setting relations,
but takes an optional `output_serializer_class` parameter, which if specified, will be used to
serialize the data in responses.
Usage:
class MyModelSerializer(serializers.ModelSerializer):
related_model = PrimaryKeyInObjectOutRelatedField(
queryset=MyOtherModel.objects.all(), output_serializer_class=MyOtherModelSerializer)
class Meta:
model = MyModel
fields = ('related_model', 'id', 'foo', 'bar')
"""
def __init__(self, **kwargs):
self._output_serializer_class = kwargs.pop('output_serializer_class', None)
super(PrimaryKeyInObjectOutRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return not bool(self._output_serializer_class)
def to_representation(self, obj):
if self._output_serializer_class:
data = self._output_serializer_class(obj).data
else:
data = super(PrimaryKeyInObjectOutRelatedField, self).to_representation(obj)
return data
You'll need to use a different serializer for POST and GET in that case.
Take a look into overriding the get_serializer_class() method on the view, and switching the serializer that's returned depending on self.request.method.