I have a simple m2m relationship as below:
class Category(ModelBase):
name = models.CharField(max_length=255)
icon = models.CharField(max_length=50)
class Course(ModelBase):
name = models.CharField(max_length=255, unique=True)
categories = models.ManyToManyField(Category, related_name="courses")
I am using ListView to show all the courses in a category or all courses if no category provided.
views.py
class CourseListView(ListView):
model = Course
paginate_by = 15
template_name = "courses.html"
context_object_name = "courses"
def get_queryset(self):
queryset = (
super()
.get_queryset()
.select_related("tutor")
.prefetch_related("categories")
.filter(active=True)
)
category_id = self.kwargs.get("category_id")
return (
queryset
if not category_id
else queryset.filter(categories__in=[category_id])
)
def get_context_data(self, *args, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs)
category_id = self.kwargs.get("category_id")
if category_id:
context["current_category"] = Category.objects.get(id=category_id)
context["categories"] = Category.objects.all()
return context
Django is making duplicate calls as I am doing something like this in the template.
<div class="icon"><span class="{{ course.categories.first.icon }}"></span></div>
Not sure why, help much appreciated. Thanks!
When you do .prefetch_related('categories') the result of this prefetch will be used when you access course.categories.all. Any other queryset on course.categories will do a fresh query. Since course.categories.first is a new queryset, it does not use the prefetched result.
What you want to access in your template is the first result from course.categories.all(). But this is not easy in the template. I would recommend a method on the Course model:
class Course(...):
...
def first_category(self):
# Equivalent to self.categories.first(), but uses the prefetched categories
categories = self.categories.all()
if len(categories):
return categories[0]
else:
return None
And then in your template you can call this method
<div class="icon"><span class="{{ course.first_category.icon }}"></span></div>
You can also access the first value like:
{{ course.categories.all.0.icon }}
It is not necessary to write a method.
because categories is ManyToMany which means one category may appear in many courses, but in the template you just calling the first category's icon, so there maybe more than two course with the same first category, and it will retrieve them all, i recommend using another for loop to loops through categories too.
{% for course in courses %}
<div>
<h1>{{ course.name</h1>
......
<h4>categories</h4>
{% for category in course.categories %}
<div class="icon"><span class="{{ category.icon }}"></span></div>
{% endfor %}
</div>
{% endfor %}
Related
EDIT:
As per schillingt's answer below I have switched to using Case/When:
context['db_orders'] = Order.objects.filter(
retailer_code=self.object.retailer_code).annotate(
in_db=Case(When(Q(Subquery(self.object.suppliers.filter(
supplier_code=(OuterRef('supplier_code')))
), then=Value(True), default=Value(False), output_field=NullBooleanField()))))
However I'm now struggling with an errror:
FieldError at /retailers/A001/
Cannot resolve expression type, unknown output_field
Original question:
I have the DetailView below with a query/subquery that checks whether supplier_code exists within instances of a second model set up to represent purchase orders received into a live stock database.
The intention is for this to function as a checklist/ticksheet that will return whether or not the order has been received for each supplier expected to send one.
Getting it to return that there is a match seems to be working fine, and if there is a value it does not recognize (I have purposefully created an invalid order that won't match against the list) it will return that there is no match.
However I need this to also tell me that there is simply no data, yet I don't seem to be able to achieve this.
For example the below shows the template output; G001 is the 'fake' code I have set up and G002 is a valid one that exists in the suppliers list. However if there is not an order present for G002 it will not return anything.
Order not received: G001
Order received: G002
I have tried writing a second query for the context that is a mirror of context['db_orders'] but using the ~Exists() and then nesting the if statements in the template but this will just tell me that the orders both exist and don't exist or vice versa.
context['not_db_orders'] = Order.objects.filter(
retailer_code=self.object.retailer_code).annotate(in_db=~Exists(squery))
I've also tried to do this in the template using 'is not' or 'is None' or 'is False' but cannot seem to get the output I need
Ultimately the intended output is a table that lists all the suppliers expected into a particular retailer with some manner of 'Yes' or 'No' next to them based on whether the order exists among the Order instances. (The template HTML doesn't currently reflect this but that is not the issue)
Template:
{% extends 'tick_sheet/base.html' %}
{% block content %}
<h1>{{ object.retailer_name }}</h1>
<ul>
{% for supplier in object.get_supplier_values %}
<li>{{ supplier }}</li>
{% endfor %}
</ul>
<ul>
{% for item in db_orders %}
{% if item.in_db %}
<li>Order received: {{ item.supplier_code }} - {{ item.supplier_name }}</li>
{% elif not item.in_db or item.in_db is None %}
<li>Order not received: {{ item.supplier_code }} - {{item.supplier_name}}</li>
{% endif %}
{% endfor %}
</ul>
{% endblock content %}
The DetailView:
class RetailerDetailView(DetailView):
model = Retailer
slug_field = 'retailer_code'
slug_url_kwarg = 'retailer_code'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['now'] = timezone.now()
context['title'] = 'Order Checklist'
squery = self.object.suppliers.filter(
supplier_code=OuterRef('supplier_code'))
context['db_orders'] = Order.objects.filter(
retailer_code=self.object.retailer_code).annotate(in_db=Exists(squery))
return context
Models.py
from django.db import models
from django.utils import timezone
class Order(models.Model):
''' To simulate connection to main stock db '''
retailer_code = models.CharField(max_length=4)
retailer_name = models.CharField(max_length=100)
supplier_code = models.CharField(max_length=4)
supplier_name = models.CharField(max_length=100)
order_reference = models.CharField(max_length=20)
despatch_date = models.DateTimeField(default=timezone.now)
def __str__(self):
return f"< {self.order_reference}', {self.supplier_name}, {self.retailer_name} >"
# -------------------------------------------------------------------------------------
class Retailer(models.Model):
retailer_code = models.CharField(max_length=4)
retailer_name = models.CharField(max_length=100)
suppliers = models.ManyToManyField('Supplier')
slug = models.SlugField(unique=True, null=True)
def get_supplier_values(self):
return [(suppliers.supplier_code + ' - ' + suppliers.supplier_name) for suppliers in self.suppliers.all()]
def save(self, *args, **kwargs):
self.slug = self.slug or slugify(self.retailer_code)
super().save(*args, **kwargs)
def __str__(self):
return f"< {self.retailer_code} - {self.retailer_name} >"
class Supplier(models.Model):
supplier_code = models.CharField(max_length=4)
supplier_name = models.CharField(max_length=100)
def __str__(self):
return f"< {self.supplier_code}, {self.supplier_name} >"
If there's a difference in the case between False and None you can't use Exists. That is a strictly boolean operation. You will need to use a Subquery that returns a NullableBooleanField whose result is calculated with When and Case
I am writing a generic template that I can use across all my models that require a ListView.
To do this, I know I can simply create a generic table in my template with a for loop over the object_list, but as each model is different I can't capture all the fields this way.
Instead I have created a (abstract) method that each model inherits, which produces a list of fields, names and values:
class MyModel(models.Model):
def get_display_fields(self, exclude_fields=[], adminonly_fields=[]):
"""Returns a list of all field names on the instance."""
fields = []
for f in self._meta.fields:
fname = f.name
# resolve picklists/choices, with get_xyz_display() function
get_choice = 'get_' + fname + '_display'
if hasattr(self, get_choice):
value = getattr(self, get_choice)()
else:
try:
value = getattr(self, fname)
except AttributeError:
value = None
if f.editable and f.name not in (exclude_fields or adminonly_fields):
fields.append(
{
'label': f.verbose_name,
'name': f.name,
'help_text': f.help_text,
'value': value,
}
)
return fields
I can then use this in my template which works universally across any model:
{% for obj in object_list %}
{% for f in obj.get_display_fields %}
<p>{{f.label}}</p>
<p>{{f.name}}</p>
<p>{{f.value}}</p>
{% endfor %}
{% endfor %}
Where I am stuck, is I want to allow some customisation of the exclude_fields and adminonly_fields in the view (which is on the model method). For example:
class MyGenericView(ListView):
exclude_fields = ['field1', 'field2']
adminonly_fields = ['field3',]
How can I pass these lists to get_display_fields?. I know I can just write them into the model method, but that defeats the point of this DRY approach. Can I append it to/modify the queryset somehow?
I don't want to use editable=False as I want to allow each view that subclasses MyGenericView to provide excluded_fields as an option.
Create a custom template tag that takes an argument. You will need to use the {% load %} tag to make it available.
It's important that you use a simple tag so that you can pass multiple arguments from your view.
from django import template
register = template.Library()
#register.simple_tag
def get_display_fields(obj, adminonly_fields=[], excluded_fields=[]):
if hasattr(obj, 'get_display_fields')
return obj.get_display_fields(adminonly_fields, excluded_fields)
return []
Pass adminonly_fields and excluded_fields as extra context data in your view so it can be used with your template tag.
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['adminonly_fields'] = self.adminonly_fields
context['excluded_fields'] = self.excluded_fields
return context
Then in your template.
{% for obj in object_list %}
{% get_display_fields obj adminonly_fields excluded_fields as display_fields %}
{% for f in display_fields %}
<p>{{f.label}}</p>
<p>{{f.name}}</p>
<p>{{f.value}}</p>
{% endfor %}
{% endfor %}
In models:
class Match(models.Model):
hot_league = models.ManyToManyField(HotLeague, blank=True)
class HotLeague(models.Model):
user = models.ManyToManyField(User, blank=True)
price_pool = models.IntegerField()
winner = models.IntegerField()
In Views:
match = get_object_or_404(Match, pk=pk)
Here i need to access this Match queryset.
that's why
In template:
{% for hot_league in match.hot_league.all %}
By writing match.hot_league.all in template I can get all queryset of HotLeague class. But I want to use filter here with user. Like in views we can use HotLeague.objects.filter(user=request.user). But {% for hot_league in match.hot_league.filter(user=request.user) %} is not working on template.
How can I do that kind of filter in template?
How can I do that kind of filter in template?
Templates are deliberately restricted to avoid that. Some template processors, like Jinja can make function calls, but usually if you have to do that, something is wrong with the design. Views should determine what to render, and templates should render that content in a nice format.
In your view, you thus can render this as:
def some_view(request, pk):
match = get_object_or_404(Match, pk=pk)
hot_leagues = match.hot_league.filter(user=request.user)
return render(
request,
'some_template.html',
{'match': match, 'hot_leagues': hot_leagues}
)
In your template, you can then render this like:
{% for hot_league in hot_leagues %}
<!-- -->
{% endfor %}
Models:
class Instructional_Cycle(models.Model):
date_started = models.DateField()
date_finished = models.DateField()
standard_tested = models.OneToOneField(Standard, on_delete=models.CASCADE)
class Standard(models.Model):
subject = models.CharField(max_length=14, choices=subjects)
grade_level = models.IntegerField(choices=gradeLevels)
descriptor = models.CharField(max_length=15)
description = models.TextField()
essential_status = models.BooleanField(default=False)
View:
class CycleCreateView(CreateView):
model = Instructional_Cycle
template_name = 'cycle_new.html'
fields = '__all__'
success_url = reverse_lazy('student_progress:cycles')
Template:
<!-- student_progress/cycle_new.html -->
{% extends 'base.html' %}
{% block content %}
<h1>Add a new instructional cycle:</h1>
<form action="{% url 'student_progress:cycle_new' %}" method="post">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">add cycle</button>
</form>
{% endblock content %}
The problem I'm having with this form is that the dropdown to select Instructional_Cycle.standard_tested has literally 1000 records from Standard. There's no way that the user can scroll through all of those and find the one record they want.
What I need is some way to click a link and filter the dropdown list by subject or grade_level and/or a search box, similar to what's achieved on the admin side by creating a custom admin model in admin.py like so:
class StandardAdmin(admin.ModelAdmin):
list_display = ('descriptor', 'description', 'essential_status')
list_filter = ('subject', 'grade_level', 'essential_status')
search_fields = ('descriptor',)
inlines = [MilestoneInLine]
def get_search_results(self, request, queryset, search_term):
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
try:
search_term_as_int = int(search_term)
except ValueError:
pass
else:
queryset |= self.model.objects.filter(age=search_term_as_int)
return queryset, use_distinct
Please "dumb it down" for this newbie. I just finished working through Django for Beginners, and my conceptual model of how this all fits together is still full of holes. Please assume that I know hardly anything. Thanks!
That amount of reactive work on one page will require you to be comfortable with Javascript, Ajax, etc. If that is the case, there are a number of approaches you could take that let you refresh the form with the desired options.
Alternatively, you could ask the user for the necessary data one step earlier in the process and let Django build the correct form for you in the first place by overriding the form's default queryset.
You should look into using something like django-ajax-select. https://github.com/crucialfelix/django-ajax-selects
I am using django-extra-views in order to have sortable tables in my Django ListViews.
I'm not 100% sure of why I can't get it working, but I've always found working from tests.py difficult wrt templates.
So I have this in my views.py
class PartTypePartList(SortableListMixin, generic.ListView):
model = PartNumber
template_name = 'inventory/newparttype_list.html'
sort_fields = ['name',]
paginate_by = 25
def get_queryset(self):
self.parttype = self.kwargs['parttype']
return PartNumber.objects.filter(fds_part_type=self.parttype)
def get_context_data(self, **kwargs):
context = super(PartTypePartList, self).get_context_data(**kwargs)
context['parttype'] = self.parttype
return context
And in urls.py
url(r'^newparttype/(?P<parttype>\d{2})/$', views.PartTypePartList.as_view(), name='new_part_type_view'),
And with these two we are getting the list as expected.
In the relevant template:
Name
asc name
desc name
{% if sort_helper.is_sorted_by_name %} ordered by name {{ sort_helper.is_sorted_by_name }} {% endif %}
The issue is that there is no sorting happening. In particular,
{{ sort_helper.get_sort_query_by_name }} and
{{ sort_helper.get_sort_query_by_name_asc }} and
{{ sort_helper.get_sort_query_by_name_desc }}
each return an empty string.
What am I doing wrong?
I was using django-tables2 but the owner admitted he would not be continuing dev on it and I'm not skilled enough or time rich enough to take it on myself.
[EDIT]
I believe this still deserves a solution, but I've re-written the view to be a FBV rather than a CBV and am manipulating the data accordingly
[/EDIT]
You need to call get_queryset parent method:
def get_queryset(self):
self.parttype = self.kwargs['parttype']
qs = super(PartTypePartList, self).get_queryset()
qs = qs.filter(fds_part_type=self.parttype)
return qs