I'm new in Django 3.0 and I'm lost in this easy question, please help me.
I have 2 models:
class Product(models.Model):
fields...
class ProductType(models.Model):
product = models.ForeignKey(Product, related_name='product_types', on_delete=models.CASCADE)
color = models.Charfield(...)
In my template, I would like to show all the related product types and their fields to a specific product:
...
{% for product in products %}
{{ product.??? }}
{% endfor %}
Here is my view:
class ProductsView(ListView):
collection = None
model = Product
paginate_by = 6
template_name = 'shop/product/list.html'
context_object_name = 'products'
def get_queryset(self):
products = Product.objects.filter(available=True)
collection_name = self.kwargs['collection_name'] if 'collection_name' in self.kwargs else None
if collection_name:
collection = get_object_or_404(Collection, name=collection_name)
products = products.filter(collection=collection)
return products
def get_context_data(self):
context = super().get_context_data(**self.kwargs)
context['notification'] = Notification.objects.all()
if 'collection_name' in self.kwargs:
context['collection'] = get_object_or_404(Collection, name=self.kwargs['collection_name'])
context['collection_name'] = self.kwargs['collection_name']
context['collections'] = Collection.objects.all()
return context
Thank you
You access the related ProductTypes through a manager that has as name the value you specify as related_name=… [Django-doc], so in this case:
{% for product in products %}
{% for type in product.product_types.all %}
{{ type.color }}
{% endfor %}
{% endfor %}
To boost efficiency, you can fetch all related ProductTypes for the elements in the queryset with .prefetch_related(…) [Django-doc]:
class ProductsView(ListView):
# …
def get_queryset(self):
products = Product.objects.prefetch_related('product_types').filter(available=True)
collection_name = self.kwargs['collection_name'] if 'collection_name' in self.kwargs else None
if collection_name:
collection = get_object_or_404(Collection, name=collection_name)
products = products.filter(collection=collection)
return products
I have this simplified model and form:
class Books(models.Model):
name = models.CharField(max_length=500)
price = models.DecimalField(max_digits=6, decimal_places=2)
default = models.BooleanField(default=False)
def __str__(self):
return self.name
class BookForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['Field'] = forms.ModelChoiceField(queryset=None, empty_label=None, widget=forms.RadioSelect)
self.fields['Field'].queryset = Books.objects.all()
self.fields['Field'].initial = Books.objects.filter(default=True).first()
This will result in a RadioSelect-Form, like this:
(x) Book1
( ) Book2
( ) Book3
My problem is, how can I add the price in the RadioSelect form, that it's just visible.
It should appear after the name of the book, ideally even in a different font, which I set over a bootstrap class (e.g "text-primary") (this is not mandatory)
(x) Book1 (10 €)
( ) Book2 (20 €)
( ) Book3 (30 €)
I know i can return the name and price in the model, like
class Books(models.Model):
name = models.CharField(max_length=500)
price = models.DecimalField(max_digits=6, decimal_places=2)
default = models.BooleanField(default=False)
def __str__(self):
return '%s (%s €)' % (self.value, str(self.price))
But because of other reasons, i can not do this. I just need to return the name. Are there other ways to do this?
I even read into django-crispy-forms, but couldnt find a solution.
You can use .label_from_instance.
From the documentation:
The __str__() method of the model will be called to generate string representations of the objects for use in the field’s choices. To provide customized representations, subclass ModelChoiceField and override label_from_instance.
You can define a function that gives you the representation that you want and then set .label_from_instance on your field.
Your BookForm then looks like this:
class BookForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['Field'] = forms.ModelChoiceField(queryset=None, empty_label=None, widget=forms.RadioSelect)
self.fields['Field'].queryset = Books.objects.all()
self.fields['Field'].initial = Books.objects.filter(default=True).first()
# customize how your option is rendered in the template
self.fields["Field"].label_from_instance = lambda item: f"{item} ({item.price} €)"
To apply CSS on the label add HTML. Instead of using style='...' you can also use classes so it works with Bootstrap.
self.fields["Field"].label_from_instance = lambda item: f"{item} <span style='color:red;'>({item.price} €)</span>"
For Python versions before 3.7:
self.fields["Field"].label_from_instance = lambda item: str(item) + "<span style='color:red;'>(" + str(item.price) + " €)</span>"
Then render your form in the template like this:
<form action="{% url 'books' %}" method="post">
{% csrf_token %}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
<ul>
{% for choice in form.Field %}
<li>{{ choice.choice_label|safe }}</li>
{% endfor %}
</ul>
<input class="btn btn-dark" type="submit" value="Submit">
</form>
Iterate over the choices for your field and then you can get your customized labels with:
{{ choice.choice_label|safe }}
The safe filter is needed so your HTML is not escaped.
I have two models Damaged and Product.In the Product model the quantity of the product depends upon the value of damaged_quantity which is stored in another table.For example if the damaged_quantity is in damaged table then the value of quantity in product should be quantity-damaged_quantity if the damaged.product_id == product.id .I tried like this but it is not working
models.py
class Damaged(models.Model):
product = models.ForeignKey('Product', on_delete=models.CASCADE)
damaged_quantity = models.IntegerField(default=0)
def __str__(self):
return self.product
class Product(models.Model):
name = models.CharField(max_length=250)
category = models.ForeignKey(Category,on_delete=models.CASCADE)
quantity = models.IntegerField()
views.py I access the queryset of product like this in my views
def list_products(request):
products = Product.objects.annotate(damaged_product_quantity=Sum('damagedproducts__damaged_quantity')).annotate(
real_quantity=ExpressionWrapper(F('quantity') - F('damaged_product_quantity'), output_field=IntegerField()))
list_product_template. Here while displaying the real_quantity if the damage.damaged_quanity and the product.quantity are euqal then it doesnot changes the value.Instead of becomming Zero it doesnot change the value.In other case it is working fine.
{% if not product.real_quantity %}
{{product.quantity}}
{% elif product.real_quantity == product.quantity %}
0
{% else %}
{{ product.real_quantity }}
{% endif %}
product_detail page
def product_detail(request, slug):
product = get_object_or_404(Product, slug=slug)
damaged = Damaged.objects.all()
return render(request, 'pos/product_detail.html', {'product': product,'damaged':damaged})
product_detail template.I tried like this to get the current quantity of products after adding damaged_products but it is nt working well.It is giving me both {% if %} and {% else %} part.How can i solve this?
product quantity:
{% for damage in damaged %}
{% if product.id == damage.product_id %}
{{product.quantity|subtract:damage.damaged_quantity}}
{% else %}
{{product.quantity}}
{% endif %}
{% endfor %}
I think you need to override the save method in Damaged model:
class Damaged(models.Model):
product = models.ForeignKey('Product', on_delete=models.CASCADE)
damaged_quantity = models.IntegerField(default=0)
def __str__(self):
return self.product
def save(self, *args, **kwargs):
super(Damaged, self).save(*args, **kwargs)
self.product.quantity = self.product.quantity - self.damaged_quantity
self.product.save()
But this solution might be inconsistent. For example, if you try to update the Damaged model, then value of the product will be updated again.
I would recommend using annotation to attach value with Product, so that you can query if needed. For example:
For this, lets add a related_name field in Damaged model:
class Damaged(models.Model):
product = models.ForeignKey('Product', on_delete=models.CASCADE, related_name='damagedproducts')
damaged_quantity = models.IntegerField(default=0)
Usage:
from django.db.models import Sum, F, IntegerField
products = Product.objects.annotate(damaged_product_quantity=Sum('damagedproducts__damaged_quantity')).annotate(real_quantity=ExpressionWrapper(F('quantity') - F('damaged_product_quantity'), output_field=IntegerField())
real_quantity_more_than_ten = products.filter(real_quantity__gt=10)
for p in real_quantity_more_than_ten:
print(p.real_quantity)
Update
from django.db.models import Sum
def product_detail(request, slug):
product = get_object_or_404(Product, slug=slug)
damaged = product.productdamaged.all()
if damaged.exists():
damage_amount = damaged.aggregate(d_amount = Sum('productdamaged__damaged_quantity')).get('d_amount', 0)
else:
damage_amount = 0
return render(request, 'pos/product_detail.html', {'product': product,'damage_amount':damage_amount})
# template
{% if damage_amount != 0 %}
{{product.quantity|subtract:damage_amount}}
{% else %}
{{product.quantity}}
{% endif %}
I want to be able to vary the placeholder like so:
<input placeholder=" {{ input.placeholder }}">
Where input is a model with the "placeholder" field. The placeholder field will vary since I'll be using a formset, and each placeholder will vary.
Here's my modelForm
class Value(forms.ModelForm):
class Meta:
model = ValueModel
fields = ['value_text']
widgets = {
'value_text': forms.TextInput(attrs={'class': 'form-control'})
and my modelformset
values_formset = modelformset_factory(model=ValueModel, extra=0, form=Value)
I've tried
class Value(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(Value, self).__init__(*args, **kwargs)
self.fields['value_text'].widget.attrs['placeholder'] = self.fields['placeholder']
class Meta:
model = ValueModel
fields = ['value_text', 'placeholder']
widgets = {
'value_text': forms.TextInput(attrs={'class': 'form-control'})
And other attempts at trying to modify the self.fields with no success.
Edit: The relevant part of my views.py:
def page_view(request, values_id):
values_form = values_formset(queryset=ValueModel.objects.filter(
values_id=values_id))
context = {'value': values_form}
return render(request, 'view.html', context)
My template view:
{{ value.management_form }}
{% for form in value %}
{{ form.id }}
{{ form.value_text }}
{% endfor %}
self.fields['placeholder'] refers to a form field object, not a value; you couldn't use it as a placeholder. But it seems like what you want is to use the value of the model instance.
def __init__(self, *args, **kwargs):
super(Value, self).__init__(*args, **kwargs)
self.fields['value_text'].widget.attrs['placeholder'] = self.instance.placeholder
How can I group checkboxes produced by CheckboxSelectMultiple by a related model?
This is best demonstrated by example.
models.py:
class FeatureCategory(models.Model):
name = models.CharField(max_length=30)
class Feature(models.Model):
name = models.CharField(max_length=30)
category = models.ForeignKey(FeatureCategory)
class Widget(models.Model):
name = models.CharField(max_length=30)
features = models.ManyToManyField(Feature, blank=True)
forms.py:
class WidgetForm(forms.ModelForm):
features = forms.ModelMultipleChoiceField(
queryset=Feature.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False
)
class Meta:
model = Widget
views.py:
def edit_widget(request):
form = WidgetForm()
return render(request, 'template.html', {'form': form})
template.html:
{{ form.as_p }}
The above produces the following output:
[] Widget 1
[] Widget 2
[] Widget 3
[] Widget 1
[] Widget 2
What I would like is for the feature checkboxes to be grouped by feature category (based on the ForeignKey):
Category 1:
[] Widget 1
[] Widget 2
[] Widget 3
Category 2:
[] Widget 1
[] Widget 2
How can I achieve this? I have tried using the {% regroup %} template tag to no avail.
Any advice much appreciated.
Thanks.
You have to write the custom CheckboxSelectMultiple widget. Using the snippet I have tried make the CheckboxSelectMultiple field iterable by adding the category_name as an attribute in field attrs. So that I can use regroup tag in template later on.
The below code is modified from snippet according to your need, obviously this code can be made more cleaner and more generic, but at this moment its not generic.
forms.py
from django import forms
from django.forms import Widget
from django.forms.widgets import SubWidget
from django.forms.util import flatatt
from django.utils.html import conditional_escape
from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.safestring import mark_safe
from itertools import chain
import ast
from mysite.models import Widget as wid # your model name is conflicted with django.forms.Widget
from mysite.models import Feature
class CheckboxInput(SubWidget):
"""
An object used by CheckboxRenderer that represents a single
<input type='checkbox'>.
"""
def __init__(self, name, value, attrs, choice, index):
self.name, self.value = name, value
self.attrs = attrs
self.choice_value = force_unicode(choice[1])
self.choice_label = force_unicode(choice[2])
self.attrs.update({'cat_name': choice[0]})
self.index = index
def __unicode__(self):
return self.render()
def render(self, name=None, value=None, attrs=None, choices=()):
name = name or self.name
value = value or self.value
attrs = attrs or self.attrs
if 'id' in self.attrs:
label_for = ' for="%s_%s"' % (self.attrs['id'], self.index)
else:
label_for = ''
choice_label = conditional_escape(force_unicode(self.choice_label))
return mark_safe(u'<label%s>%s %s</label>' % (label_for, self.tag(), choice_label))
def is_checked(self):
return self.choice_value in self.value
def tag(self):
if 'id' in self.attrs:
self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index)
final_attrs = dict(self.attrs, type='checkbox', name=self.name, value=self.choice_value)
if self.is_checked():
final_attrs['checked'] = 'checked'
return mark_safe(u'<input%s />' % flatatt(final_attrs))
class CheckboxRenderer(StrAndUnicode):
def __init__(self, name, value, attrs, choices):
self.name, self.value, self.attrs = name, value, attrs
self.choices = choices
def __iter__(self):
for i, choice in enumerate(self.choices):
yield CheckboxInput(self.name, self.value, self.attrs.copy(), choice, i)
def __getitem__(self, idx):
choice = self.choices[idx] # Let the IndexError propogate
return CheckboxInput(self.name, self.value, self.attrs.copy(), choice, idx)
def __unicode__(self):
return self.render()
def render(self):
"""Outputs a <ul> for this set of checkbox fields."""
return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
% force_unicode(w) for w in self]))
class CheckboxSelectMultipleIter(forms.CheckboxSelectMultiple):
"""
Checkbox multi select field that enables iteration of each checkbox
Similar to django.forms.widgets.RadioSelect
"""
renderer = CheckboxRenderer
def __init__(self, *args, **kwargs):
# Override the default renderer if we were passed one.
renderer = kwargs.pop('renderer', None)
if renderer:
self.renderer = renderer
super(CheckboxSelectMultipleIter, self).__init__(*args, **kwargs)
def subwidgets(self, name, value, attrs=None, choices=()):
for widget in self.get_renderer(name, value, attrs, choices):
yield widget
def get_renderer(self, name, value, attrs=None, choices=()):
"""Returns an instance of the renderer."""
choices_ = [ast.literal_eval(i[1]).iteritems() for i in self.choices]
choices_ = [(a[1], b[1], c[1]) for a, b, c in choices_]
if value is None: value = ''
str_values = set([force_unicode(v) for v in value]) # Normalize to string.
if attrs is None:
attrs = {}
if 'id' not in attrs:
attrs['id'] = name
final_attrs = self.build_attrs(attrs)
choices = list(chain(choices_, choices))
return self.renderer(name, str_values, final_attrs, choices)
def render(self, name, value, attrs=None, choices=()):
return self.get_renderer(name, value, attrs, choices).render()
def id_for_label(self, id_):
if id_:
id_ += '_0'
return id_
class WidgetForm(forms.ModelForm):
features = forms.ModelMultipleChoiceField(
queryset=Feature.objects.all().values('id', 'name', 'category__name'),
widget=CheckboxSelectMultipleIter,
required=False
)
class Meta:
model = wid
Then in template:
{% for field in form %}
{% if field.name == 'features' %}
{% regroup field by attrs.cat_name as list %}
<ul>
{% for el in list %}
<li>{{el.grouper}}
<ul>
{% for e in el.list %}
{{e}} <br />
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% else %}
{{field.label}}: {{field}}
{% endif %}
{% endfor %}
Results:
I added countries name in category table, and cities name in features table so in template I was able to regroup the cities (features) according to country (category)
Here's a solution for current versions of Django (~2.1).
## forms.py
from itertools import groupby
from django import forms
from django.forms.models import ModelChoiceIterator, ModelMultipleChoiceField
from .models import Feature, Widget
class GroupedModelMultipleChoiceField(ModelMultipleChoiceField):
def __init__(self, group_by_field, group_label=None, *args, **kwargs):
"""
``group_by_field`` is the name of a field on the model
``group_label`` is a function to return a label for each choice group
"""
super(GroupedModelMultipleChoiceField, self).__init__(*args, **kwargs)
self.group_by_field = group_by_field
if group_label is None:
self.group_label = lambda group: group
else:
self.group_label = group_label
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return GroupedModelChoiceIterator(self)
choices = property(_get_choices, ModelMultipleChoiceField._set_choices)
class GroupedModelChoiceIterator(ModelChoiceIterator):
def __iter__(self):
"""Now yields grouped choices."""
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
for group, choices in groupby(
self.queryset.all(),
lambda row: getattr(row, self.field.group_by_field)):
if group is None:
for ch in choices:
yield self.choice(ch)
else:
yield (
self.field.group_label(group),
[self.choice(ch) for ch in choices])
class WidgetForm(forms.ModelForm):
class Meta:
model = Widget
fields = ['features',]
def __init__(self, *args, **kwargs):
super(WidgetForm, self).__init__(*args, **kwargs)
self.fields['features'] = GroupedModelMultipleChoiceField(
group_by_field='category',
queryset=Feature.objects.all(),
widget=forms.CheckboxSelectMultiple(),
required=False)
Then you can use {{ form.as_p }} in the template for properly grouped choices.
If you would like to use the regroup template tag and iterate over the choices, you will also need to reference the following custom widget:
class GroupedCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
def optgroups(self, name, value, attrs=None):
"""
The group name is passed as an argument to the ``create_option`` method (below).
"""
groups = []
has_selected = False
for index, (option_value, option_label) in enumerate(self.choices):
if option_value is None:
option_value = ''
subgroup = []
if isinstance(option_label, (list, tuple)):
group_name = option_value
subindex = 0
choices = option_label
else:
group_name = None
subindex = None
choices = [(option_value, option_label)]
groups.append((group_name, subgroup, index))
for subvalue, sublabel in choices:
selected = (
str(subvalue) in value and
(not has_selected or self.allow_multiple_selected)
)
has_selected |= selected
subgroup.append(self.create_option(
name, subvalue, sublabel, selected, index,
subindex=subindex, attrs=attrs, group=group_name,
))
if subindex is not None:
subindex += 1
return groups
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None, group=None):
"""
Added a ``group`` argument which is included in the returned dictionary.
"""
index = str(index) if subindex is None else "%s_%s" % (index, subindex)
if attrs is None:
attrs = {}
option_attrs = self.build_attrs(self.attrs, attrs) if self.option_inherits_attrs else {}
if selected:
option_attrs.update(self.checked_attribute)
if 'id' in option_attrs:
option_attrs['id'] = self.id_for_label(option_attrs['id'], index)
return {
'name': name,
'value': value,
'label': label,
'selected': selected,
'index': index,
'attrs': option_attrs,
'type': self.input_type,
'template_name': self.option_template_name,
'wrap_label': True,
'group': group,
}
class WidgetForm(forms.ModelForm):
class Meta:
model = Widget
fields = ['features',]
def __init__(self, *args, **kwargs):
super(WidgetForm, self).__init__(*args, **kwargs)
self.fields['features'] = GroupedModelMultipleChoiceField(
group_by_field='category',
queryset=Feature.objects.all(),
widget=GroupedCheckboxSelectMultiple(),
required=False)
Then the following should work in your template:
{% regroup form.features by data.group as feature_list %}
{% for group in feature_list %}
<h6>{{ group.grouper|default:"Other Features" }}</h6>
<ul>
{% for choice in group.list %}
<li>{{ choice }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
Credit to the following page for part of the solution:
https://mounirmesselmeni.github.io/2013/11/25/django-grouped-select-field-for-modelchoicefield-or-modelmultiplechoicefield/