Django ListView: taking a time difference in all items - django

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

Related

Next and previous buttons for a queryset in a DetailView

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>

Django Annotations - More efficient way?

In a class Based ListView I want to annotate each Post with the number of comments (this method works)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['approved_comments'] = Comment.objects.values('post_id').annotate(c_count=Count('post_id')).filter(approved=True)
return context
in my Template I do the following (which feels really inefficient)
{% for comment in approved_comments %}
{% if post.id == comment.post_id %}
{{comment.c_count}}
{% endif %}
{% endfor %}
This gets me the result I want but I'm struggling to find a better way to deliver this since this seems really redundant.
I was trying to find a SO question that already deals with this but haven't found one so if you could point me to the link that would be helpful.
Thanks in advance :)
This is a quadratic JOIN, you should not do that. You can simply annotate the Posts with the number of approved comments in the view:
from django.db.models import Count, Q
from django.views.generic import ListView
class MyListView(ListView):
model = Post
queryset = Post.objects.annotate(
napproved_comments=Count('comment', filter=Q(comment__approved=True))
)
and then render this in the template with:
{{ post.napproved_comments }}

Adding extra content to Class Based View and filtering it

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.

Cannot get selection from one page to another - need to know what choice user chose

I'm trying to let the user select one 'thing' from a list (from the database), then go find other stuff in the database using that record. But I cannot get the selection info from the selection page.
I'll try to make this a pretty complete snapshot of the relevant code, but I may remove too much or leave too much in, sorry.
my models.py:
urlpatterns = patterns('',
url(r'^$', 'dblook.views.index', name='home'),
url(r'^dblook3/', 'dblook.views.try3', name='home2'),
url(r'^dblook4/', 'dblook.views.try4', name='home3'),
)
my dblook/models.py:
from django.db import models
class serial_number(models.Model):
def __unicode__(self):
return self.serialno
#return self.question
class Meta:
managed=False
db_table='serial_number'
sn_id = models.AutoField(primary_key=True)
serialno = models.CharField(max_length=128)
comment = models.ForeignKey(comment,null=True,db_column='comment')
my views.py (I will skip all the imports other than the database model import. If anyone really wants them I'll update with them)
from dblook.models import *
class SerialnoSelectForm(forms.Form):
serialno = forms.CharField(max_length=16)
selected = forms.BooleanField()
class serialform(ModelForm):
class Meta:
model = serial_number
exclude=('comment','sn_id')
selected = forms.BooleanField()
class snselect(forms.Form):
sno = forms.ChoiceField()
def try3(request):
if ( request.POST ):
output = "HEllo world, thanks for posting"
return HttpResponse(output)
else:
sslst = snselect(serial_number.objects.filter(serialno__startswith="A128").order_by('-serialno'))
t = loader.get_template('select_serialno.html')
c = Context({
'sslst': sslst,
})
c.update(csrf(request))
return HttpResponse(t.render(c))
def try4(request,dsn):
if ( request.POST ):
output = "HEllo world, thanks for posting to 4"
return HttpResponse(output)
else:
return HttpResponse("Error")
And my template (select_serialno.html) is:
<h1>Select a serial number</h1>
<ul>
<form method='post' action'='/dbtest4/{{serial_number.sn_id}}/showme'>
{% csrf_token %}
{% for sn in sslst %}
<input type="submit" name="sn.serialno" id="choice{{ forloop.counter }}" value="{{choice.id}}"/>
<label for="choice{{ forloop.counter }}">{{ sn.serialno }}</label><br/>
{% endfor %}
<input type="submit" value="data" />
</form>
When I go to dblook3, I get a nice list from the database of serial numbers, along with a button that, if I hit goes immediately to the dblook4 URL (in this case, its ALWAYS '/dbtest4//showme/' instead of something like '/dbtest4/3/showme/). Unfortunately, I cannot seem to have any way to tell what button they hit.
No matter WHAT I put in for the 'stuff' in <form method='post' action'='/dbtest/{{stuff}}/showme'>, it is always empty.
I also tried things like if( 'choice' in request.POST ): in try4 in veiws.py, but that didn't work either.
So, how do I get ANY information about what was selected from 'look3' over to 'look4'? I'll take just about anything... However, if you can explain why I'm doing that hopefully your answer will not only solve my problem, but help others understand...
(if the above looks pretty 'evolutionary' that's because I've been hacking on this for 3 days now...)
Thanks!
You need to POST the information to the look4 dblook form:
<form method='post' action'='{% url dblook.views.try4 %}'>
At the moment you have /dbtest/{{serial_number.sn_id}}/showme which doesn't make any sense. You don't have a serial_number variable in your context so I don't know where that comes from. You have def try4(request,dsn): as your view definition which suggests that you are trying to load information on the try4 view depending on what was selected fromt he try3 view (although I am guessing this as you haven't explained what you are trying to do). If that is the case, you need to do that based on the data passed via POST instead of url parameters. Something very vaguely like the following:
def try4(request):
if request.method == "POST":
form = snselect(request.POST)
if form.is_valid():
data = form.cleaned_data
# Get the selected item from your choice field and retrieve the
# corresonding model object with that id
...

django - 2 views in one template

i have 2 different views which are getting filtered data from db. and i have to use these views in one template file(admin.html) but i cant use multiple views on a page at same time.
here is my view1:
draft_list = Post.objects.filter(isdraft=True).order_by("-posted")
return render_to_response('userside/admin.html',
{'draft_list':draft_list,},
context_instance = RequestContext(request))
view2 :
publish_list = Post.objects.filter(isdraft=False).order_by("-posted")
return render_to_response('userside/admin.html',
{'publish_list':publish_list,},
context_instance = RequestContext(request))
i d like to use them like :
{% for d in draft_list %}
{{ d.title }}
{% endfor %}
--
{% for p in publish_list %}
{{ p.title }}
{% endfor %}
i want to make these 2 views 'one view' .what is the correct way?
You do not want to have 2 views in 1 template (which is not possible anyway), but have 2 models available in 1 template for rendering. Just do it like this:
draft_list = Post.objects.filter(isdraft=True).order_by("-posted")
publish_list = Post.objects.filter(isdraft=False).order_by("-posted")
return render_to_response('userside/admin.html',
{'draft_list':draft_list,'publish_list':publish_list})
From your question, it seems that you're using function based views. An alternative way to solve the problem you're having is to use class based views and override the get_context_data method to pass your template two contexts, one for each queryset.
#views.py
class MyClassBasedView(DetailView):
context_object_name = 'draft_list'
template='my-template'
queryset = Post.objects.filter(isdraft=True).order_by("-posted")
def get_context_data(self, **kwargs):
context = super(MyClassBasedView, self).get_context_data(**kwargs)
context['publish_list'] = Post.objects.filter(isdraft=False).order_by("-posted")
return context