How to have multiple lists in Django CBV ListView? - django

In my app, I want to have an index page which will display two lists of objects, where both lists contain the same type of object (i.e, same Model).
In traditional function based views this is easy: I define two variables, assign them to querysets, and pass them into the context to my template where I can easily access them by name.
I'm still new to CBVs and it seems there is a lot of magic, a lot of things that are handled automatically. I understand how I can override the queryset for a ListView (which defaults to all objects for the given Model), but what I don't get is how to supply multiple querysets, so that my ListView can actually display two lists.
My only thought so far is to override self.object_list to be a tuple of two querysets, but that seems like it would make my template code less clear and I'm not even certain it would work.

If you do not want to support pagination in your multiple lists view, I would suggest overwriting the get_context_data and get methods of your view class
def get_context_data(self, **kwargs):
"""
Get the context for this view.
"""
queryset = kwargs.pop('object_list', self.object_list)
queryset2 = kwargs.pop('object_list', self.object_list2)
context = {
'paginator': None,
'page_obj': None,
'is_paginated': False,
'object_list': queryset,
'object_list3': queryset2
}
context.update(kwargs)
return context
def get(self, request, *args, **kwargs):
self.object_list1 = self.get_queryset1()
self.object_list2 = self.get_queryset2()
context = self.get_context_data()
return self.render_to_response(context)
Sorry for the names (1 and 2) but you should place more descriptive whenever I placed names like "get_queryset1"

What is the difference between the items in both lists? If you're talking about a single list (i.e. queryset) which can be split in two by some of their properties, this should simply be done in the template itself.
For example, imagine you have a list of users, and you want to display them by gender, i.e. men in one list and women in the other. In this case simply return a single queryset like normally with the ListView, and then in the template put something like:
<h4>Male Users</h4>
<li>
{% for user in users %}
{% if not user.is_female %}<ul>{{ user.full_name }}</ul>
{% endfor %}
</li>
<h4>Female Users</h4>
<li>
{% for user in users %}
{% if user.is_female %}<ul>{{ user.full_name }}</ul>
{% endfor %}
</li>

Related

How to get annotated attributes in a template from a DetailView?

I'm working on a small e-commerce. For a model 'Product' I keep track of the stock using a Custom Manager and a method called Products.objects.with_stock() that uses annotations (I'm required to do so, there's no way I can add a stock attribute in Product)
In a ListView of all the products, it's pretty straightforward to add the stock, to access it from a template:
# app/views.py
class ProductListView(ListView):
...
def get_stock(self, obj):
return obj.stock
def get_queryset(self):
return super().get_queryset().with_stock()
And then in the template I just call it:
<!-- templates/home.html-->
{% for product in products %}
<p> Stock: {{product.stock}} <p>
{% endfor %}
How do I perform something similar for a DetailView?
Given that a DetailView gets a particular object instance, where or how do I run something similar to what I did in the ListView? Where do I run that query method that affects all objects as a whole, so that then I can access it in the same way from the template?
It couldn't be simpler: just do the same in the DetailView:
# app/views.py
class ProductDetailView(DetailView):
...
def get_stock(self, obj):
return obj.stock
def get_queryset(self):
return super().get_queryset().with_stock()

Django: get data from tables and display together using ListView

I want to display data from 2 tables (and more in the future), but something doesnt work in my code.
my views.py:
**imports**
def home(request):
context = {'users': Person.object.all(),
'emails': Email.object.all()
}
return render(request,'app/home.html',context)
class PersonListView(ListView):
model = Person
template_name = 'app/home.html'
context_object_name = 'users'
and in my home.html
{% extends "app/base.html" %}
{% block content %}
{% for user in users %}
Displaying user attributes works fine
{% endfor %}
Here should be emails
{% for email in emails %}
This displaying doesnt work
{% endfor %}
{% endbock content %}
So, displaying users works without any problem, but cant display anything form emails, but if I do it in shell, everything works well
A ListView [Django-doc] is designed to display only one queryset at a time. If you need to pass extra querysets, you can override the get_context_data(..) method [Django-doc]:
class PersonListView(ListView):
model = Person
template_name = 'app/home.html'
context_object_name = 'users'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(emails=Email.objects.all())
return context
Here we thus pass an extra variable emails to the template rendering engine. Note however that this queryset will not be paginated (or at least not without adding some pagination yourself).
my models.py:
Note that those are views, you need to write these in views.py.

How to add extra context data in LISTVIEW without breaking built-in pagination

Question about pagination issue:
Why if I have for example following LISTVIEW:
class BoatListView(ListView):
model = BoatModel
template_name = "boats.html"
paginate_by = 5
def get_context_data(self, *, object_list=None, **kwargs):
context = ListView.get_context_data(self, object_list=None, **kwargs)
context["boats"] = BoatModel.objects.all()
context["images"] = BoatImage.objects.all()
return context
and I would use “boats” and “images” context in template , for example :
{% for boat in boats %}
some code here
{% endfor %}
...
…
….
{% bootstrap_pagination page_obj %}
paginator will not work at all in this case ( bootstrap one or original Django https://docs.djangoproject.com/en/2.2/topics/pagination/#using-paginator-in-a-view), no difference?
But as soon as I change “boats” and “images” to “object_list” - paginator would begin pagination.
Whats the problem and how in this case could I add extra context in view if I need to do so within ability to use paiginator indeed?
Thank you!
ListView declares an attribute object_list which takes the queryset from get_queryset(). When constructing the context, this attribute is used to define pagination. You can override the behaviour of the pagination in get_context_data itself by changing what is sent as a queryset in self.paginate_queryset(queryset, page_size)(I don't see a reason to do this though).
Take a look at how ListView works here.

Revert objects on site instead of admin using django simple history

I have employed django simple history package on the admin site to be able to track and revert to previous versions of the object of the model. I am designing a web form that allows users to change instances of the model object using model form on django and would like to allow the users to view and revert to previous versions. Also to allow them to see what are the changes compared to the current version.
With the code below I am able to get the list of historical records on my template under histoire.
class CompanyDetailView(LoginRequiredMixin,generic.DetailView):
model = Company
def get_context_data(self, **kwargs):
context = super(CompanyDetailView, self).get_context_data(**kwargs)
company_instance = self.object
context['histoire'] = company_instance.history.all()
return context
In my template,
<p>
Previous versions:
{% for item in histoire %}
<li>
{{ item }} submitted by {{ item.history_user }} {{
item.history_object }}
</li>
{% endfor %}
</p>
But ideally I want item.history_object to be a link that users can view the previous object and be able to revert if desired.
I did something similar by adding HistoricForm to my model forms.
class MyModelForm(HistoricForm, ModelForm):
...
HistoricForm takes and extra history_id kwarg.
If history_id is provided HistoricForm swaps the ModelForm instance with the historic_instance (what your instance looked like at the time of history_id). This way your form will show the historic version of your object.
class HistoricForm(object):
def __init__(self, *args, **kwargs):
self.history_id = kwargs.pop('history_id', None)
instance = kwargs.get('instance')
if instance and self.history_id:
kwargs['instance'] = self.get_historic_instance(instance)
super(HistoricForm, self).__init__(*args, **kwargs)
def get_historic_instance(self, instance):
model = self._meta.model
queryset = getattr(model, 'history').model.objects
historic_instance = queryset.get(**{
model._meta.pk.attname: instance.pk,
'history_id': self.history_id,
}).instance
return historic_instance
If history_id is not provided the ModelForm works as usual.
You can revert by showing the historic instance and save (this way you will post your historic data).

Django filter ModelFormSet field choices... different from limiting the Formset's queryset

I understand that it is possible to override the default queryset 'used' by the modelformset. This just limits the objects for which a form is created.
I also found a Stack Overflow question about filtering ForeignKey choices in a Django ModelForm, but not a ModelForm Set and about limiting available choices in a Django formset, but not a Model FormSet. I have included my version of this code below.
What I want to do is render a ModelFormSet, for a school class ('teachinggroup' or 'theclass' to avoid clashing with the 'class' keyword) with one field limited by a queryset. This is for a teacher's class-editing form, to be able to reassign pupils to a different class, but limited to classes within the same cohort.
My models.py
class YearGroup(models.Model):
intake_year = models.IntegerField(unique=True)
year_group = models.IntegerField(unique=True, default=7)
def __unicode__(self):
return u'%s (%s intake)' % (self.year_group, self.intake_year)
class Meta:
ordering = ['year_group']
class TeachingGroup(models.Model):
year = models.ForeignKey(YearGroup)
teachers = models.ManyToManyField(Teacher)
name = models.CharField(max_length=10)
targetlevel = models.IntegerField()
def __unicode__(self):
return u'Y%s %s' % (self.year.year_group, self.name)
class Meta:
ordering = ['year', 'name']
My views.py
def edit_pupils(request, teachinggroup):
theclass = TeachingGroup.objects.get(name__iexact = teachinggroup)
pupils = theclass.pupil_set.all()
PupilModelFormSet = modelformset_factory(Pupil)
classes_by_year = theclass.year.teachinggroup_set.all()
choices = [t for t in classes_by_year]
# choices = [t.name for t in classes_by_year] #### I also tried this
if request.method == 'POST':
formset = PupilModelFormSet(request.POST,queryset=pupils)
if formset.is_valid():
formset.save()
return redirect(display_class_list, teachinggroup = teachinggroup)
else:
formset = PupilModelFormSet(queryset=pupils)
for form in formset:
for field in form:
if 'Teaching group' == field.label:
field.choices = choices
return render_to_response('reassign_pupils.html', locals())
As you can see, I am limiting the choices to the queryset classes_by_year, which is only classes which belong to the same year group. This queryset comes out correctly, as you can see in the rendered page below, but it doesn't affect the form field at all.
My template
{% for form in formset %}
<tr>
{% for field in form.visible_fields %}
<td> {# Include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
<p><span class="bigtable">{{ field }}</span>
{% if field.errors %}
<p><div class="alert-message error">
{{field.errors|striptags}}</p>
</div>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input type="submit" value="Submit changes"></p>
</form>
{{ choices }} <!-- included for debugging -->
The page renders with all teaching groups (classes) visible in the select widget, but the tag at the bottom of the page renders as: [<TeachingGroup: Y8 82Ma2>, <TeachingGroup: Y8 82Ma3>], accurately showing only the two classes in Year 8.
Note that I've also read through James Bennett's post So you want a dynamic form as recommended by How can I limit the available choices for a foreign key field in a django modelformset?, but that involves modifying the __init__ method in forms.py, and yet the only way I know how to create a ModelFormSet is with modelformset_factory, which doesn't involve defining any classes in forms.py.
Further to help from Luke Sneeringer, here is my new forms.py entry. After reading Why do I get an object is not iterable error? I realised that some of my problems came from giving a tuple to the field.choices method, when it was expecting a dictionary. I used the .queryset approach instead, and it works fine:
class PupilForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PupilForm, self).__init__(*args, **kwargs)
thepupil = self.instance
classes_by_year = thepupil.teaching_group.year.teachinggroup_set.all()
self.fields['teaching_group'].queryset = classes_by_year
class Meta:
model = Pupil
As best as I can tell, you've actually put all the pieces together except one. Here's the final link.
You said you read the dynamic form post, which involves overriding the __init__ method in a forms.Form subclass, which you don't have. But, nothing stops you from having one, and that's where you can override your choices.
Even though modelformset_factory doesn't require an explicit Form class (it constructs one from the model if none is provided), it can take one. Use the form keyword argument:
PupilModelFormset = modelformset_factory(Pupil, form=PupilForm)
Obviously, this requires defining the PupilForm class. I get the impression you already know how to do this, but it should be something like:
from django import forms
class PupilForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PupilForm, self).__init__(*args, **kwargs)
self.fields['teaching_group'].choices = ______ # code to generate choices here
class Meta:
model = Pupil
The last problem you might have is that a modelformset_factory just takes the class, which means that the constructor will be called with no arguments. If you need to send an argument dynamically, the way to do it is to make a metaclass that generates the form class itself, and call that metaclass in your modelformset_factory call.
You can accomplish this by setting field choices of a form in a formset is in the forms init and overwriting the self.fields['field_name'].choices. This worked fine for me but I needed more logic in my view after the formset was initialized. Here is what works for me in Django 1.6.5:
from django.forms.models import modelformset_factory
user_choices = [(1, 'something'), (2, 'something_else')] # some basic choices
PurchaserChoiceFormSet = modelformset_factory(PurchaserChoice, form=PurchaserChoiceForm, extra=5, max_num=5)
my_formset = PurchaserChoiceFormSet(self.request.POST or None, queryset=worksheet_choices)
# and now for the magical for loop and override each desired fields choices
for choice_form in my_formset:
choice_form.fields['model'].choices = user_choices
I wasn't able to find the answer for this but tried it out and it works in Django 1.6.5. I figured it out since formsets and for loops seem to go so well together :)