Counting all items linked to foreign key - django

I'm looking for a solution to combine in one view information from two models.
One model is for "node" definition. Second model is "drive" definition, where I have foreign key for "node". One "node" can contain many drives.
In a template I need to show all assigned drives for a given node in a list view. I'm interested in count, a number of drives assigned to given node.
I don't have any new idea how calculate this information and pass to template. Should I count this as a "pure python"? I believe there is a way in Django as this doesn't look very complex.
View:
class NodeListView(ListView):
template_name = 'nodes/nodes_list.html'
queryset = Node.objects.all()
context_object_name = 'nodes'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
drives = Drive.objects.all()
context.update({
'drives': drives
})
return context
Models:
class Node(models.Model):
name = models.CharField(max_length=32)
.... more ....
class Drive(models.Model):
node = models.ForeignKey(Node, related_name='nodes', on_delete=models.CASCADE)
.... more ....

You can annotate the Nodes with the number of related Drives:
from django.db.models import Count
class NodeListView(ListView):
template_name = 'nodes/nodes_list.html'
queryset = Node.objects.annotate(ndrives=Count('nodes'))
context_object_name = 'nodes'
then you can render this with:
{% for node in nodes %}
{{ node.ndrives }}
{% endfor %}
You can also enumerate over the related Drives with:
{% for node in nodes %}
{{ node.ndrives }}
{% for drive in node.nodes.all %}
{{ drive }}
{% endfor %}
{% endfor %}
where you can boost efficiency with:
from django.db.models import Count
class NodeListView(ListView):
template_name = 'nodes/nodes_list.html'
queryset = Node.objects.annotate(
ndrives=Count('nodes')
).prefetch_related('nodes')
context_object_name = 'nodes'
The reason that this is named nodes is because of the related_name='nodes'. This however does not make much sense: related_name='drives' would make more sense, since this deals with a collection of Drives, you thus might want to rename this to:
class Node(models.Model):
name = models.CharField(max_length=32)
# …
class Drive(models.Model):
node = models.ForeignKey(
Node,
related_name='drives',
on_delete=models.CASCADE
)
# …
Then the view thus looks like:
from django.db.models import Count
class NodeListView(ListView):
template_name = 'nodes/nodes_list.html'
queryset = Node.objects.annotate(
ndrives=Count('drives')
).prefetch_related('drives')
context_object_name = 'nodes'
and we render this with:
{% for node in nodes %}
{{ node.ndrives }}
{% for drive in node.drives.all %}
{{ drive }}
{% endfor %}
{% endfor %}

Related

How to perform addition (+) of attributes from multiple model instances connected via one ManytoManyField in Django?

Problem Statement: I want to add up the Activity.models attribute net_cost from within Trip.models i.e. connected via ManytoManyField. I have 3 or more choices per instance, so add: from the template language is inadequate (https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#add)
More Context: Activity is connected to Trip via ManytoManyField. Accessing and adding up via save method in models.py is causing id needs to be assigned first error, I believe since ManytoMany can only be assigned once an instance of the model is saved.
Even if I access them all in the views.py before rendering, the context passed even after iterating over each object in list, can only repeat a common context["grand_total"] for all entries rendered in the template, whereas I need grandtotal for each Trip instance in frontend List.view.
Models:
Activity ->
class Activity(models.Model):
activity_location = [
...
]
acitivity_duration = [
...
]
activity_title = models.CharField(max_length=20, unique=True)
...
net_cost = models.PositiveIntegerField(validators=[MaxValueValidator(100000), MinValueValidator(0)], default=0)
class Meta:
ordering = ['-margin']
def __repr__(self):
return f"{self.activity_title} - {self.activity_location} - {self.net_cost}"
def __str__(self):
return f"Activity {self.activity_title}, Location {self.activity_location}, Duration {self.acitivity_duration}, Cost {self.net_cost}"
def get_absolute_url(self):
return reverse('activitys-list')
Trip ->
class Trip(models.Model):
transfer_choices = [
...
]
transfers = models.CharField(max_length=11, choices=transfer_choices, blank=True, null=True)
activity = models.ManyToManyField(Activity, related_name="activities", blank=True, verbose_name='Activities', help_text='select multiple, note location tags')
....
class Meta:
ordering = ['-entry_created']
def __repr__(self):
return f'{self.customer.name} for {self.duration} day/s'
def __str__(self):
return f"{self.customer.name} - {self.duration} - {self.start_date} - {self.end_date}"
def save(self, *args, **kwargs):
...
#successfully processing all inhenrent model attributes for grand total, but
unable to process activity.net_cost for all entries.
super(Trip, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('trip-lists')
View:
class TripLists(LoginRequiredMixin, ListView):
login_url = '/login/'
redirect_field_name = 'index'
model = Trip
template_name = 'gobasic/trip_list.html'
paginate_by = 10
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in extra QuerySets here
Trips = Trip.objects.all()
activity_total = 0
for i in Trips:
for a in i.activity:
activity_total += a.net_cost
# Is pointless as same gets repeated.
context['activity_total'] = activity_total
context['total_trips'] = Trip.objects.all().count()
return context
Template:
<h2>Trip List - {{total_trips}}</h2>
<p><button type="button" class="btn btn-info">Add Trip</button>
<a href="{% url 'index' %}"> <button type="button" class="btn btn-info">Home</button>
</a></p>
<ol>
{% for t in object_list %}
<li><strong>🆔:</strong> {{ t.customer.name}}<strong> PB:</strong> {{ t.hotel_pb.hotel_name }} <strong>HV:</strong> {{ t.hotel_hv.hotel_name }} <strong>NL:</strong> {{ t.hotel_nl.hotel_name }} <!--<strong>Activities:</strong> {% for activity in t.activity.all %} {{ activity.activity_title}}, {% endfor %}--> <strong>Start:</strong>{{t.start_date.date}} <strong>End:</strong>{{t.end_date.date}} <strong>🏨:</strong>{{t.hotel_cost}} 🚕 {{t.transfer_cost}} | 🤿 {% for act in t.activity.all %} {{ act.net_cost}} {% endfor %}<strong> Grand Total= {{t.hotel_cost |add:t.transfer_cost}}</strong> <button class="btn">🖋️</button> </li>
<br>
{% empty %}
<p>No Entries Detected Yet !</p>
{% endfor %}
</ol>
How do I get a grand total i.e. t.hotel_cost + t.transfer_cost + t.activity.net_cost (for all objects within activity i.e. ManytoMany inside object_list )

Django many-to-many making too many calls

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 %}

DRY approaches for displaying fields in a ListView with custom [exclude]

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 %}

How to overide django modelform to achieve custom behaviour

I have an Item object that has a manytomany relation to another object Option. I create a modelform from the Item object like so;
class Item(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=9, decimal_places=2, blank=True, null=True)
options = models.ManyToManyField(Option)
class OptionForm(ModelForm):
options = forms.ChoiceField(widget=forms.RadioSelect())
class Meta:
model = Item
fields = ( 'options', )
When i render the form in the template it renders all available options for the Item object(the expected behavior) even those not created by a specific item. I want to be able to load options defined by the specific Item that will be chosen by the user. How do i override the form to achieve such behavior.for example without a form i can render an Items own Options through its id. item = Item.objects.get(pk=id)
Its tough to make ModelForm's defined on the fly because they are intimately tied to the structure in your model. Nevertheless you could use some clever template control flow and view rendering to get your desired effect. This is untested so you millage with this might vary.
<form method="post" action="">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
<ul>
{% if user_option.category %}
<li>{{ form.caregory }}</li>
{% endif %}
{% if user_option.name %}
<li>{{ form.name }}</li>
{% endif %}
{% if user_option.p_opt %}
<li>{{ form.price }}</li>
<li>{{ form.options }}</li>
{% endif %}
</ul>
{% endfor %}
</form>
From the Djano docs here.
Try overriding the form's init method and passing in the Item pk as an additional argument. The trick here is to pop the argument before calling the parent init.
class ItemOptionsForm(forms.ModelForm):
class Meta:
model = Item
def __init__(self, *args, **kwargs):
# pop 'item_id' as parent's init is not expecting it
item_id = kwargs.pop('item_id', None)
# now it's safe to call the parent init
super(ItemOptionsForm, self).__init__(*args, **kwargs)
# Limit options to only the item's options
if item_id:
try:
item = Item.objects.get(id=item_id)
except:
raise ValidationError('No item found!')
self.fields['options'] = forms.ChoiceField(item.options)
Then, in your view, create the form like:
form = ItemOptionsForm(item_id=item_id)
The nice thing about this is that you can raise ValidationErrors that will show up in the form.
Be aware that this doesn't prevent someone from POSTing option IDs to your form which do not belong to the Item, so you'll likely want to override the ModelForm.clean() to validate the options as well.
learning from link django: How to limit field choices in formset? provided by #jingo, i solved the problem by first of all creating dynamic form like so;
def partial_order_item_form(item):
"""dynamic form limiting optional_items to their items"""
class PartialOrderItemform(forms.Form):
quantity = forms.IntegerField(widget=forms.TextInput(attrs={'size':'2', 'class':'quantity','maxlength':'5'}))
option = forms.ModelChoiceField(queryset=OptionalItems.objects.filter(item=item),widget= forms.RadioSelect())
return PartialOrderItemform
then validating form like so;
def show_item(request,id):
option = get_object_or_404(Item,pk=id)
if request.method == 'POST':
form = partial_order_item_form(option)
#bound form to POST data,
final_form = form(request.POST)
# check validation of posted data
if final_form.is_valid():
order.add_to_order(request)
url =urlresolvers.reverse('order_index',kwargs={'id':a.id})
# redirect to order page
return HttpResponseRedirect(url)
else:
form = partial_order_item_form(item=id)
context={
'form':form,
}
return render_to_response('item.html',context,context_instance=RequestContext(request))

Referencing a dynamic number of fields in a template in django

It is all very simple. I have this form:
class add_basketForm(forms.Form):
def __init__(self, selected_subunits, *args, **kwargs):
self.selected_subunits = selected_subunits
super(add_basketForm, self).__init__(*args, **kwargs)
for subunit in self.selected_subunits:
self.fields['su%d' % (subunit['unit__id'])] = forms.IntegerField()
The number of subunits are unknown. I would like to use something like this (you get the idea):
{% for unit in selected_subunits %}
{{ form.su%s }} % (unit.unit__id)
{% endfor %}
But of course that doesn't work. My question is how do I reference those formfields in Django template language?
In order to access the BoundField instances for your dynamic field instances, which is what gives you access to all of the attributes and methods necessary to render the field, you need to access the field objects using the form of form.fieldname rather than form.fields[fieldname]
Here's a potential refactoring of your form class:
class add_basketForm(forms.Form):
def __init__(self, selected_subunits, *args, **kwargs):
super(add_basketForm, self).__init__(*args, **kwargs)
for subunit in self.selected_subunits:
self.fields['su%d' % (subunit['unit__id'])] = forms.IntegerField()
def su_fields(self):
for name in self.fields:
if name.startswith('su'):
yield(self[name])
Then in your template, you should be able to iterate over the fields as you would normally expect by accessing form.su_fields:
{% for su_field in form.su_fields %}
....
{% endfor %}
(I had been struggling with this same problem for several hours. Thanks to this answer from Carl Meyer and this article on dynamic form generation from Jacob Kaplan-Moss for pointing me in the right directions.)
Group those fields in an additional list and then simply iterate over this list.
In __init__:
self.subunit_list = []
for subunit in self.selected_subunits:
field = forms.IntegerField()
self.fields['su%d' % (subunit['unit__id'])] = field
self.subunit_list.append(field)
In template:
{% for field in form.subunit_list %}
...
{% endfor %}
To correct gruszczy's answer, this code worked for me:
In __init__ of your form:
self.subunit_list = []
for subunit in self.selected_subunits:
field = forms.IntegerField()
self.fields['su%d' % (subunit['unit__id'])] = field
self.subunit_list.append(self['su%d' % (subunit['unit__id'])])
In your template:
{% for field in form.subunit_list %}
<!-- show form field (inputbox) -->
{{ field }}
{% endfor %}