I have a table inside a form, generated by a formset.
In this case, my problem is to save all the items after one of them is modified, adding a new "virtual" column as the sum of other two (that is only generated when displaying the table, not saved).
I tried different ways, but no one is working.
Issues:
This save is not working at all. It worked when it was only one form, but not for the formset
I tried to generate the column amount as a Sum of box_one and box_two without success. I tried generating the form this way too, but this is not working:
formset = modelformset_factory(
Item, form=ItemForm)(queryset=Item.objects.order_by(
'code__name').annotate(amount=Sum('box_one') + Sum('box_two')))
This issue is related to this previous one, but this new one is simpler:
Pre-populate HTML form table from database using Django
Previous related issues at StackOverflow are very old and not working for me.
I'm using Django 2.0.2
Any help would be appreciated. Thanks in advance.
Current code:
models.py
class Code(models.Model):
name = models.CharField(max_length=6)
description = models.CharField(max_length=100)
def __str__(self):
return self.name
class Item(models.Model):
code = models.ForeignKey(Code, on_delete=models.DO_NOTHING)
box_one = models.IntegerField(default=0)
box_two = models.IntegerField(default=0)
class Meta:
ordering = ["code"]
views.py
class ItemForm(ModelForm):
description = CharField()
class Meta:
model = Item
fields = ['code', 'box_one', 'box_two']
def save(self, commit=True):
item = super(ItemForm, self).save(commit=commit)
item.box_one = self.cleaned_data['box_one']
item.box_two = self.cleaned_data['box_two']
item.code.save()
def get_initial_for_field(self, field, field_name):
if field_name == 'description' and hasattr(self.instance, 'code'):
return self.instance.code.description
else:
return super(ItemForm, self).get_initial_for_field(
field, field_name)
class ItemListView(ListView):
model = Item
def get_context_data(self, **kwargs):
data = super(ItemListView, self).get_context_data()
formset = modelformset_factory(Item, form=ItemForm)()
data['formset'] = formset
return data
urls.py
app_name = 'inventory'
urlpatterns = [
path('', views.ItemListView.as_view(), name='index'),
item_list.html
...
<div>
<form action="" method="post"></form>
<table>
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<thead>
<tr>
{% if forloop.first %}
<th>{{ form.code.label_tag }} </th>
<th>{{ form.description.label_tag }} </th>
<th> <label>Amount:</label> </th>
<th>{{ form.box_one.label_tag }} </th>
<th>{{ form.box_two.label_tag }} </th>
{% endif %}
</tr>
</thead>
<tbody>
<tr>
<td>{{ form.code }}</td>
<td>{{ form.description }}</td>
<td>{{ form.amount }}</td>
<td>{{ form.box_one }}</td>
<td>{{ form.box_two }}</td>
</tr>
</tbody>
{% endfor %}
<input type="submit" value="Update" />
</table>
</form>
</div>
...
Annotating query with virtual column
Sum is an aggregate expression and is not how you want to be annotating this query in this case. Instead, you should use an F exrepssion to add the value of two numeric fields
qs.annotate(virtual_col=F('field_one') + F('field_two'))
So your corrected queryset would be
Item.objects.order_by('code__name').annotate(amount=F('box_one') + F('box_two'))
The answer provided by cezar works great if intend to use the property only for 'row-level' operations. However, if you intend to make a query based on amount, you need to annotate the query.
Saving the formset
You have not provided a post method in your view class. You'll need to provide one yourself since you're not inheriting from a generic view that provides one for you. See the docs on Handling forms with class-based views. You should also consider inheriting from a generic view that handles forms. For example ListView does not implement a post method, but FormView does.
Note that your template is also not rendering form errors. Since you're rendering the formset manually, you should consider adding the field errors (e.g. {{ form.field.errors}}) so problems with validation will be presented in the HTML. See the docs on rendering fields manually.
Additionally, you can log/print the errors in your post method. For example:
def post(self, request, *args, **kwargs):
formset = MyFormSet(request.POST)
if formset.is_valid():
formset.save()
return SomeResponse
else:
print(formset.errors)
return super().post(request, *args, **kwargs)
Then if the form does not validate you should see the errors in your console/logs.
You're already on the right path. So you say you need a virtual column. You could define a virtual property in your model class, which won't be stored in the database table, nevertheless it will be accessible as any other property of the model class.
This is the code you should add to your model class Item:
class Item(models.Model):
# existing code
#property
def amount(self):
return self.box_one + self.box_one
Now you could do something like:
item = Item.objects.get(pk=1)
print(item.box_one) # return for example 1
print(item.box_two) # return for example 2
print(item.amount) # it will return 3 (1 + 2 = 3)
EDIT:
Through the ModelForm we have access to the model instance and thus to all of its properties. When rendering a model form in a template we can access the properties like this:
{{ form.instance.amount }}
The idea behind the virtual property amount is to place the business logic in the model and follow the approach fat models - thin controllers. The amount as sum of box_one and box_two can be thus reused in different places without code duplication.
Related
As I loop through a Prize.objects.all() set, I am trying to get the quantity value from PrizeItem, but it seems to reference only the GameItem model, not giving access to the intermediate PrizeItem attributes.
How do I get access to the Through model's attributes?
MODELS:
class GameItem(models.Model):
'...'
class Prize(models.Model):
'...'
item = models.ManyToManyField(GameItem, through='PrizeItem')
class PrizeItem(models.Model):
#relations
game_item = models.ForeignKey(GameItem, on_delete=models.CASCADE)
prize = models.ForeignKey(Prize, on_delete=models.CASCADE)
#Item details
quantity = models.IntegerField(null=True, blank=True, default=1)
VIEWS:
def gameprizes(request):
prizes=Prize.objects.all()
context={'prizes':prizes}
return render(request, "the-app/game-prizes.html", context)
TEMPLATE:
{% if prize.item.all %}
<table class="table table-condensed">
<tbody>
{% for prize_item in prize.item.all %}
<tr>
<td>{{ prize_item.type }}</td><td>{{ prize_item.name }} {{ prize_item.quantity }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
It gets a little ugly when it comes to intermediate models in querysets. One way you can address this would be:
class Prize(models.Model):
'...'
item = models.ManyToManyField(GameItem, through='PrizeItem')
def quantity(self):
return self.item.quantity
Note that this does lead to additional queries, and you can use prefetch_related in your query to optimize
Another option would be to use the template
For game item, you would be doing something like:
class GameItem(models.Model):
'...'
def quantity(self):
return self.prizeitem_set.first()
What the hell with the models?
~> Use the #karthikr answer
Just prepare in views.py (overwrite query_set)
Use (_set ~ join) in html
Have a nice day, don't make it hard, give the hardest part for the strongest (server - database)
I'm developing a Django App for a car rental customer.
In my model I have a Car model with their car properties (ex: passengers, ports, transmission, category, ecc...).
I had correctly setup my view and template that list all cars.
Now, I need to implement a form for filters the cars list by some characteristics.
I would like to implement in my CarListView a filter logic that filter cars by characteristics included in the query string.
ex: /cars/?ports=1&passengers=3
How can I implements this in an elegant Django way?
Is better to use a form with GET method or setup custom urls in my urls.py??
One possible solution (for other users):
class CarResultsView(BreadcrumbsMixin, ListView):
queryset = Vehicle.objects.all()
template_name = "my_app/cars.html"
def get_queryset(self):
queryset = super(CarResultsView, self).get_queryset()
search_form = VehicleFilterForm(self.request.GET)
if search_form.is_valid():
queryset = queryset.filter(**search_form.cleaned_data)
return queryset
def get_context_data(self, **kwargs):
context = super(CarResultsView, self).get_context_data(**kwargs)
get_params = self.request.GET
context['form'] = VehicleFilterForm(get_params)
return context
In your opinion, could exists one more elegant solution?
This is right in the docs.
You're going to want to override the get_queryset() method, as well as modify your URL patterns to reflect the kwargs you're going to be capturing.
You must define the correct regexp in your "urls.py" to capture the car parameters. See for example this question.
Knowing the parameters, you will be able to filter the list of all cars by the needed characteristics in your view function. Something like this:
cars = Car.objects.filter(ports = ports, passengers = passengers)
Be sure to check for empty/undefined car parameters in your view. So that if the request doesn't have any parameters, all cars are displayed.
Forms
If you don't have previous experience with forms, read and try Working with forms in Django.
In your car list template, include both the search form and the filtered list. Similar to this:
<form action="/cars" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Search" />
</form>
<table>
{% for car in cars %}
<tr><td>{{ car.name }}</td>
<td>{{ car.engine }}</td>
<td>{{ car.price }}</td>
</tr>
{% endfor %}
</table>
When the form is submitted, a POST request is issued to your site. The GET request must show an empty form ("unbound form") and list of all cars. The template will be used to generate response both for POST and GET requests.
My problem is simple: I have Users who own Assets or Assets which belong to Users If you prefer and I cannot make it to retrieve the number (count) of Assets each User has. I know this might be sound silly to most of you but I am new to python/django (coming from PHP/MySQL) and I do not know how things work here. I do not want to be engaged with raw SQL - this would be my last choice If nothing else works.
(*) I have removed all non-related raws from the code
Users
class Users(models.Model):
firstname = models.CharField(max_length=100)
lastname = models.CharField(max_length=100)
Assets
class Assets(models.Model):
serial = models.CharField(unique=True, max_length=100)
user = models.ForeignKey('Users', blank=True, null=True)
# this is what I am playing with to retrieve the number of assets each user owns
#classmethod
def user_assets(self):
return Assets.objects.filter(user=user).count()
views.py
class UserList(ListView):
model = Users
def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs)
context['user_assets'] = self.model.user_assets()
return context
template
{% for user in object_list %}
<tr>
<td>{{ user.id }}</td>
<td>
{{ user_assets }}
</td>
</tr>
{% endfor %}
How can I get that number? I have read about aggregations, annotations and filters but can't really get it.
EDIT:
I am looking for a simple solution by using class based views and easily expandable (I may want to add other models later)
In your UserList instead using model, use this queryset:
from django.db.models import Count
class UserList(ListView):
queryset = Users.objects.annotate(num_assets=Count('assets'))
and define your user field like so:
user = models.ForeignKey('Users', blank=True, null=True, related_name='assets')
then from template:
{{ user.num_assets }}
Also please remember, it's a good practice to use singular model names, to avoid confusion with reverse relation names..
You are doing weird things. Use the related managers that django give you instead. I'll write the view as a function based view:
views.py
def users_list(request):
object_list = Users.objects.all()
render(request, 'mytemplate.html', { 'object_list': object_list })
You can get the counts directly in the template via the RelatedManager:
mytemplate.html
{% for user in object_list %}
<tr>
<td>{{ user.id }}</td>
<td>
{{ user.assets_set.count }}
</td>
</tr>
{% endfor %}
You could also annotate with a count. But learn to float before you swim :)
BTW, you should call your models "User" and "Asset", not Users and Assets.
You need to use select_related(), count() and pass user instance as argument to class method like so:
#classmethod
def user_assets(cls,user):
return Assets.objects.select_related('Users').filter(user=user).count()
and then use it like so:
user = Users.objects.all()[0] # some user object (this assumes you have at least one user)
Assets.user_assets(user)
this should work fine, you can try it in the shell.
In your context this will be used like this:
user = self.model.all()[0] # or get or filter just get some particular user
context['user_assets'] = Assets.user_assets(user)
EDIT: added links, and Users.object.all() instead of Users.object.get(), also added example suited to your specific use case.
Currently, I am building a form using the default django template like this:
class old_form(forms.Form):
row_1 = forms.FloatField(label='Row 1')
row_2_col_1 = forms.FloatField(label='Row 2_1')
row_2_col_2 = forms.FloatField(label='Row 2_2')
html = str(old_form())
However, I would like to add multiple columns to my template, and still use django forms object to define parameters.
New temp. should something like (or it can loop through all the variables):
def getdjtemplate():
dj_template ="""
<table>
<tr>{{ table.row_1 }}</tr>
<tr>
<td>{{ table.row_2_col_1 }}</td>
<td>{{ table.row_2_col_2 }}</td>
</tr>
"""
return dj_template
djtemplate = getdjtemplate()
newtmpl = Template(djtemplate)
My question is how to 'combine' the new template and class old_form()?
Thanks for the help!
You can customize the form HTML using its fields, as shown in the documentation. You are doing this in an unusual way; normally you would have the template in a file instead of returning it from a function, but you can still do this:
from django.template import Context
def getdjtemplate():
dj_template = """
<table>
{% for field in form %}
<tr>{{ field }}</tr>
{% endfor %}
</table>
"""
return dj_template
form = old_form()
djtemplate = getdjtemplate()
newtmpl = Template(djtemplate)
c = Context({'form': form})
newtmpl.render(c)
I'd like to know how can I add .error class to input elements (to registration app) when the form validation fails.
If you want to place your error CSS class to form input widgets (not their containers), you can derive your form class from the following one:
class StyledErrorForm(forms.Form):
def is_valid(self):
result = super().is_valid()
# loop on *all* fields if key '__all__' found else only on errors:
for x in (self.fields if '__all__' in self.errors else self.errors):
attrs = self.fields[x].widget.attrs
attrs.update({'class': attrs.get('class', '') + ' is-invalid'})
return result
It's now easy -- new feature in Django 1.2
Just add an attribute on the form class & you're good to go. This feature is mentioned in the docs under a "new in 1.2" note, but you can find the magic at django.forms.forms.BoundField.css_classes Here's the API reference, and an example:
class MyForm(forms.Form):
required_css_class = "required"
error_css_class = "error"
This can be done completely through your template.
You build the form template for each form field that you want to test you can use the following example construct
<input type="text" class="reg-txt{% if form.fieldname.errors %} errors{% endif %}"/>
This lets you provide the interface you want without modifying the view & django form code.
Using a Custom Template...
Personally never had much luck using the built in Django error classing solutions, and besides, I like to use the built in 'striptags' template filter on the errors, to get rid of all the html list stuff which I cant figure out how to render nicely anyway.
I use the following custom template to class them as 'error_id'.
#register.filter(is_safe=True)
#stringfilter
def error_id(value):
if value=='':
return ''
else:
return r'<span class="error_id">'+value+'</span>'
Render the individual errors in your template using:
{{ form.my_field.errors|striptags|error_id}}
Or render the whole form using something like:
<table border="1" cellpadding="5px" align="center">
{% for field in form.visible_fields %}
<tr>
<td> {{ field.label_tag }}: </td>
<td> {{ field }} </td>
<td> {{ field.errors|striptags|error_id }} </td>
</tr>
{% endfor %}
</table>
(Better late than never)
You should be able to do this with Django Uni Form