I'm new to Django, and I'm working on my first real (i.e., non-tutorial) project. I have class-based ListViews on four models, and the lists can be filtered in various ways. The user can click on anything in a filtered list to get a DetailView of the item. This is all straightforward and works fine.
I would like to have Previous and Next buttons on the detail pages that allow the user to step through the current filtered set in the default order (which is not date or id). I've found various bits and pieces on StackOverflow and elsewhere that look like parts of a solution, but I haven't been able to figure out how to make them all work together in my views.
Here is slightly simplified code for one of my tables. "Works" are various items (plays, operas, ballets, etc.) that were performed in one of two theaters.
models.py
class Work(models.Model):
title = models.CharField(max_length=100)
sort_title = models.CharField(max_length=100)
genre = models.CharField(max_length=50)
class Meta:
ordering = ['sort_title']
The field sort_title strips off articles at the beginnings of titles (which are in German and French) and deals with accented characters and the like, so that the titles will sort correctly alphabetically. This is the order of the filtered sets, and I want to retain that order for the Previous and Next buttons.
views.py
class WorkList(ListView):
context_object_name = 'works'
model = Work
paginate_by = 50
template_name = 'works.html'
def get_queryset(self):
query = self.request.GET.get('q')
if query is not None:
return Work.objects.filter(Q(title__icontains=query))
else:
return Work.objects.all()
class WorkDetail(DetailView):
context_object_name = 'work'
model = Work
template_name = 'work.html'
At the moment, the user can only filter Works by title, but I may add the possibility of filtering by genre (hence the Q, which I'm already using for other views). I'm using Bootstrap 4, and I would use some version of the following for the buttons on the detail pages:
<ul class="pagination pt-3">
<li class="page-link">Previous</li>
<li class="page-link ml-auto">Next</li>
</ul>
But since I don't know how to make this work yet, I don't know what the URLs will be.
I've tried django-next-previous, which works well in the shell, but I can't figure out how to make it work in my views. Since I want to preserve the filtered queryset from the ListView and use it in the DetailView, I've also experimented with this approach to saving the queryset in the session: https://gist.github.com/bsnux/4672788. But I haven't been able to figure out how to use this to pass the queryset between the two views.
Any help would be welcome!
The queries
Previous page:
Work.objects.filter(sort_title__lt=self.object.sort_title).reverse().values('id')[:1]
Next page:
Work.objects.filter(sort_title__gt=self.object.sort_title).values('id')[:1]
Next page if sort_title is not unique - note the gte rather than gt:
Work.objects.filter(sort_title__gte=self.object.sort_title).exclude(id=self.object.id).values('id')[:1]
Explanation
filter(...): The Work object is ordered by the sort_title field by default. So by asking for a sort_title that is greater than the sort_title of the current object, so we will find the next Work object in the set.
self.object is how we access the current object from a DetailView.
values('id'): Only select the values we need to reverse() the URL to a different WorkDetail. I'm making a presumption that this is the id field, but if it's not, it can be substituted with another field.
[:1]: Basically just adds LIMIT 1 to the SQL query. We only need the next in the set.
This all keeps the queries lightweight.
Note that these queries only work (using __lt with reverse() for previous page and __gt for next page) because the default ordering of the Work model is by sort_title ascending.
Putting it together
Given that your ListView and DetailView will be sharing the same queryset logic, it might make sense to use a mixin, for example:
class WorkQueryMixin:
def get_queryset(self):
query = self.request.GET.get('q')
if query is not None:
return Work.objects.filter(Q(title__icontains=query))
else:
return Work.objects.all()
The query parameter could be returned in a different way (e.g. through the session data).
Getting it into context, for example for the next page:
class WorkDetail(WorkQueryMixin, DetailView):
context_object_name = 'work'
model = Work
template_name = 'work.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
next_id = (
self.get_queryset()
.filter(sort_title__gt=self.object.sort_title)
.values('id')[:1]
)
# There may be no next page
if next_id:
context['next_id'] = next_id[0]['id']
return context
Many thanks to elyas for the great detailed initial answer and for taking the time to coach me through the details of the implementation. I now have functional Previous and Next buttons that retain the current queryset. Here is the code I ended up with, including both Previous and Next buttons, with a genre filter added:
views.py
class WorkQueryMixin:
def get_queryset(self):
query = self.request.GET.get('q')
if query is not None:
return Work.objects.filter(Q(title__icontains=query) |
Q(genre__icontains=query))
else:
return Work.objects.all()
class WorkList(WorkQueryMixin, ListView):
context_object_name = 'works'
model = Work
paginate_by = 50
template_name = 'my_app/works.html'
class WorkDetail(WorkQueryMixin, DetailView):
context_object_name = 'work'
model = Work
template_name = 'my_app/work.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
prev_pk = (
self.get_queryset()
.filter(sort_title__lt=self.object.sort_title)
.reverse().values('pk')[:1]
)
# There may be no next page
if prev_pk:
context['prev_pk'] = prev_pk[0]['pk']
next_pk = (
self.get_queryset()
.filter(sort_title__gt=self.object.sort_title)
.values('pk').values('pk')[:1]
)
# There may be no next page
if next_pk:
context['next_pk'] = next_pk[0]['pk']
return context
In the template for the Works list view, using classes from Bootstrap 4:
<ul>
{% for work in works %}
<a href="{% url 'my_app:work' work.pk %}?{{
request.GET.urlencode }}" class="list-group-item list-group-
item-action">
{% if work.title == "No title" %}
{{work.title}}
{% else %}
<em>{{work.title}}</em>
{% endif %} ({{work.genre}})
</a>
{% empty %}
<p>No works match this search</p>
{% endfor %}
</ul>
And in the template for the Work detail view:
<ul class="pagination pt-3">
{% if prev_pk %}
<li class="page-link"><a href="{% url 'my_app:work' prev_pk %}?{{
request.GET.urlencode }}">Previous</a></li>
{% endif %}
{% if next_pk %}
<li class="page-link ml-auto"><a href="{% url 'my_app:work' next_pk
%}?{{ request.GET.urlencode }}">Next</a></li>
{% endif %}
</ul>
Related
First section of code works fine; it is for reference.
#Basic Model
class MyTestModel(models.Model):
record = models.CharField(max_length=100)
def __str__(self):
return self.record
#Specify verbose_name
class Meta:
verbose_name = 'UniqueNameExample'
verbose_name_plural = verbose_name
#Generic ListView.
class MyTemplateView(ListView):
model = MyTestModel
template_name = 'base.html'
context_object_name = 'model_list'
ordering = ['record']
#Python block in HTML template. So far, so good.
{% for item in model_list %}
{{ item.record }}<br>
#{{ item }} also works
{% endfor %}
I am trying to access the Model's verbose_name ('UniqueNameExample') AND the model_list in the view. I've tried registering a filter, a tag, and simple_tag.
Something like: templatetags/verbose.py
from django import template
register = template.Library()
#register.filter (or #register.tag or #register.simple_tag)
def verbose_name(obj):
#Could be verbose_name(model) or whatever input
return obj._meta.verbose_name
And then after
{% load verbose %}
in my HTML (which also works fine), I'll try something like this:
{{ object|verbose_name }}
And I'll get the error 'str' object has no attribute '_meta'. Error is the same if using a tag:
{% verbose_name object %}
Note: tags apparently worked for earlier versions, but maybe I'm using them incorrectly? Not asking to access the Model field verbose_name for "record," btw -- that's answered adequately on SO.
The one thing I've tried that gets the answer half right is if I set the following under MyTemplateView:
queryset = model._meta.verbose_name
The problem with this is it overrides the model_list, and the only result I'm left with is 'UniqueNameExample' without being able to access the record(s) I've used in the model.
I know private=True for _meta (not sure if that's relevant or worth exploring/possibly breaking), but Django admin displays the verbose_name (if set) in the list of created models, so I don't see why I can't do the same (also had a rough time tracing back exactly how it does it in the source code). Maybe it's not a generic ListView but a MixIn? Function-based?
Large(ish) db with thousands of models, each with unique verbose_name[s]; would very much like to keep it simple.
EDIT: Found a fantastic solution from Dominique Barton # https://blog.confirm.ch/accessing-models-verbose-names-django-templates/
First, create a templatags folder at the app level and populate with an init file. Next, create a template tag file. Something like verbose.py.
from django import template
register = template.Library()
#register.simple_tag
def verbose_name(value):
#Django template filter which returns the verbose name of a model.
#Note: I set my verbose_name the same as the plural, so I only need one tag.
if hasattr(value, 'model'):
value = value.model
return value._meta.verbose_name
Next, the ListView should be modified.
from django.views.generic.list import ListView as DjangoListView
from .models import MyTestModel
class ListView(DjangoListView):
#Enhanced ListView which includes the `model` in the context data,
#so that the template has access to its model class.
#Set normally
model = MyTestModel
template_name = 'base.html'
context_object_name = 'model_list'
ordering = ['record']
def get_context_data(self):
#Adds the model to the context data.
context = super(ListView, self).get_context_data()
context['model'] = self.model
return context
Don't forget to add the path to urls.py:
path('your_extension/', views.ListView.as_view(), name='base')
Lastly, load the tag and iterate through the "records" normally:
{% load verbose %}
<h1> {% verbose_name model%} </h1>
<ul style='list-style:none'>
{% for item in model_list %}
<li>{{ item }}}</a></li>
{% endfor %}
</ul>
Pagination also works as advertised.
I've built a for-loop in my HTML template and its nearly working. The issue I'm having is its listing Matches that are apart of a different tour.
I think the way to fix this is adding a filter to the view that basically says "only pull in the matches to do with this tour" which is what I've tried to do below in the Match.objects.filter() but it isnt working and I'm not sure why.
class CricketCalendar(generic.ListView):
template_name="monthly_view/cricket-monthly-view.html"
context_object_name='cricket_monthly_view'
queryset = CricketMonthlyView.objects.all()
def get_context_data(self, **kwargs):
context = super(CricketCalendar, self).get_context_data(**kwargs)
context['Tour'] = Tour.objects.all()
context['Match'] = Match.objects.filter(tour=self.request.Tour)
return context
I have also tried the following and neither worked:
self.kwargs['pk']
self.kwargs['Tour']
Edit, forgot to add the following:
Monthly View models.py:
class CricketMonthlyView(models.Model):
tour = models.ForeignKey('cricket.Tour', on_delete=models.CASCADE,
related_name='tour_name')
match_id = models.ForeignKey('cricket.Match', on_delete=models.CASCADE)
and the URLs.py:
url(r'^monthly-view/$', monthly_view.CricketCalendar.as_view(), name='cricket-monthly'),
Cricket models.py:
class Tour(models.Model):
name = models.CharField(max_length=200)
tier_level = models.ForeignKey('sports.Tier')
country = CountryField()
class Match(models.Model):
tour = models.ForeignKey('Tour', on_delete=models.CASCADE)
And the HTML Template:
{% for match_info in cricket_monthly_view %}
{% for tour in Tour %}
<ul>
<li>{{tour.name}}</li>
</ul>
{% for match in Match %}
<ul>
<li>{{match.home_team}}</li>
<li>{{match.away_team}}</li>
</ul>
{% endfor %}
{% endfor %}
{% endfor %}
This is a great place for adding a break-point. You pretty much want to know the fields on your context, and on self. Add import pdb; pdb.set_trace() in get_context_data, and you'll be able to see the fields on your objects. Use dir(obj) and obj.keys() in order to see all the fields on something.
Alternatively, if you have access to the tour object in your context variable, in your template you can get its matching Matches with tour.match_set.all
Also, be careful about naming the context variable Tour with a capital T, because that's the name of your model.
I have a class called Features in my models.py. In my html, I am displaying a list on the right that excludes two of these Features, one is the active feature that has been selected, the other is the most recently added since they are the main content of my page. The remaining Features in the list are displayed by date and do show what I am expecting.
Now, I want to single out the first, second and third Features (title only) in THAT list so I can place them in their own separate divs - because each has unique css styling. There are probably numerous ways of doing this, but I can't seem to figure any of them out.
This is a link to my project to give a better idea of what I want (basically trying to get the content in those colored boxes on the right.)
I'm just learning Django (and Python really), so thanks for your patience and help!
HTML
{% for f in past_features %}
{% if f.title != selected_feature.title %}
{% if f.title != latest_feature.title %}
<h1>{{ f.title }}</h1>
{% endif %}
{% endif %}
{% endfor %}
VIEWS
def feature_detail(request, pk):
selected_feature = get_object_or_404(Feature, pk=pk)
latest_feature = Feature.objects.order_by('-id')[0]
past_features = Feature.objects.order_by('-pub_date')
test = Feature.objects.last()
context = {'selected_feature': selected_feature,
'latest_feature': latest_feature,
'past_features': past_features,
'test': test}
return render(request, 'gp/feature_detail.html', context)
MODELS
class Feature(models.Model):
title = models.CharField(db_index=True, max_length=100, default='')
content = models.TextField(default='')
pub_date = models.DateTimeField(db_index=True, default=datetime.now, blank=True)
def __str__(self):
return self.title
def __iter__(self):
return [
self.id,
self.title ]
You can either store the first three Features in separate variables in your context or add checks to your template loop like {% if forloop.first %} or {% if forloop.counter == 2 %}.
If all you want is to not have the
selected_feature
latest_feature
these two records out of the past_features queryset, then you can use exclude on the past_features query and pass the id's of the selected_features and latest_feature objects.
The views.py would look like:
def feature_detail(request, pk):
selected_feature = get_object_or_404(Feature, pk=pk)
latest_feature = Feature.objects.order_by('-id')[0]
# Collect all the id's present in the latest_feature
excluded_ids = [record.pk for record in latest_feature]
excluded_ids.append(selected_feature.pk)
#This would only return the objects excluding the id present in the list
past_features = Feature.objects.order_by('-pub_date').exclude(id__in=excluded_ids)
test = Feature.objects.last()
context = {'selected_feature': selected_feature,
'latest_feature': latest_feature,
'past_features': past_features,
'test': test}
return render(request, 'gp/feature_detail.html', context)
Django provides a rich ORM and well documented, go through the Queryset options for further information.
For access to a specific object in Django templates see following example:
For access to first object you can use {{ students.0 }}
For access to second object you can use {{ students.1 }}
For access to a specific field for example firstname in object 4 you can use {{ students.3.firstname }}
For access to image field in second object you can use {{ students.1.photo.url }}
For access to id in first object you can use {{ students.0.id }}
I am changing a list of field's values upon viewing, but I want to pass the unchanged values to the context. The case here is of a primitive notification system where upon viewing the notification, it should change its status to viewed.
views.py
class Notification(TemplateView):
template_name='myapp/notification.html'
def get_context_data(self, **kwargs):
user = self.request.user
user_unread = user.notification_set.filter(viewed=False)
user_read = user.notification_set.filter(viewed=True)
context = super(Notification, self).get_context_data(**kwargs)
context.update({'user_unread': user_unread, 'user_read': user_read})
for msg in user_unread:
msg.viewed = True
msg.save()
return context
The Problem with this code however, is that I am getting duplicated values in the read and unread lists, even though I have saved the new values to the model after updating the context that is passed to the template.
template:
Unread:
<ul>
{% for msg in user_unread %}
<li> {{ msg }} </li>
{% endfor %}
</ul>
Already read:
<ul>
{% for msg in user_read %}
<li> {{ msg }} </li>
{% endfor %}
</ul>
On a sidenote, I am new to CBVs and if there if my view code above could be improved I'd love some pointers.
This is due to the lazy nature of querysets. A queryset does not actually touch the database until you evaluate it, which usually occurs when you iterate. So in your code, you iterate through user_unread in the view, to set the read status: so the contents of the queryset are fixed at that point. But you don't iterate through user_read until you reach the template, so the query is not made until then, after you have updated all the unread notifications.
The way to fix this is to explicitly evaluate the read queryset in the view, before you update the unread ones. You can do this by simply calling list on it:
context.update({'user_unread': user_unread, 'user_read': list(user_read)})
for msg in user_unread:
...
You can try getting another duplicate query set to update the objects. Use non-updated for template context and update only another one.
Like:
def get_context_data(self, **kwargs):
user = self.request.user
user_unread = user.notification_set.filter(viewed=False)
#Do some operation on qs so that it gets evaluated, like
nitems = len(user_unread)
#get another queryset to update
user_unread_toupdate = user.notification_set.filter(viewed=False)
user_read = user.notification_set.filter(viewed=True)
context = super(Notification, self).get_context_data(**kwargs)
context.update({'user_unread': user_unread, 'user_read': user_read})
#Use 2nd queryset
for msg in user_unread_toupdate:
msg.viewed = True
msg.save()
return context
Django would cache for each queryset differently. So once evaluated user_unread will have its own copy of objects.
Although, its not very elegant/efficient as multiple copies of similar queryset are loaded, so if number of records high, it will be slower.
I'm rather new to Class Based Views, so this is probably obvious, but any tips are appreciated. I want to display "time left" for each item on a list. That is if I have 10 objects, each should display in the template the number of days, hours, mn left until a deadline arrives. Here's my attempt:
model.py
class Law(models.Model):
deadline = models.DateTimeField(_(u'The Deadline'),)
name = ..
more_stuff = ..
views.py
class LawList(ListView):
model = Law
context_object_name = 'law'
template_name = 'template.html'
template.html
{% for l in law %}
<h3>{{ l.deadline }} - {{l.name }} </h3>
{{l.more_stuff}}
{% endfor %}
all good up to here. However I would like to have {{l.time-left}} instead of {{l.deadline}}. Is there a way for the view to calculate this and pass it to the template?
I thought of adding a get_context_data to the 'LawList' view, but I don't know how to do so for every item in my list. Below is what works for a single item.
# views.py, below the section above
def get_context_data(self, **kwargs):
context = super(LawList, self).get_context_data(**kwargs)
context['time_left'] = Law.objects.all()[0].deadline - timezone.now()
but I'm a little stuck. Thanks!
have a look at the timeuntil template tag