Limit serializer by user - django

Here is my model serializer:
class FoodImagesSerializerGet(serializers.ModelSerializer):
current_user = serializers.HiddenField(default=serializers.CurrentUserDefault())
food = serializers.SlugRelatedField(
many=False, read_only=False,slug_field='id', queryset=Food.objects.filter(users=current_user)
)
Model:
class FoodImages(models.Model):
food = models.ForeignKey(Food, related_name='food_images', related_query_name='food_image', on_delete=models.CASCADE, null=True)
class Food(models.Model):
users = models.ManyToManyField(
Profile,
related_name='userfoods',
related_query_name='userfood',
)
I want to limit the serializer by users so only the users of the food can post images
.
I want to do that Just with the SlugRelatedField

You can override SlugRelatedField like this:
class CustomSlugRelatedField(SlugRelatedField):
def get_queryset(self):
return self.queryset.filter(users=self.context['request'].user)
class FoodImagesSerializerGet(serializers.ModelSerializer):
food = CustomSlugRelatedField(
many=False, read_only=False,slug_field='id', queryset=Food.objects.all()
)
Finally, you need to pass the request via context in the serializer. This is taken cared by the GenericViews and ModelViewSets automatically, for function based view, you can try like this:
serializer = FoodImagesSerializerGet(Food.objects.first(), context={'request': request})

it's a class attribute, it couldn't access to request context on runtime. You have to access it via init method
class FoodImagesSerializerGet(serializers.ModelSerializer):
current_user = serializers.HiddenField(default=serializers.CurrentUserDefault())
food = serializers.SlugRelatedField(
many=False, read_only=False,slug_field='id', queryset=Food.objects.all()
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['food'].queryset = Food.objects.filter(users=self.context['request'].user)

Related

Use same serializer class but with different action in a view in Django rest framework

I have a modelset view in which different customs functions are defined based on the requirement. I have to write another get function in which I want to use the same serializer class. But the field which I have defined in the serializer class in pkfield but for the get function, I want it as a stringfield rather than pk field. How to achieve that??
Also, I have defined depth=1, which is also not working.
class Class(TimeStampAbstractModel):
teacher = models.ForeignKey(
Teacher,
on_delete=models.CASCADE,
null=True,
related_name="online_class",
)
subject = models.ForeignKey(
Subject,
on_delete=models.SET_NULL,
null= True,
related_name= "online_class",
)
students_in_class = models.ManyToManyField(Student, related_name="online_class")
My view:
class ClassView(viewsets.ModelViewSet):
queryset = Class.objects.all()
serializer_class = ClassSerializer
serializer_action_classes = {
'add_remove_students': AddStudentstoClassSerializer,
'get_all_students_of_a_class': AddStudentstoClassSerializer,
}
def get_serializer_class(self):
"""
returns a serializer class based on the action
that has been defined.
"""
try:
return self.serializer_action_classes[self.action]
except (KeyError, AttributeError):
return super(ClassView, self).get_serializer_class()
def add_remove_students(self, request, *args, **kwargs):
"""
serializer class used is AddStudentstoClassSerializer
"""
def get_all_students_of_a_class(self,request,pk=None):
"""
for this I function too, I want to use the same AddStudentstoClassSerializer class but
there is a problem. The field students_in_class is already defined as pkfield, whereas I
want to use it as a stringfields in the response of this function
""""
My serializer:
class AddStudentstoClassSerializer(serializers.ModelSerializer):
students_in_class = serializers.PrimaryKeyRelatedField(
many=True, queryset=Student.objects.all()
)
class Meta:
model = Class
fields = ["students_in_class"]
depth = 1
def update(self, instance, validated_data):
slug = self.context["slug"]
stu = validated_data.pop("students_in_class")
/................other codes....../
return instance
Here we can see the student_in_class is defined as pkfield which is ok when using the update api, but when I want to use the get api and call get_all_students_of_a_class I want the field to be stringfield or some other field. How to do that? Also depth= 1 is also not working.
Update:
Treid the following but still not working:
def to_representation(self, instance):
rep = super().to_representation(instance)
# rep["students_in_class"] = instance.students_in_class
rep['students_in_class'] = StudentSerializer(instance.students_in_class).data
return rep
class StudentSerializer(serializers.ModelSerializer):
user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Student
fields = ['user', 'college_name', 'address']
what i got in the response is
{
"students_in_class": {}
}
it is empty dict. what should be done!
You can override you to_representation method like this.
class AddStudentstoClassSerializer(serializers.ModelSerializer):
students_in_class = serializers.PrimaryKeyRelatedField(
many=True, queryset=Student.objects.all()
)
class Meta:
model = Class
fields = ["students_in_class"]
def to_representation(self, instance):
data = {
"students_in_class": # Write your logic here
}
return data
def update(self, instance, validated_data):
slug = self.context["slug"]
stu = validated_data.pop("students_in_class")
/................other codes....../
return instance

Auto create related model

I am wondering if it's possible to auto create a related model upon creation of the first model.
This is the models
class Team(models.Model):
name = models.CharField(max_length=55)
class TeamMember(models.Model):
team = models.ForeignKey('Team', on_delete=models.CASCADE, null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False)
So what I want to do is something like this on the 'Team' model
class Team(models.Model):
name = models.CharField(max_length=55)
#on_new.do_this
TeamMember.team = self
TeamMember.user = request.user
TeamMember.save()
I have tried to find any documentation about this. But only found some example about onetoonefields. But nothing about this.
Appreciate any help. Cheers!
I am assuming you are using forms to create team.
There is no direct way to create the TeamMember instance without the current user(via request). request is available in views only(unless you are using special middleware or third party library to access it), so we can send it form and create the user by overriding the save method of the modelform.
So you can try like this:
# Override the model form's save method to create related object
class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(TeamForm, self).__init__(*args, **kwargs)
class Meta:
model = Team
def save(self, **kwargs):
user = self.request.user
instance = super(TeamForm, self).save(**kwargs)
TeamUser.objects.create(team=instance, user=user)
return instance
And use this form in View:
# Update get_form_kwargs methods in create view
class TeamCreateView(CreateView):
form_class = TeamForm
template = 'your_template.html'
def get_form_kwargs(self):
kw = super(TeamCreateView, self).get_form_kwargs()
kw['request'] = self.request
return kw
Update
(from comments)If you have the user FK availble in Team then you can use it to create TeamMember by overriding the save method. Try like this:
class Team(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL)
name = models.CharField(max_length=55)
def save(self, *args, **kwargs): # <-- Override
instance = super(Team, self).save(*args, **kwargs)
TeamMember.objects.create(user=instance.user, team=instance)
return instance

Filtering using viewsets in django rest framework

Please consider these three models:
class Movie(models.Model):
name = models.CharField(max_length=254, unique=True)
language = models.CharField(max_length=14)
synopsis = models.TextField()
class TimeTable(models.Model):
date = models.DateField()
class Show(models.Model):
day = models.ForeignKey(TimeTable)
time = models.TimeField(choices=CHOICE_TIME)
movie = models.ForeignKey(Movie)
class Meta:
unique_together = ('day', 'time')
And each of them has their serializers:
class MovieSerializer(serializers.HyperlinkedModelSerializer):
movie_id = serializers.IntegerField(read_only=True, source="id")
class Meta:
model = Movie
fields = '__all__'
class TimeTableSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = TimeTable
fields = '__all__'
class ShowSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Show
fields = '__all__'
And their routers
router.register(r'movie-list', views.MovieViewSet)
router.register(r'time-table', views.TimeTableViewSet)
router.register(r'show-list', views.ShowViewSet)
Now I would like to get all the TimeTable objects (i.e. date list) by filtering all the Show objects by a specific movie object. This code seems to be the working and getting the list like I want it
m = Movie.objects.get(id=request_id)
TimeTable.objects.filter(show__movie=m).distinct()
But I have no clue how to use this in django rest framework? I tried doing this way (which I am pretty sure its wrong), and I am getting error:
views.py:
class DateListViewSet(viewsets.ModelViewSet, movie_id):
movie = Movie.objects.get(id=movie_id)
queryset = TimeTable.objects.filter(show__movie=movie).distinct()
serializer_class = TimeTableSerializer
urls.py:
router.register(r'date-list/(?P<movie_id>.+)/', views.DateListViewSet)
error:
class DateListViewSet(viewsets.ModelViewSet, movie_id):
NameError: name 'movie_id' is not defined
How can I filter using viewsets in django rest framework? Or if there is any other prefered way than please list it out. Thank you.
ModelViewSet by design assumes that you want to implement a CRUD(create, update, delete)
There is also a ReadOnlyModelViewSet which implements only the GET method to read only endpoints.
For Movie and Show models, a ModelViewSet or ReadOnlyModelViewSet is a good choice whether you want implement CRUD or not.
But a separate ViewSet for a related query of a TimeTable which describes a Movie model's schedule doesn't looks so good.
A better approach would be to put that endpoint to a MovieViewSet directly. DRF provided it by #detail_route and #list_route decorators.
from rest_framework.response import Response
from rest_framework.decorators import detail_route
class MovieViewSet(viewsets.ModelViewset):
queryset = Movie.objects.all()
serializer_class = MovieSerializer
#detail_route()
def date_list(self, request, pk=None):
movie = self.get_object() # retrieve an object by pk provided
schedule = TimeTable.objects.filter(show__movie=movie).distinct()
schedule_json = TimeTableSerializer(schedule, many=True)
return Response(schedule_json.data)
This endpoint will be available by a movie-list/:id/date_list url
Docs about extra routes
Register your route as
router.register(r'date-list', views.DateListViewSet)
now change your viewset as shown below,
class DateListViewSet(viewsets.ModelViewSet):
queryset = TimeTable.objects.all()
serializer_class = TimeTableSerializer
lookup_field = 'movie_id'
def retrieve(self, request, *args, **kwargs):
movie_id = kwargs.get('movie_id', None)
movie = Movie.objects.get(id=movie_id)
self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
return super(DateListViewSet, self).retrieve(request, *args, **kwargs)
Use a retrieve method, which will match any GET requests to endpoint /date-list/<id>/.
Advantage is that you don't have to explicitly handle the serialization and returning response you make ViewSet to do that hard part. We are only updating the queryset to be serialized and rest framework does the rest.
Since ModelViewSet is implemented as,
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
Its implementation includes the following methods (HTTP verb and endpoint on bracket)
list() (GET /date-list/)
create()(POST /date-list/)
retrieve()(GET date-list/<id>/)
update() (PUT /date-list/<id>/)
partial_update() (PATCH, /date-list/<id>/
destroy() (DELETE /date-list/<id>/)
If you want only to implement the retrieve() (GET requests to endpoint date-list/<id>/), you can do this instead of a `ModelViewSet),
from rest_framework import mixins, views
class DateListViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
queryset = TimeTable.objects.all()
serializer_class = TimeTableSerializer
lookup_field = 'movie_id'
def retrieve(self, request, *args, **kwargs):
movie_id = kwargs.get('movie_id', None)
movie = Movie.objects.get(id=movie_id)
self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
return super(DateListViewSet, self).retrieve(request, *args, **kwargs)
The error
class DateListViewSet(viewsets.ModelViewSet, movie_id): NameError: name 'movie_id' is not defined
happens because movie_id is being passed as parent class of DataListViewSet and not as parameter as you imagined
This example in the documentation should be what you are looking for.
Adjust your URL:
url(r'date-list/(?P<movie_id>.+)/', views.DateListView.as_view())
Adjust your Model:
class Show(models.Model):
day = models.ForeignKey(TimeTable, related_name='show')
time = models.TimeField(choices=CHOICE_TIME)
movie = models.ForeignKey(Movie)
class Meta:
unique_together = ('day', 'time')
Your view would look like this:
class DateListView(generics.ListAPIView):
serializer_class = TimeTableSerializer
def get_queryset(self):
movie = Movie.objects.get(id=self.kwargs['movie_id'])
return TimeTable.objects.filter(show__movie=movie).distinct()
Another way to do it would be:
Adjust your URL:
router.register(r'date-list', views.DateListViewSet)
Adjust your Model:
class Show(models.Model):
day = models.ForeignKey(TimeTable, related_name='show')
time = models.TimeField(choices=CHOICE_TIME)
movie = models.ForeignKey(Movie)
class Meta:
unique_together = ('day', 'time')
Your view would look like this:
class DateListViewSet(viewsets.ModelViewSet):
serializer_class = TimeTableSerializer
queryset = TimeTable.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('show__movie_id')
Which will allow you to make requests such as:
http://example.com/api/date-list?show__movie_id=1
See documentation
Ivan Semochkin has the correct answer but the detail decorator is deprecated. It was replaced by the action decorator.
from rest_framework.decorators import action
class MovieViewSet(viewsets.ModelViewset):
#action(detail=True)
def date_list(self, request, pk=None):
movie = self.get_object() # retrieve an object by pk provided
schedule = TimeTable.objects.filter(show__movie=movie).distinct()
schedule_json = TimeTableSerializer(schedule, many=True)
return Response(schedule_json.data)
To improve #all-is-vanity answer, you can explicitly use movie_id as a parameter in the retrieve function since you are overriding the lookup_field class property:
def retrieve(self, request, movie_id=None):
movie = Movie.objects.get(id=movie_id)
self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
return super(DateListViewSet, self).retrieve(request, *args, **kwargs)
You can also call self.get_object() to get the object:
def retrieve(self, request, movie_id=None):
movie = self.get_object()
self.queryset = TimeTable.objects.filter(show__movie=movie).distinct()
return super(DateListViewSet, self).retrieve(request, *args, **kwargs)

Limit the Choices shown from ManyToMany ForeignKey

How do I limit the values returned via the ManyToMany relationship and thus displayed in the <SELECT> field on my form to only show the spots which were created by the currently logged in user?
models.py
class Project(models.Model):
owner = models.ForeignKey(User, editable=False)
...
spots = models.ManyToManyField(to='Spot', blank=True, )
class Spot(models.Model):
owner = models.ForeignKey(User, editable=False)
spot_name = models.CharField(max_length=80, blank=False)
forms.py
from django import forms
from .models import Project, Spot
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
exclude = ('owner', )
class SpotForm(forms.ModelForm):
class Meta:
model = Spot
exclude = ('owner', )
I'm using GenericViews for Update and Create and currently see all of the entries everyone has made into Spots when I'm updating or creating a Project. I want to see only the entries entered by the logged in user. For completeness sake, yes, the project.owner and spot.owner were set to User when they were created.
I've tried def INIT in the forms.py and using limit_choices_to on the manytomany field in the model. Either I did those both wrong or that's not the right way to do it.
thank you!
in your forms.py
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
exclude = ('owner', )
def __init__(self, user_id, *args, **kwargs):
self.fields['spots'] = forms.ModelChoiceField(widget=forms.Select, queryset=Project.objects.filter(owner=user_id))
class SpotForm(forms.ModelForm):
class Meta:
model = Spot
exclude = ('owner', )
def __init__(self, user_id, *args, **kwargs):
self.fields['spot_name'] = forms.ModelChoiceField(widget=forms.Select, queryset=Spot.objects.filter(owner=user_id))
in your views.py
user_id = Project.objects.get(owner=request.user).owner
project_form = ProjectForm(user_id)
spot_form = SpotForm(user_id)
As I mentioned above, Dean's answer was really close, but didn't work for me. Primarily because request is not accessible in the view directly. Maybe it is in older Django versions? I'm on 1.9. Thank you Dean, you got me over the hump!
The gist of what's going on is adding User into the kwargs in the View, passing that to the ModelForm, remove User from the kwargs and use it to filter the Spots before the form is shown.
This is the code that worked for my project:
views.py
class ProjectUpdate(UpdateView):
model = Project
success_url = reverse_lazy('projects-mine')
form_class = ProjectForm
def dispatch(self, *args, **kwargs):
return super(ProjectUpdate, self).dispatch(*args, **kwargs)
def get_form_kwargs(self):
kwargs = super(ProjectUpdate, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
forms.py
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
exclude = ('owner', 'whispir_id')
def __init__(self, *args, **kwargs):
user_id = kwargs.pop('user')
super(ProjectForm, self).__init__(*args, **kwargs)
self.fields['spots'] = forms.ModelMultipleChoiceField(queryset=Spot.objects.filter(owner=user_id))
class SpotForm(forms.ModelForm):
class Meta:
model = Spot
exclude = ('owner', )
def __init__(self, *args, **kwargs):
user_id = kwargs.pop('user')
super(SpotForm, self).__init__(*args, **kwargs)
self.fields['spot_name'] = forms.ModelMultipleChoiceField(queryset=Spot.objects.filter(owner=user_id))

Django How to override a child form in inlineformset_factory

I'm trying to override concept queryset in my child form, to get a custom list concepts based on the area got from request.POST, here is my list of concepts, which i need to filter based on the POST request, this lists is a fk of my child form (InvoiceDetail). is it possible to have these filters?
after doing some test when I pass the initial data as the documentation says initial=['concept'=queryset_as_dict], it always returns all the concepts, but i print the same in the view and its ok the filter, but is not ok when i render in template, so I was reading that I need to use some BaseInlineFormset. so when I test I obtained different errors:
django.core.exceptions.ValidationError: ['ManagementForm data is missing or has been tampered with']
'InvoiceDetailFormFormSet' object has no attribute 'fields'
so here is my code:
models.py
class ConceptDetail(CreateUpdateMixin): # here, is custom list if area='default' only returns 10 rows.
name = models.CharField(max_length=150)
area = models.ForeignKey('procedure.Area')
class Invoice(ClusterableModel, CreateUpdateMixin): # parentForm
invoice = models.SlugField(max_length=15)
class InvoiceDetail(CreateUpdateMixin): # childForm
tax = models.FloatField()
concept = models.ForeignKey(ConceptDetail, null=True, blank=True) # fk to override using custom queryset
invoice = models.ForeignKey('Invoice', null=True, blank=True)
views.py
class CreateInvoiceProcedureView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
template_name = 'invoice/invoice_form.html'
model = Invoice
permission_required = 'invoice.can_check_invoice'
def post(self, request, *args, **kwargs):
self.object = None
form = InvoiceForm(request=request)
# initial initial=[{'tax': 16, }] removed
invoice_detail_form = InvoiceDetailFormSet(request.POST, instance=Invoice,
request=request)
return self.render_to_response(
self.get_context_data(
form=form,
invoice_detail_form=invoice_detail_form
)
)
forms.py
class BaseFormSetInvoice(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
# call first to retrieve kwargs values, when the class is instantiated
self.request = kwargs.pop("request")
super(BaseFormSetInvoice, self).__init__(*args, **kwargs)
self.queryset.concept = ConceptDetail.objects.filter(
Q(area__name=self.request.POST.get('area')) | Q(area__name='default')
)
class InvoiceForm(forms.ModelForm):
class Meta:
model = Invoice
fields = ('invoice',)
class InvoiceDetailForm(forms.ModelForm):
class Meta:
model = InvoiceDetail
fields = ('concept',)
InvoiceDetailFormSet = inlineformset_factory(Invoice, InvoiceDetail,
formset=BaseFormSetInvoice,
form=InvoiceDetailForm,
extra=1)
How can i fix it?, what do i need to read to solve this problem, I tried to debug the process, i didn't find answers.
i try to do this:
def FooForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(FooForm, self).__init__(*args, **kwargs)
self.fields['concept'].queryset = ConceptDetail.objects.filter(area__name='default')
In a inlineformset_factory how can do it?.
After a lot of tests, my solution is override the formset before to rendering, using get_context_data.
def get_context_data(self, **kwargs):
context = super(CreateInvoiceProcedureView, self).get_context_data(**kwargs)
for form in context['invoice_detail_form']:
form.fields['concept'].queryset = ConceptDetail.objects.filter(area__name=self.request.POST.get('area'))
return context