Django: Managing urls like id:slug with duplicated slug - django

Using Django 1.5 I am storing the slug in DB
I configured my urls like follows:
url(r'^(?P<id>[0-9]+):(?P<slug>[-\w]+)$', TracksDetailView.as_view(), name="track-view"),
And in my podel I have:
#models.permalink
def get_absolute_url(self):
return ('track-view', [str(self.id), str(self.slug)])
If 2 slugs are the same I get the following error:
get() returned more than one GPXTrack -- it returned 2! Lookup parameters were {}
I use a generic view (DetailView) to render the page:
class TracksDetailView(DetailView):
model = GPXTrack
context_object_name = 'track'
Any idea on how to avoid this without needing unique slugs?

You are going to have to override your view's get_object method. The one you're inheriting does not take into account that you're using the ID and slug. It assumes you're passing either a pk or a slug - and also assumes they're unique.

The trick was simple but thanks for pointing me in the right direction!
Well the method get_object looks for a kwargs with key 'pk' rather than 'id', therefore I changed my urls to:
url(r'^(?P<pk>[0-9]+):(?P<slug>[-\w]+)$', TracksDetailView.as_view(), name="track-view"),
And everything worked fine!

Related

Getting fields from extra manager methods using django-rest-framework

I have the following custom model manager in Django that is meant to count the number of related comments and add them to the objects query set:
class PublicationManager(models.Manager):
def with_counts(self):
return self.annotate(
count_comments=Coalesce(models.Count('comment'), 0)
)
Adding this manager to the model does not automatically add the extra field in DRF. In my API view, I found a way to retrieve the count_comments field by overriding the get function such as:
class PublicationDetails(generics.RetrieveUpdateAPIView):
queryset = Publication.objects.with_counts()
...
def get(self, request, pk):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset.get(id=pk))
data = {**serializer.data}
data['count_comments'] = queryset.get(id=pk).count_comments
return Response(data)
This works for a single instance, but when I try to apply this to a paginated list view using pagination_class, overriding the get method seems to remove pagination functionality (i.e. I get a list of results instead of the usual page object with previous, next, etc.). This leads me to believe I'm doing something wrong: should I be adding the custom manager's extra field to the serializer instead? I'm not sure how to proceed given that I'm using a model serializer. Should I be using a basic serializer?
Update
As it turns out, I was using the model manager all wrong. I didn't understand the idea of table-level functionality when what I really wanted was row-level functionality to count the number of comments related to a single instance. I am now using a custom get_paginated_response method with Comment.objects.filter(publication=publication).count().
Original answer
I ended up solving this problem by creating a custom pagination class and overriding the get_paginated_response method.
class PaginationPublication(pagination.PageNumberPagination):
def get_paginated_response(self, data):
for item in data:
publication = Publication.objects.with_counts().get(id=item['id'])
item['count_comments'] = publication.count_comments
return super().get_paginated_response(data)
Not sure it's the most efficient solution, but it works!

Django detailview get_queryset and get_object

I am using Django detailview. initially, I used the URL pattern
url(r'^todo/details/(?P<pk>[\d]+)', views.todoDetailView.as_view(), name='detail_todo'),
my view is
class todoDetailView(DetailView):
model = models.todo
It worked fine.
In the second case, my URL is
url(r'^todo/details/(?P<id>[\d]+)', views.todoDetailView.as_view(), name='detail_todo'),
this time, I modified my view to
class todoDetailView(DetailView):
model = models.todo
# context_object_name = 'todo_detail'
def get_object(self, **kwargs):
print(kwargs)
return models.todo.objects.get(id=self.kwargs['id'])
It worked fine, I modified the second case to
class todoDetailView(DetailView):
model = models.todo
# context_object_name = 'todo_detail'
def get_queryset(self):
return models.todo.objects.get(id=self.kwargs['id'])
then I get an error,
Generic detail view todoDetailView must be called with either an object pk or a slug.
I know that there is no proper slug or pk provided. So, initially I added get_object() (it worked) but get_queryset() is not working. What is the difference in their working ??
And also if a user is getting details only based on the slug, I read on StackOverflow that
this can be used
slug_field = 'param_name'
slug_url_kwarg = 'param_name'
link - Generic detail view ProfileView must be called with either an object pk or a slug
Can anyone explain me actual working of get_object() and get_queryset() (also get_slug_field() if possible)
Along with the terms slug_field and slug_url_kwarg
Thanks in advance
get_object returns an object (an instance of your model), while get_queryset returns a QuerySet object mapping to a set of potentially multiple instances of your model. In the case of the DetailView (or in fact any class that inherits from the SingleObjectMixin, the purpose of the get_queryset is to restrict the set of objects from which you'll try to fetch your instance.
If you want to show details of an instance, you have to somehow tell Django how to fetch that instance. By default, as the error message indicates, Django calls the get_object method which looks for a pk or slug parameter in the URL. In your first example, where you had pk in the URL, Django managed to fetch your instance automatically, so everything worked fine. In your second example, you overrode the get_object method and manually used the id passed as parameter to fetch the object, which also worked. In the third example, however, you didn't provide a get_object method, so Django executed the default one. SingleObjectMixin's default get_object method didn't find either a pk or a slug, so it failed.
There are multiple ways to fix it:
1. Use pk in the URL
The simplest one is to simply use the code you provided in your first example. I don't know why you were unsatisfied with that, it is perfectly fine. If you're not happy, please explain why in more detail.
2. Override get_object
This is the second solution you provided. It is overkill because if you properly configured your view with the correct options (as you will see in the following alternatives), Django would take care of fetching the object for you.
3. Provide the pk_url_kwarg option
If you really want to use id in the URL for some reason, you can indicate that in your view by specifying the pk_url_kwarg option:
class todoDetailView(DetailView):
model = models.todo
pk_url_kwarg = 'id'
4. Provide the slug_field and slug_url_kwarg options [DON'T DO THIS]
This is a terrible solution because you are not really using a slug, but an id, but it should in theory work. You will basically "fool" Django into using the id field as if it was a slug. I am only mentioning it because you explicitly asked about these options in your question.
class todoDetailView(DetailView):
model = models.todo
slug_field = 'id'
slug_url_kwarg = 'id'
Regarding your get_queryset method: in your example, it doesn't even get to be executed, but in any case it is broken because it returns an individual object instead of a queryset (that's what objects.get does). My guess is you probably don't need a custom get_queryset method at all. This would be useful for example if you had a complex permission system in which different users can only access a different subset of todo objects, which I assume is not your case. Currently, if you provide this get_queryset method, even if everything else is configured properly, you will get an error. Probably an AttributeError saying that the queryset object has no attribute filter (because it will actually be a todo object and not a QuerySet object as Django expects).
The default get_object for DetailView tries to fetch the object using pk or slug from the URL. The simplest thing for you to do is to use (?P<pk>[\d]+) in the URL pattern.
When you override get_object, you are replacing this default behaviour, so you don't get any errors.
When you override get_queryset, Django first runs your get_queryset method an fetches the queryset. It then tries to fetch the object from that queryset using pk or slug, and you get an error because you are not using either of them.
The slug_field and slug_url_kwarg parameters are both defined in the docs. The slug_fields is the name of the field in the model used to fetch the item, and slug_url_kwarg is the name of the parameter in the URL pattern. In your case, you are fetching the object using the primary key (pk/id), so you shouldn't use either of these options.
For your URL pattern with (?P<id>[\d]+), you could use pk_url_kwarg = 'id'. That would tell Django to fetch the object using id from the URL. However, it's much simpler to use your first URL pattern with (?P<pk>[\d]+), then you don't have to override any of the methods/attributes above.
get_object() majorly uses with the generic views which takes pk or id
like: DetailView, UpdateView, DeleteView
where get_queryset() uses with the ListView where we are expecting more objects
Also, get_object() or self.get_object() uses pk as default lookup field or can use slug Field
a little peek at get_object()
if queryset is None:
queryset = self.get_queryset()
# Next, try looking up by primary key.
pk = self.kwargs.get(self.pk_url_kwarg)
slug = self.kwargs.get(self.slug_url_kwarg)
if pk is not None:
queryset = queryset.filter(pk=pk)
# Next, try looking up by slug.
if slug is not None and (pk is None or self.query_pk_and_slug):
slug_field = self.get_slug_field()
queryset = queryset.filter(**{slug_field: slug})
I can't help you with what the error message explicitly means, but get_queryset is used in listviews by get multiple objects, while get_object is used to get a single object (ie DetailView).
If you have a pk you can use to get an object, you don't need to specify a slug field. Slug field are used to filter out objects when you don't have or can't show the primary key publicly. This gives a better explanation of a slug field.

Use generic UpdateView to always edit a single configuration object

What I Need
I want to have a global configuration for my app and I want to reuse a generic UpdateView.
What I Tried
For this purpose I created a model (example fields):
class Configuration(models.Model):
admin = models.ForeignKey('User', on_delete=models.CASCADE)
hostname = models.CharField(max_length=23)
A generic Updateview:
class ConfigurationView(UpdateView):
model = Configuration
fields = ['admin','hostname']
And urls.py entry
path(
'configuration/',
views.ConfigurationView.as_view(
queryset=Configuration.objects.all().first()
),
name='configuration'
),
As you can see I want the configuration/ path to link to this configuration and always only edit this one object.
Problem
I get the error
AttributeError: 'Configuration' object has no attribute 'all'
Questions
How can I hardcode the object into the path in urls.py so that always the first Configuration object is used for the UpdateView?
Is there a better way to do this? I simply want to have a global configuration object and want it to be editable and displayable with a template of my choice.
You're trying to provide a single object to a class expecting a queryset. The view calls get_queryset which does this;
def get_queryset(self):
"""
Return the `QuerySet` that will be used to look up the object.
This method is called by the default implementation of get_object() and
may not be called if get_object() is overridden.
"""
if self.queryset is None:
if self.model:
return self.model._default_manager.all()
else:
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
return self.queryset.all()
You've provided a queryset so that lands on self.queryset.all() which for your example is calling all() on an instance of your class.
To use the queryset kwarg of as_view() you'd do something like MyView.as_view(queryset=MyModel.objects.filter(enabled=True))
So you need to change the way the view looks for the object;
class ConfigurationView(UpdateView):
def get_object(self):
return Configuration.objects.first()
By default UpdateView does this to get an object; https://ccbv.co.uk/projects/Django/2.0/django.views.generic.edit/UpdateView/
If you're limiting the config to 1 object you will also want to implement a Singleton design. Essentially this is a way to ensure only 1 object can exist. Read more here; https://steelkiwi.com/blog/practical-application-singleton-design-pattern/
There is a really helpful package for singletons called django-solo

ManyToManyField causing Django Model Save Problems

I want to save a Django model instance with a ManyToManyField. When I try to do so with the create() manager, it produces the following error:
Exception Value:'post' is an invalid keyword argument for this function
Here is my model:
class Amenity(models.Model):
post=models.ManyToManyField(Post,blank=True,null=True)
name=models.CharField(max_length=50, choices=AMENITIES)
def __unicode__(self):
return str(self.name)
Here is the relevant part of the view:
if request.POST.get('amenities'):
amens=request.POST['amenities'].split(',')
p=int(post.id)
for a in amens:
Amenity.objects.create(post=p,name=a)
return HttpResponse('success')
I'm trying to save multiple amenities at one time and I'd doing so outside of a modelform because I have design in mind and I didn't want to have to create a custom field in this case.
post.id is returning the correct value here, so that doesn't seem to be the issue.
Thanks!
There are two ways you can solve this:
1) By making another database hit: (which is the safest)
p = Post.objects.get(pk=post.id)
if p:
Amenity.objects.create(post=p, name=a)
else:
...
2) Passing the id to post_id
p = int(post.id)
Amenity.objects.create(post_id=p, name=a)
EDIT:
Ok, got it working on my pc. First of all as it is Many to Many, sounds better to use posts not post as a model field. Well anyway this is how you do it:
post = Post.objects.get(pk=id)
for a in amens:
a = Amenity(name=a)
a.post.add(post) #better if it said a.posts.add(post)
a.save()
You might be able to do it via Amenity.objects.create(posts=[p],name=a) since posts expects a list though I haven't tested it myself - all my ManyToMany use a through since they add additional metadata.
You shouldn't pass post.id to post field, cause post is m2m and django take care of it. Where you set post instance ?

Django queryset custom manager - refresh caching

In my userprofile model, I have a method (turned into a property) that returns a queryset based on another model's custom manager.
Concretely, a user can sell non-perishable and perishable items, and on the Item model level, several custom managers live that contain the logic (and return the querysets) for determining whether an item is perished or not. Within the userprofile, a method lives that returns something similar to:
Item.live_objects.filter(seller=self.user),
where non_perished_objects is one of the said custom managers.
If however an item is added, it is never reflected through these userprofile methods. Only when restarting the server (and the queryset caches being refilled) are the results correct.
Is there a way to force Django to reload the data and drop the cached data?
Thanks in advance!
Update:
class LiveItemsManager(models.Manager):
kwargs = {'perished': False,
'schedule__start_date__lte': datetime.datetime.now(),
'schedule__end_date__gt': datetime.datetime.now()}
def get_query_set(self):
return super(LiveItemsManager, self).get_query_set().filter(**self.kwargs)
class Item(models.Model):
live_objects = LiveItemsManager()
perished = models.BooleanField(default=False)
seller = models.ForeignKey(User)
As you see, there's also a Schedule model, containing a start_date, an end_data and an item_id field.
In the UserProfile model, I have:
def _get_live_items(self):
results = Item.live_objects.filter(seller=self.user)
return results
live_items = property(_get_live_items)
The problem is that when calling the live_items property, the results returned are only the cached results.
(PS: Don't mind the setup of the models; there's a reason why the models are what they are :))
The issue is that the kwargs are evaluated when the Manager is first defined - which is when models.py is first imported. So the values to be used against schedule__start_date and schedule__end_date are calculated then, and will not change. You can fix this by moving the kwargs declaration inside the method:
def get_query_set(self):
kwargs = {'perished': False,
'schedule__start_date__lte': datetime.datetime.now(),
'schedule__end_date__gt': datetime.datetime.now()}
return super(LiveItemsManager, self).get_query_set().filter(**kwargs)
(Putting the definition into __init__() won't help, as it will have the same effect: the definition will be evaluated at instantiation of the manager, rather than definition, but since the manager is instantiated when the model is defined, this is pretty much the same time.)