Django Rest Framework Nested Routes - PK alternatives - django

I am using drf-nested-routers to nest my resources and everything is working well. I would like, however, to use something other than the pk to refer to a parent object.
What I currently have is:
api/movies/4/scenes - generates a list of scenes from movie with pk=4.
What I would like is:
api/movies/ghost-busters/scenes - where the identifier is movie.title instead of movie.pk
Any suggestions?
Thanks

you can use slug for the url you want to make "api/movies/ghost-busters/scenes"
at first you have to make model with slugField eg.
class Blog(models.Model):
qoute = models.CharField(max_length=30)
slug = models.SlugField()
def save(self, *args, **kwargs):
self.slug = slugify(self.qoute)
super(Blog, self).save(*args, **kwargs)
during the saving model it will create a slug by "qoute" and save to the column "slug"
make your urls.py entry
url(r'^api/movies/(?P<slug>[\w-]+)/scenes/$', 'myapp.views.blog_detail', name='blog_detail'),
then for the drf you have set lookup_field in the serializer and view also.
N.B: you can user ModelSerializer or Serializer or HyperlinkSerialzer as you wish..
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = ('quote', 'slug',)
lookup_field = 'slug'
and the views..
class blog_detail(generics.RetrieveUpdateDestroyAPIView):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
lookup_field = 'slug'

Related

Django DRF serializer method field on many to many running 2n queries

I'm using Django 2.2 and Django REST Framework.
I have the following model structure
class MyModel(models.Model):
name = models.ChartField(max_length=200)
class Tag(models.Model):
name = models.ChartField(max_length=50, unique=True)
class MyModelRelation(models.Model):
obj = models.ForeignKey(MyModel, related_name='relation')
user = models.ForeignKey(User)
tags = models.ManyToManyField(Tag)
def tag_list(self):
return self.tags.all().values_list('name', flat=True).distinct()
I want to get the tags list with the MyModel instance and for that, the serializer is
class MyModelSerializer(serializers.ModelSerializer):
tags_list = serializers.SerializerMethodField(read_only=True)
def get_tags_list(self, obj):
return obj.relation.tag_list()
class Meta:
fields = [
'name',
'tags_list'
]
and the view is
class ObjListView(ListAPIView):
serializer_class = MyModelSerializer
def get_queryset(self):
return super().get_queryset().select_related('relation').prefetch_related('relation__tags')
But to get 58 records, it is running almost 109 queries.
The my_app_mymodel`, `my_app_mymodelrelation_tags is repeated multiple times
This is how I suggest you solve the problem. Instead of extracting the name in the DB level, you can do it in the serializer level. It will make things way easier and faster. First, remove the tag_list method from the model class. First add the annotation to your views:
from django.db.models import F
def get_queryset(self):
return super().get_queryset().annotate(tags_list=F('relation__tags')).select_related('relation')
Then in your serializers
class MyModelSerializer(serializers.ModelSerializer):
tags_list = serializers.SlugRelatedField(many=True, slug_field='name', read_only=True)
...

How to get origin model's name in django queryset ?

I'm using django restframework and get a complete queryset at start in every view.
models.py
class MAccount(BasicModel):
account_id = models.CharField(max_length=45, verbose_name='ID', null=False)
...
class Meta:
db_table = 'account'
unique_together = ('account_id', 'medium',)
ordering = ['-updated_time', '-created_time', '-id']
app_label = 'account'
def __str__(self):
return "account"
CustomModelViewSet general viewset that I want to make different work by different model name in dispatch method.
class CustomModelViewSet(viewsets.ModelViewSet):
parser_classes = [JSONParser, ]
pagination_class = Pagination
# permission_classes = [IsAuthenticated, BaseDataPermission]
def dispatch(self, request, *args, **kwargs):
query_model = self.queryset.model
print(str(query_model))
# how can I get model's name as account here ?
VSAccount specified view for actual work
class VSAccount(CustomModelViewSet):
queryset = MAccount.objects.all().filter(active=True)
My question is how can I get the name of MAccount in dispatch method in CustomModelViewSet?
Try this method:
your_queryset.model.__name__
But actually you may look at self.action field in view class. It contains a string with current action name

DRF: values() does not return groupby object

Suppose I have:
# models.py
class Project(models.Model):
project = models.CharField(max_length=200)
subproject = models.CharField(max_length=200)
physical_pct = models.FloatField()
cost = models.FloatField()
# serializers.py
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__'
In my viewset, I want to display a grouped by object by name that will later be annotated. I referred to this example from values.
# views.py
class ProjectsViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
def get_queryset(self):
queryset = Project.objects.values('project')
print(queryset)
return queryset
When I print queryset it displays a list of all project without the other fields in the terminal. However it raises an error:
"Got KeyError when attempting to get a value for field `subproject` on serializer `ProjectSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `dict` instance.\nOriginal exception text was: 'subproject'."
My desired output is a json grouped by the project field.
UPDATE 1:
It will not have an error if I put all fields in the values() arguments i.e.
.values('project', 'subproject', 'physical_pct', 'cost',)
Which now then destroy the purpose of values being grouped by.
Main cause of the problem is for your serializer defination. If you only want to return back project field response make sure you are using appropriate serializer for that.
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = '__all__' // this means you are suppose to pass all model fields
So we need custom serializer for this purpose
class ProjectListSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ('project',)
And also we need to update get_serializer method according to our necessary need
# views.py
class ProjectsViewSet(viewsets.ModelViewSet):
def get_queryset(self):
// If you are doing so you are suppose to have only one 'project' field response
queryset = Project.objects.values('project')
return queryset
def get_serializer_class(self):
if self.action == 'list' or self.action == 'retrive':
return ProjectListSerializer
return ProjectSerializer
Try:
queryset = Project.objects.all().values('project')
You are querying only project field in Project model but trying to serialize all field in Project model. If you want to serialize one field, you don't need the serializer:
class ProjectsViewSet(viewsets.ModelViewSet):
serializer_class = ProjectSerializer
queryset = Project.objects.all()
def list(self, request, *args, **kwargs):
qs = self.get_queryset().values_list('project', flat=True)
return Response(qs)

Django) queryset filtering in view with ManyToMany Relationship

I've been spending hours on queryset handling of ManyToMany field type.
I want to GET objects(model B) which has ManyToMany relationship with another object(model A), by using filter on model A.
views.py
I get my_user_id from urls.py, which is str.
id part works fine, but...
class UserUserId(generics.RetrieveUpdateAPIView):
#permission_classes = (IsOwner,)
queryset = User.objects.all()
serializer_class = UserSerializer #serializer for User model
def get(self, *args, **kwargs):
id = self.kwargs['my_user_id']
return self.queryset.filter(user_id=id).user_schedules.all()
urls.py
path('user/<str:my_user_id>', views.UserUserId.as_view()),
models.py
class User(models.Model):
user_id = models.TextField(blank=True, null=True)
user_schedules = models.ManyToManyField('Schedule',
related_name='%(class)s_id')
class Schedule(models.Model):
sched_id = models.IntegerField(blank=True, null=True)
sched_name = models.TextField(blank=True, null=True)
It gives me following error :
AttributeError: 'QuerySet' object has no attribute 'user_schedules'
I tried to resolve this issue by put [0] at the end of filter(), but It seems wrong and It doesn't work if I have to check multiple User objects.
So how can I GET user_schedules list of specific User whose user_id is my_user_id?
I'm stuck on this for hours, any help would be much appreciated.
You want to get data for an instance, but your code is trying to get it from a queryset.
The following lines should help you out:
class UserUserId(generics.RetrieveUpdateAPIView):
#permission_classes = (IsOwner,)
queryset = User.objects.all()
serializer_class = UserSerializer #serializer for User model
def get(self, *args, **kwargs):
id = self.kwargs['my_user_id']
user = User.objects.get(id=id)
return list(user.user_schedules.all())
First of all, I think you probably want to use a different serializer (something like SchedulerSerializer) since you suggest that you want to serialize, well, Schedules.
You can obtain all Schedules for a User with a given user_id with:
class UserUserId(generics.RetrieveUpdateAPIView):
#permission_classes = (IsOwner,)
queryset = Schedule.objects.all()
serializer_class = UserSerializer #serializer for User model
def get(self, *args, **kwargs):
id = self.kwargs['my_user_id']
return self.queryset.filter(user__user_id=id)
We thus query over the Schedule model, and filter such that we retrieve all Schedules for which there exists a related User with user_id=id.

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)