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.
Related
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>
I have a generic ListView which displays all objects from a model and I would like users to be able to choose one object for further processing by storing in session or in another model. What would be the best way to go about this?
views.py
class TranscriptListView(generic.ListView):
model = Transcript
template_name = 'transcript_list.html'
template
{% block content %}
<ul>
{% for transcript in transcript_list %}
<li>
{{transcript.name}}
<p>{{transcript.text}}</p>
</li>
{% endfor %}
</ul>
For selecting something to store in a session, I'd just do a
class SelectTranscriptView(SingleObjectMixin, View):
model = Transcript
def post(self, request, *args, **kwargs):
object = self.get_object()
request.session['selected_transcript'] = object.id
return redirect(...)
This view only accepts POST, since GET requests should be idempotent, i.e. they should not modify any state. Setting a session value is definitely modifying state. This means you'll need to use a form (or a JavaScript function) to POST data to the URL you hook this up to.
More importantly, though: Setting something in the session is not necessarily a good idea at all. Consider an advanced user deciding to open multiple tabs to your site, and trying to select one transcript for one tab, and another in the other tab. This won't be possible if you store the selected thing in the session! Instead, I'd design things so the ID of the object being edited or viewed is always in the URL (as happens with DetailViews and the ilk anyway).
I have the following class based view in which I perform to queryset:
class PatientDetail(LoginRequiredMixin, DetailView):
model = PatientProfile
template_name = 'patient_detail.html'
context_object_name = 'patientdetail'
def get_context_data(self, **kwargs):
context=super(PatientDetail, self).get_context_data(**kwargs)
queryset= RehabilitationSession.objects.filter(patient__slug=self.kwargs['slug'])
context.update({'patient_session_data': queryset,})
return context
When I acces the value of patient_session_data key sent, in my template:
{% extends "base.html" %}
{% block content %}
{{patient_session_data}}
{% endblock content %}
I get this the QuerySet object:
<QuerySet [<RehabilitationSession: Hernando Zambrano Cuellar>, <RehabilitationSession: Hernando Zambrano Cuellar>, <RehabilitationSession: Hernando Zambrano Cuellar>]>
I want access to specific attibute named upper_extremity of my RehabilitationSession model, then I make this:
{% for upperlimb in patient_session_data %}
{{upperlimb.upper_extremity}}
{%endfor%}
And I get this in my template:
Izquierda Izquierda Izquierda
This mean, three times the value, because my queryset return me three objects. This is logic.
For access to value of a separate way I make this in my template:
{{patient_session_data.0.upper_extremity}}
And I get: Izquierda
My goal
I unknown the amount of querysets objects RehabilitationSession that will be returned by the queryset executed in my PatientDetail cbv, because the number is dynamic, may be any amount of objects returned.
I want read the value content of each patient_session_data upper_extremity and accord to the value make something in my template,
But I want read it of a dynamic way,without use {{patient_session_data.<0-1-2-3>.upper_extremity}}
For example in a hypotetical case:
#if all objects returned have same value in upper_extremity
{% if patient_session_data.upper_extremity == 'Izquierda' %}
<div class="col-md-10">
<h5>Segmentos corporales a tratar</h5>
<small>Codo - mano - falange</small>
</div>
{%endif%}
I think that I have count the amount of objects and make with them some actions, because I don't have clear ...
How to can I access of a individual way to the objects returned of a dynamic way without matter the objects number returned?
UPDATE
I did have not taken into account the possibility of that all objects returned have different value in upper_extremity. This mean that I wish that when the queryset objects returned have a mix of values of Izquierda Derecha Izquierda also make something.
In this moment (with the support provided by #2ps when the querysets objects all have upper_extremity='Izquierda' make something and when the querysets objects all have upper_extremity='Derecha' make something.
Is want perform some actions when the values of upper_extremity of the querysets objects may be:
Object queryset 1: upper_extremity='Derecha'
Object queryset 2:upper_extremity='Izquierda'
Object queryset 3:upper_extremity='Derecha'
. ..
and so...
According to the solution of #2ps I think that the core of this assesment is in:
# Perform a set union with the values of 'upper_extremity' attribute for remove the repeated values(Izquierda and Derecha) and distinct them
upper_extremities = set(queryset.values_list('upper_extremity', flat=True).distinct())
# Create a empty dict like data destiny
all_match = dict()
# Iterate on all values possible
for value in ('Izquierda', 'Derecha', ): # add any other values you want here
# Fill the dictionary always that the longitude of upper_extremities (values get) be equal to 1 (?)
all_match[value] = value in upper_extremities and len(upper_extremities) == 1
# Send the result to the template
context['all_match'] = all_match
In my template I think ...
{% if all_match.Izquierda and all_match.Derecha %}
<div class="col-md-10">
<h5>Segmentos corporales a tratar</h5>
<small>Codo - mano - falange</small>
</div>
{%endif%}
How to can I cover this another scenary when the objects querysets have a mix of upper_extremity values (Derecha and Izquierda)
Thanks and excuse me for the new question, I have not clear this process.
Several possible approaches you can take here. For the all izquierda or all derecha, here is what you can do:
class PatientDetail(LoginRequiredMixin, DetailView):
model = PatientProfile
template_name = 'patient_detail.html'
context_object_name = 'patientdetail'
def get_context_data(self, **kwargs):
context=super(PatientDetail, self).get_context_data(**kwargs)
queryset= RehabilitationSession.objects.filter(patient__slug=self.kwargs['slug'])
context['patient_session_data'] = queryset
# get a list of all upper extremities
upper_extremities = set(queryset.values_list('upper_extremity', flat=True).distinct())
all_match = dict()
for value in ('Izquierda', 'Derecha', ): # add any other values you want here
all_match[value] = value in upper_extremities and len(upper_extremities) == 1
context['all_match'] = all_match
return context
And in your template
#if all objects returned have same value in upper_extremity
{% if all_match.Izquierda or all_match.Derecha %}
<div class="col-md-10">
<h5>Segmentos corporales a tratar</h5>
<small>Codo - mano - falange</small>
</div>
{%else%}
{# here we handle all cases with mixed results #}
{%endif%}
I have multiple ListViews for a specific model. I have a page that should show the number of results per ListView and link to that ListView.
How do I count the number of items in that View?
dashboard.html
{% if status == 'Open' %}
{{ jobs.count }}
{% endif %}
dashboard view:
Dashboard(ListView):
model = Job
Open View:
class JobOpenList(ListView):
def get_queryset(self):
return Job.objects.filter(status='Open')
Closed View:
class JobClosedList(ListView):
def get_queryset(self):
return Job.objects.filter(status='Closed')
This is not the way to think about things. You can't ask for the number of things in a completely separate view. Instead, you need to calculate them in the view you are currently using.
You can use aggregation to count the number of items in each state:
Job.objects.values('state').annotate(count=Count('state'))
all_I'm having a hard time having an attribute that I'm adding to a QS to show up in my template.
I have a manager that looks something like this:
def created_by(self, _userProfile, dateGt=None, dateLt=None):
results = self.filter(creator=_userProfile, ...) //Some QS here
for result in results:
result.finished = "It works!"
for result in results:
print result.finished
return results
This prints in my console: (I have 2 events). I did the double for loop as a debug thing to make sure that after exiting the for loop the data was still there.
It works!
It works!
And then I'm calling the manager:
events = Event.manager.created_by(some stuff here).order_by('-id')
And then the weird part: I'm passing the results of the line above to the template and show something like that:
{% for event in all_events %}
<li>{{ event.title }} Finished:{{ event.finished }}</li>
{% empty %}
<em>{% trans "NOTHING" %}</em>
{% endfor %}
And everything shows up (title, id, etc) but not the "finished" part.
Any idea where that could come from ? I trimmed the code for readability, but what I don't think that anything I removed is relevant here.
Thanks!
EDIT:
Here's the view (weird indentation because of stackoverflow 4 spaces thing)
def homepage(request):
variables = {}
if request.user.is_authenticated():
datenow=datetime.now()
me = request.user.get_profile()
variables['all_events'] = Event.manager.created_by(me).order_by('-id') [:20]
context_instance = RequestContext(request)
template_name = "common/homepage.html"
return render_to_response(template_name, variables, context_instance)
I ended up creating a finished() method in the Event model and call directly event.finished in the template.
It works like that, but I still don't know why my first setup did not