Django REST Framework different depth for POST/PUT? - python

I am using Django REST Framework to create an API for my web app. I have a class 'Comment', that has depth=2 set in the Meta class. This works great when GETing the Comments. When I try to send a POST or PUT request though (i.e. create a new Comment) I am told I need to include objects instead of ForeignKey IDs.
Here's my Serializer class:
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
depth = 2
The model:
class Comment(models.Model):
user = models.ForeignKey(User, null=True, blank=True,
related_name='comments')
budget = models.ForeignKey(Budget, related_name='comments')
published = models.BooleanField(default=False)
body = models.TextField()
created = models.DateTimeField(auto_now_add=True)
The view code:
class Comments(generics.ListCreateAPIView):
model = Comment
serializer_class = CommentSerializer
def pre_save(self, obj):
obj.user = self.request.user
And the error that is displayed in the output (JSON) is:
{"user": ["This field is required."], "budget": [{"non_field_errors": ["Invalid data"]}]}
When this raw data is sent:
{"budget": 2, "published": true, "body": "Another comment"}

I know this is a little bit late but I ended up using 2 serializers like so:
class CommentReadSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
depth = 2
class CommentWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
Then used like this:
class Comments(generics.ListCreateAPIView):
model = Comment
serializer_class = CommentReadSerializer
def create(self, request, *args, **kwargs):
serializer = CommentWriteSerializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
serializer = CommentReadSerializer(serializer.object)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

You can set different serializers by overriding the get_serializer_class() function, like so:
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return YourWriteSerializer
else:
return YourReadSerializer
I thought to add this one, since i came here from Googling after a while.

I believe the proper way to define a serializer field that refers to a foreign key relationship is through something like serializers.PrimaryKeyRelatedField. I don't believe that model serializers automatically use this field class without defining it explicitly in the serializer class.
http://www.django-rest-framework.org/api-guide/relations/#primarykeyrelatedfield
I would imagine that a PrimaryKeyRelatedField serializer would correctly handle JSON data submissions like the one you used in your example.

I had the same problem so I Solved making custom generic methods.This is better implementation of above answers
class CustomListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return self.writeSerializers
else:
return self.readSerializers
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
Similarily RUD,
class CustomRetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
"""
Concrete view for retrieving, updating or deleting a model instance.
"""
def get_serializer_class(self):
method = self.request.method
if method == 'PUT' or method == 'POST':
return self.writeSerializers
else:
return self.readSerializers
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) # enter code here
Now I just give writeSerializers and readSerializers values in Views.py
Also to create Read-write Serializers there is an easy way.
class employeeWriteSerializer(serializers.ModelSerializer):
class Meta:
model = employee
fields = ('username','email',..)
class employeeReadSerializer(serializers.ModelSerializer):
class Meta(employeeWriteSerializer.Meta):
depth = 1
It saves time and repetitive work you can also add authentication classes in custom generic Api(Retitve work). Thanks.

Related

make decorator Only the creator of Content can manipulate it

I have created Two decorator to block anyone to Access to specific content
like:
#method_decorator(login_required(login_url='core:login'), name='dispatch')
#method_decorator(allowed_users(allowed_roles=['writer']), name='dispatch')
class BookDeleteView(BSModalDeleteView):
model = Book
template_name = 'book/book_delete.html'
success_message = 'Success: book was deleted.'
success_url = reverse_lazy('core:book_list')
i want to create decorator seems like this
book=Book.objects.get(id=pk)
if request.user==book.writer.profile.user:
If you want to use a decorator:
def writers_only(function):
#wraps(function)
def wrap(request, *args, **kwargs):
if request.user == Book.objects.get(id=kwargs.get('pk')).writer.profile.user:
return function(request, *args, **kwargs)
else:
# Do something else
return wrap
Then add this to your class based view:
# Other decorators
#method_decorator(writers_only, name='dispatch')
class BookDeleteView(BSModalDeleteView):
# Your code
Otherwise you can also override BookDeleteView.get_object (you can create a mixin to reuse this implementation of get_object in various views):
#method_decorator(login_required(login_url='core:login'), name='dispatch')
#method_decorator(allowed_users(allowed_roles=['writer']), name='dispatch')
class BookDeleteView(BSModalDeleteView):
model = Book
template_name = 'book/book_delete.html'
success_message = 'Success: book was deleted.'
success_url = reverse_lazy('core:book_list')
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if obj.writer.profile.user != self.request.user:
raise PermissionDenied() # Or do something else
return obj
You don't need a decorator for that. What you need is queryset defined on your model view that will restrict the query:
class BookDeleteView(BSModalDeleteView):
def get_queryset(self):
return self.model.objects.filter(user=self.request.user)
In case it has to be done for delete only you may also set it dynamically depending on request:
class BookDeleteView(BSModalDeleteView):
def get_queryset(self):
if self.request.method == 'DELETE':
return self.model.objects.filter(user=self.request.user)
return self.model.objects.all()

Get Authenticated user on many=True serializer Viewset

I'm writing a rest api using Django Rest Framework, I have an endpoint to create objects on POST method and this method is overridden in order to allow bulk adding. However, the object is an "intermediate table" between Pacient and Symptoms and in order to create it I need to provide the pacient object or id and the same for the symptom. I get the Symptom id in the request, so that's not an issue, however the pacient is the authenticated user (who's making the request). Now, how do I edit the create method in the serializer in order to do that?
Here's my view:
class PacienteSintomaViewSet(viewsets.ModelViewSet):
serializer_class = SintomaPacienteSerializer
queryset = SintomaPaciente.objects.all()
permission_classes = (IsAuthenticated, )
http_method_names = ['post', 'get']
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = SintomaPacienteSerializer(data=request.data, many=many)
if serializer.is_valid():
sintomas_paciente_lista = [SintomaPaciente(**data) for data in serializer.validated_data]
print(serializer.validated_data)
SintomaPaciente.objects.bulk_create(sintomas_paciente_lista)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
And this is my serializer:
class SintomaPacienteSerializer(serializers.ModelSerializer):
def create(self, validated_data):
sintoma_paciente = SintomaPaciente.objects.create(
sintoma_id=self.validated_data['sintoma_id'],
paciente_id=THIS NEEDS TO BE FILLED,
data=self.validated_data['data'],
intensidade=self.validated_data['intensidade'],
)
return sintoma_paciente
class Meta:
model = SintomaPaciente
fields = ('id', 'sintoma_id', 'paciente_id', 'intensidade', 'data',
'primeiro_dia', 'ativo')
There is two way.
First one, you can pass your user to serializer inside context, and use it in serializer:
in your view:
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = SintomaPacienteSerializer(data=request.data, many=many,context={'user':request.user})
in your serializer you can access this user with self.context['user']
Second way, you don't need to pass user to serializer again. Also If you already override the create method in your View, you don't need to override create method in serializer. I think it is wrong logically. Anyway, you can use your user when create object in view:
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = SintomaPacienteSerializer(data=request.data, many=many)
if serializer.is_valid():
sintomas_paciente_lista = [SintomaPaciente(**data,paciente_id=request.user.id) for data in serializer.validated_data]
print(serializer.validated_data)
....

Django: Create object with ForeignKey from url

I am working on a Django project with two models linked by a ForeignKey. The parent model, Composition, is linked to the child model, NoteObject, by the id of Composition.
in models.py
class Composition(models.Model):
id = models.AutoField(primary_key=True)
...
class NoteObject(models.Model):
id = models.AutoField(primary_key=True)
composition = models.ForeignKey(Composition, on_delete=models.CASCADE)
...
Once a composition is created, the user needs to be able to create NoteObjects that belong to that composition. The notes are created with the following method:
in views.py
class NoteCreateView(CreateView):
model = NoteObject
template_name = 'entry.html'
fields = ['duration', 'pitch', 'accidental', 'octave']
success_url = reverse_lazy('compositions')
def get_context_data(self, **kwargs):
kwargs['notes'] = NoteObject.objects.filter(
composition=self.kwargs['composition'])
return super(NoteCreateView, self).get_context_data(**kwargs)
The get_context_data method is there to display only the notes for the current composition. The current composition comes from the id of the composition that is part of the url where <composition> is the id of the composition.
in urls.py
path('entry/<composition>/', views.NoteCreateView.as_view(), name='entry')
When I save a NoteObject, what do I need to do in order to set the value of the ForeignKey to be the value within <composition>?
in models.py
def save(self, *args, **kwargs):
composition_id = ????????
self.composition_id = composition_id
super(NoteObject, self).save(*args, **kwargs)
How do I get the value of kwarg in the CreateView to be the ForeignKey when the object is created?
I think you can do it by over-riding form_valid method as mentioned in docs here.
class NoteCreateView(CreateView):
model = NoteObject
template_name = 'entry.html'
fields = ['duration', 'pitch', 'accidental', 'octave']
success_url = reverse_lazy('compositions')
def form_valid(self, form):
form.instance.composition = self.kwargs['composition']
return super(NoteCreateView, self).form_valid(form)
In order to make this work, you need to override the dispatch function as well.
def dispatch(self, request, *args, **kwargs):
self.composition = Composition.objects.values_list(
'id').filter(pk=kwargs['composition'])
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.instance.composition_id = self.composition
return super().form_valid(form)
I used this solution and modified it slightly: CreateView Set ForeignKey from Url Parameter.

Django Admin - Filter ManyToManyField with through model

How can I filter a queryset inside the Admin page of an object that has a ManyToManyField relation with a manually defined through model?
Given models.py
class Foo(models.Model):
foo_field1 = models.CharField(max_length=50)
class Main(models.Model):
main_field1 = models.CharField(max_length=50)
m2mfield = models.ManyToManyField(Foo, through="FooBar")
class FooBar(models.Model):
main = models.ForeignKey(Main, on_delete=models.CASCADE)
foo = models.ForeignKey(Foo, on_delete=models.CASCADE)
new_field = models.CharField(max_length=50)
Inside admin.py
class M2MInlineAdmin(admin.TabularInline):
model = Main.m2mfield.through
extra = 1
class MainAdmin(admin.ModelAdmin):
inlines = [M2MInlineAdmin,]
...
def formfield_for_manytomany(self, db_field, request, **kwargs):
print('called formfield_for_manytomany')
return super().formfield_for_manytomany(db_field, request, **kwargs)
def get_field_queryset(self, db, db_field, request):
print('called get_field_queryset')
return super().get_field_queryset(db, db_field, request)
I try to access both of these methods, but none of them are called if I specify a through table. However, they do get called if the ManyToMany relation is simply defined as like this:
class Main(models.Model):
main_field1 = models.CharField(max_length=50)
m2mfield = models.ManyToManyField(Foo)
Is there a method to filter the queryset when a through table is specified (while being able to access the request context)?
EDIT:
The methods are indeed called when the ManyToManyField has a through model specified, only if there are no fieldsets specified inside the modelAdmin class.
How to access these methods when fieldsets are defined?
formfield_for_manytomany method seems to be called only when default form is used. When fieldsets is defined, it is using a different form which is why above method is not getting called.
Since you are using tabular admin for many to many field, you can override get_queryset to filter with field.
class M2MInlineAdmin(admin.TabularInline):
model = Main.fruits.through
extra = 1
def get_queryset(self, request):
qs = super(M2MInlineAdmin, self).get_queryset(request)
qs = qs.filter(some_arg=some_value)
return qs
Alternatively, you can write a custom model form and use it in admin instead of default form.
class MainAdminForm(forms.ModelForm):
class Meta:
model = Main
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# custom setup
class MainAdmin(admin.ModelAdmin):
form = MainAdminForm
You can use the formfield_for_foreignkey() method on the inline class.
class M2MInlineAdmin(admin.TabularInline):
model = Main.m2mfield.through
extra = 1
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.objects.filter(owner=request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)

django update view and passing context

I have a update view:
class GeneralUserUpdateView(UpdateView):
model = GeneralUser
form_class = GeneralUserChangeForm
template_name = "general_user_change.html"
def dispatch(self, *args, **kwargs):
return super(GeneralUserUpdateView, self).dispatch(*args, **kwargs)
def post(self, request, pk, username):
self.pk = pk
self.username = username
self.gnu = GeneralUser.objects.get(pk=self.pk)
#form = self.form_class(request.POST, request.FILES)
return super(GeneralUserUpdateView, self).post(request, pk)
def form_valid(self, form, *args, **kwargs):
self.gnu.username = form.cleaned_data['username']
self.gnu.email = form.cleaned_data['email']
self.gnu.first_name = form.cleaned_data['first_name']
self.gnu.last_name = form.cleaned_data['last_name']
self.gnu.address = form.cleaned_data['address']
self.gnu.save()
return redirect("user_profile", self.pk, self.username)
Here in this view I want to pass a context like:
context['picture'] = GeneralUser.objects.get(pk=self.pk)
I did trying get_context_data but I cant access pk in there..
Am I doing the update right?? How can I pass that context in there??
You shouldn't be overriding post at all. All of that logic should happen in get_context_data.
In fact, none of your overrides are needed. Everything that you do in form_valid will be done already by the standard form save. And overriding dispatch just to call the superclass is pointless.
Your view should look like this only, with no overridden methods at all:
class GeneralUserUpdateView(UpdateView):
model = GeneralUser
form_class = GeneralUserChangeForm
template_name = "general_user_change.html"
context_object_name = 'picture'
(although it seems a little odd that you want to refer to an instance of GeneralUser as "picture").
Edit to redirect to a specific URL, you can define get_success_url:
def get_success_url(self):
return reverse("user_profile", self.kwargs['pk'], self.kwargs['username'])

Categories

Resources