Multiple models in Django form - python

I have the following models:
class Category(models.Model):
label = models.CharField(max_length=40)
description = models.TextField()
class Rating(models.Model):
review = models.ForeignKey(Review, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
rating = models.SmallIntegerField()
class Review(models.Model):
author = models.ForeignKey(User, related_name="%(class)s_author", on_delete=models.CASCADE)
coach = models.ForeignKey(User, related_name="%(class)s_coach", on_delete=models.CASCADE)
comments = models.TextField()
I'd like to create a front-end form which allows a user to review a coach, including a rating for some pre-populated categories.
In my head, the form would look something like:
Coach: _______________ # Selection of all coach users from DB, this works as standard
Category: "Professionalism" # These would be DB entries from the Category model
Rating: _ / 5
Category: "Friendliness"
Rating: _ / 5
Category: "Value"
Rating: _ / 5
Comments:
_________________________________
_________________________________
Submit
I've seen Django Formsets in the documentation but these appear to exist for creating multiple forms from the same model as a single form?
Not looking for a full answer, but if someone could point me in the right direction, it'd be hugely appreciated.
EDIT: Vineet's answer (https://stackoverflow.com/a/65883875/864245) is almost exactly what I'm looking for, but it's for the Admin area, where I need it on the front-end.

Given that the categories are fairly static, you don't want your users to select the categories. The categories themselves should be labels, not fields for your users to select.
You mention in the comment, that the labels will sometimes change. I think there are two questions I would ask before deciding how to proceed here:
Who will update the labels moving forwards (do they have basic coding ability, or are they reliant on using something like the admin).
When the labels change, will their fundamental meaning change or will it just be phrasing
Consideration 1
If the person changing the labels has a basic grasp of Django, and the appropriate permissions (or can ask a dev to make the changes for them) then just hard-coding these 5 things is probably the best way forward at first:
class Review(models.Model):
author = models.ForeignKey(User, related_name="%(class)s_author", on_delete=models.CASCADE)
coach = models.ForeignKey(User, related_name="%(class)s_coach", on_delete=models.CASCADE)
comments = models.TextField()
# Categories go here...
damage = models.SmallIntegerField(
help_text="description can go here",
verbose_name="label goes here"
)
style = models.SmallIntegerField()
control = models.SmallIntegerField()
aggression = models.SmallIntegerField()
This has loads of advantages:
It's one very simple table that is easy to understand, instead of 3 tables with joins.
This will make everything up and down your code-base simpler. It'll make the current situation (managing forms) easier, but it will also make every query, view, template, report, management command, etc. you write easier, moving forwards.
You can edit the labels and descriptions as and when needed with verbose_name and help_text.
If changing the code like this isn't an option though, and the labels have to be set via something like the Django admin-app, then a foreign-key is your only way forward.
Again, you don't really want your users to choose the categories, so I would just dynamically add them as fields, rather than using a formset:
class Category(models.Model):
# the field name will need to be a valid field-name, no space etc.
field_name = models.CharField(max_length=40, unique=True)
label = models.CharField(max_length=40)
description = models.TextField()
class ReviewForm.forms(forms.Form):
coach = forms.ModelChoiceField()
def __init__(self, *args, **kwargs):
return_value = super().__init__(*args, **kwargs)
# Here we dynamically add the category fields
categories = Categories.objects.filter(id__in=[1,2,3,4,5])
for category in categories:
self.fields[category.field_name] = forms.IntegerField(
help_text=category.description,
label=category.label,
required=True,
min_value=1,
max_value=5
)
self.fields['comment'] = forms.CharField(widget=forms.Textarea)
return return_value
Since (I'm assuming) the current user will be the review.author, you are going to need access to request.user and so we should save all your new objects in the view rather than in the form. Your view:
def add_review(request):
if request.method == "POST":
review_form = ReviewForm(request.POST)
if review_form.is_valid():
data = review_form.cleaned_data
# Save the review
review = Review.objects.create(
author=request.user,
coach=data['coach']
comment=data['comment']
)
# Save the ratings
for category in Category.objects.filter(id__in=[1,2,3,4,5]):
Rating.objects.create(
review=review
category=category
rating=data[category.field_name]
)
# potentially return to a confirmation view at this point
if request.method == "GET":
review_form = ReviewForm()
return render(
request,
"add_review.html",
{
"review_form": review_form
}
)
Consideration 2
To see why point 2 (above) is important, imagine the following:
You start off with 4 categories: Damage, Style, Control and Agression.
Your site goes live and some reviews come in. Say Coach Tim McCurrach gets a review with scores of 2,1,3,5 respectively.
Then a few months down the line we realise 'style' isn't a very useful category, so we change the label to 'effectiveness'.
Now Tim McCurrach has a rating of '1' saved against a category that used to have label 'style' but now has label 'effectiveness' which isn't what the author of the review meant at all.
All of your old data is meaningless.
If Style is only ever going to change to things very similar to style we don't need to worry so much about that.
If you do need to change the fundamental nature of labels, I would add an active field to your Category model:
class Category(models.Model):
field_name = models.CharField(max_length=40, unique=True)
label = models.CharField(max_length=40)
description = models.TextField()
active = models.BooleanField()
Then in the code above, instead of Category.objects.filter(id__in=[1,2,3,4,5]) I would write, Category.objects.filter(active=True). To be honest, I think I would do this either way. Hard-coding ids in your code is bad-practice, and very liable to going wrong. This second method is more flexible anyway.

Use this in your app's admin.py file
from django.contrib import admin
from .models import Review, Rating, Category
class RatingInline(admin.TabularInline):
model = Rating
fieldsets = [
('XYZ', {'fields': ('category', 'rating',)})
]
extra = 0
readonly_fields = ('category',)
show_change_link = True
def has_change_permission(self, request, obj=None):
return True
class ReviewAdmin(admin.ModelAdmin):
fields = ('author', 'coach', 'comments')
inlines = [RatingInline]
admin.site.register(Review, ReviewAdmin)
admin.site.register(Rating)
admin.site.register(Category)
You admin page will look like this:

You could "embed" an inline formset into your review form. Then you can call form.save() to save the review and all the associated ratings in one go. Here is a working example:
# forms.py
from django import forms
from . import models
class ReviewForm(forms.ModelForm):
class Meta:
model = models.Review
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ratings = forms.inlineformset_factory(
parent_model=models.Review,
model=models.Rating,
extra=5,
min_num=5,
)(
data=self.data if self.is_bound else None,
files=self.files if self.is_bound else None,
instance=self.instance,
)
def is_valid(self):
return super().is_valid() and self.ratings.is_valid() # AND
def has_changed(self):
return super().has_changed() or self.ratings.has_changed() # OR
def save(self):
review = super().save()
self.ratings.save()
return review
As you can see, the __init__() method sets the attribute self.ratings which you can later recall in your template like this:
<form method="post">
{% csrf_token %}
<div class="review">
{{ form.as_p }}
</div>
<div class="ratings">
{{ form.ratings.management_form }}
{% for rating_form in form.ratings %}
<div class="single_rating">
{{ rating_form.as_p }}
</div>
{% endfor %}
</div>
<button>Save</button>
</form>
Finally, here's how your views.py might look like (using Django's class-based views):
from django.views import generic
from . import models
from . import forms
class ReviewView(generic.UpdateView):
model = models.Review
form_class = forms.ReviewForm

Related

Three-level inline formsets in the frontend

I'm trying to accomplish a three-level stacked inline form in Django. Suppose these models:
class Anuncio(models.Model):
title = models.CharField(max_length=200)
delivery = models.CharField(max_length=100)
class Product(models.Model):
anuncio = models.ForeignKey(Anuncio, on_delete=models.CASCADE)
name = models.CharField(max_length=200)
quantity = models.PositiveIntegerField(default=1)
price = models.PositiveIntegerField()
class Image(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
image = models.ImageField()
There is a relation Anuncio-Product and another relation Product-Image. With this Django package, I accomplished exactly what I want in the Django admin: when creating an Anuncio object, I can add as many Products as I want, and those products can have as many Images as I want. I'm trying to accomplish this in the front end.
I think the way to go is with Django formsets, but I'm facing some problems. All the resources I've been able to find online are only 'two-level' formsets or in 'three-level' cases all the foreign keys point to the same parent model.
With this forms.py file:
class ProductForm(ModelForm):
class Meta:
model = Product
fields = ['name', 'quantity', 'price']
class ImageForm(ModelForm):
class Meta:
model = Imagen
fields = ['image']
class AnuncioForm(ModelForm):
class Meta:
model = Anuncio
fields = ['title', 'delivery']
And this views.py function:
def anunciocreateview(request):
form = AnuncioForm(request.POST or None)
ProductFormSet = inlineformset_factory(Anuncio, Product, form=ProductForm)
ImageFormSet = inlineformset_factory(Product, Image, form=ImageForm)
if all([form.is_valid(), ProductFormSet.is_valid(), ImageFormSet.is_valid()]):
parent = form.save(commit=False)
parent.anunciante = request.user
parent.save()
for form in ProductoFormSet:
child = form.save(commit=False)
child.anuncio = parent
child.save()
for form in ImagenFormSet:
imagen = form.save(commit=False)
imagen.product = form.product
imagen.save()
context = {
'form_1' : form,
'form_2' : ProductFormSet,
'form_3' : ImageFormSet,
}
But I think I'm missing important points when it comes to add the proper relations between models. This set-up gives an AttributeError of: 'ProductForm' object has no attribute '__name__'
The, for example, 'add (extra) Product' that appears in AdminStackedInLine I guess it can be accomplished with JavaScript, playing with hidden forms and changing attributes on click events.
Anyone has experience doing something similar or can guide me through the correct direction? Also on how to manage the data and the relations of the submitted forms?
I think your problem is you have tried to validate a class Form instead of instanciate your formset and validate them.
Your code would be look like to something like that :
def anunciocreateview(request):
ProductFormSet = inlineformset_factory(Anuncio, Product, form=ProductForm)
ImageFormSet = inlineformset_factory(Product, Image, form=ImageForm)
anuncio_form = AnuncioForm(request.POST or None)
product_formset = ProductFormSet(request.POST or None)
image_formset = ImageFormSet(request.POST or None)
if all([form.is_valid(), product_formset.is_valid(), image_formset.is_valid()]):
...
The function inlineformset_factory just create a Form class, not a instance of form.
More information and example on the documentation : https://docs.djangoproject.com/fr/4.1/topics/forms/formsets/

Django - How to render a ModelForm with a Select field, specifying a disabled option?

I have the following models:
# Get or create a 'Not selected' category
def get_placeholder_categoy():
category, _ = ListingCategories.objects.get_or_create(category='Not selected')
return category
# Get default's category ID
def get_placeholder_category_id():
return get_placeholder_categoy().id
class ListingCategories(models.Model):
category = models.CharField(max_length=128, unique=True)
def __str__(self):
return f'{self.category}'
class Listing(models.Model):
title = models.CharField(max_length=256)
seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='listings')
description = models.TextField(max_length=5120, blank=True)
img_url = models.URLField(default='https://media.istockphoto.com/vectors/no-image-available-picture-coming-soon-missing-photo-image-vector-id1379257950?b=1&k=20&m=1379257950&s=170667a&w=0&h=RyBlzT5Jt2U87CNkopCku3Use3c_3bsKS3yj6InGx1I=')
category = models.ForeignKey(ListingCategories, on_delete=models.CASCADE, default=get_placeholder_category_id, related_name='listings')
creation_date = models.DateTimeField()
base_price = models.DecimalField(max_digits=10, decimal_places=2, validators=[
MinValueValidator(0.01),
MaxValueValidator(99999999.99)
])
With these, I have the following form:
class ListingForm(ModelForm):
class Meta:
model = Listing
exclude = ['seller', 'creation_date']
widgets = {
'title': TextInput(attrs=base_html_classes),
'description': Textarea(attrs=base_html_classes),
'img_url': URLInput(attrs=base_html_classes),
'category': Select(attrs=base_html_classes),
'base_price': NumberInput(attrs=base_html_classes)
}
One of the available categories I have is "Not selected", since I want to allow that if at some point a category were to be removed, items can be reassigned to that one, however, when rendering the form, I will do some validation on the view function to prevent it from being submitted if the "not selected" category is sent with the form.
Because of this, I want the HTML form on the template to assign the 'disabled' attribute to the option corresponding to that category, however, I have been searching for a couple of days now without finding anything that I was able to understand to the point where I could try it.
Ideally, another thing I'd like to achieve is to be able to modify the order of the rendered options on the form so that I can move to the top 'not selected' regardless of its primary key within the model.
I am aware I can just create a form instead of a model form, or just modify the template so I manually specify how to render the form itself, but I do feel like there is a simple fix to this either on the model or on the model form that I am just not finding yet.
Thanks in advance!
I would suggest you use (in model definition)
class Listing(models.Model):
..
category = model.ForeignKey(ListingCategories, on_delete=models.SET_NULL, null=True, related_name='listings')
..
and optionally in form definition
class ListingForm(ModelForm):
category = forms.ModelChoiceField(ListingCategories, empty_label='Not Selected')
..
While rendering model form, a required attribute will be automatically added, and in form validating, it is also required. It is only in database validation that the field can be left NULL

Showing Many-To-Many in different model's form in Django?

Say I have three models, a Professor model, a Course model, and a Review model. The user is allowed to make a Review, which reviews a Professor that teaches a certain Course.
I'm thinking of how to model the many to many relationship of Professor and Course, and how to reference that relationship in Review. My idea so far is to use models.ManyToMany to link Professor and Course.
Models.py (Prof)
class Prof(models.Model):
first_name = models.CharField(max_length = 20, unique = False)
last_name = models.CharField(max_length = 20, unique = False)
def __str__ (self):
return self.first_name + " " + self.last_name
class Course(models.Model):
name = models.CharField(max_length = 20, unique = True)
prof = models.ManyToManyField(Prof)
def __str__ (self):
return self.name
Models.py (Review)
class Review(models.Model):
message = models.TextField(max_length = 4000)
created_at = models.DateTimeField(auto_now_add = True)
updated_at = models.DateTimeField(null = True)
rating = models.IntegerField(
default = 5,
validators = [MaxValueValidator(5), MinValueValidator(0)]
)
prof = models.ForeignKey(Prof, related_name = 'reviews')
course = models.ForeignKey(Course, related_name = 'reviews')
user = models.ForeignKey(User, related_name = 'reviews')
def __str__ (self):
return self.message
forms.py
class ReviewForm(ModelForm):
rating = CharField(widget=TextInput(attrs={'type': 'number','value': 5, 'min': 0, 'max': 5}))
class Meta:
model = Review
fields = ['message', 'rating', 'prof', 'course', 'user']
This is my code so far for displaying the form
<h1>New Review</h1>
<form method="POST">
{% csrf_token %}
<p>{{ review_form.message }}</p>
<p>{{ review_form.rating }}</p>
<p>{{ review_form.prof }}</p>
<!-- The prof chosen's courses should be shown here -->
<button type="submit">Save</button>
</form>
Right now, forms.py shows all the objects under Course, and i'm not sure how to instead show the courses of a professor. Is it possible to filter the form after a prof is chosen from the drop down, to display the courses he/she teacher?
It sounds like you're going about this the right way. You haven't mentioned your urls.py structure yet, or views.py but the most straightforward way to do this is to display the courses by professor, taking the professor's id (or slug-field) in as a parameter - either in the URL (v straightforward) or as the output from a form on a previous page (and reload the template with a professor parameter) or in Ajax, depending on your appetite for shiny-new-things.
In your view, when you call the form, you can then do, along the lines from this answer -
form.courses.queryset = Course.objects.filter(professor__in=[professor.id,])
Note that I've put filtered on a list here, which only has one item - it does give you scope to expand, or to use a queryset for more complicated functions later.
Tweak as appropriate if you're using class-based views. :)

Django Model - Charfield with choices from a row in another model?

I have created a settings table in Django as per the below:-
class Settings(models.Model):
name = models.CharField(max_length=200)
class Meta:
verbose_name = "Settings"
verbose_name_plural = "Settings"
def __str__(self):
return self.name
class SettingChoices(models.Model):
setting = models.ForeignKey(Settings, on_delete=models.PROTECT)
choice = models.CharField(max_length=200)
class Meta:
verbose_name = "Setting Choices"
verbose_name_plural = "Setting Choices"
def __str__(self):
return '{0} - {1}'.format(self.setting, self.choice)
and a sample use of this would be:-
Setting = Circuit Type:
Choices:
DSL
4G
Fibre
then in another model I want to be able to reference this as set of choices
class Circuits(models.Model):
site_data = models.ForeignKey(SiteData, verbose_name="Site", on_delete=models.PROTECT)
order_no = models.CharField(max_length=200, verbose_name="Order No")
expected_install_date = models.DateField()
install_date = models.DateField(blank=True, null=True)
circuit_type = models.CharField(max_length=100, choices=*** here I would get model settings - Circuit Type - Choices ***)
currently I use a list in settings.py but its not fluid, I need my users to be able to alter these settings not for me to manually edit a list in settings.py and push changes each time
I attempted the below:
functions.py
def settings_circuit_types():
from home.models import SettingChoices
type_data = SettingChoices.objects.filter(setting__name='CIRCUIT_TYPES')
circuit_types = []
for c in type_data:
circuit_types.append(c.choice)
return circuit_types
models.py
from app.functions import settings_circuit_types
CIRCUIT_CHOICES = settings_circuit_types()
...
circuit_type = models.CharField(max_length=100, choices= CIRCUIT_CHOICES)
but this has thrown an error
dango.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.
which is understandable, im wondering if what im trying to achieve is possible by other means?
Thanks
So this is a better way of doing this as i mentioned it in comment section:
1 - You don't need Settings and SettingChoices. They are basically the same so you can combine them into one model called Setting:
class Setting(models.Model):
name = models.CharField(max_length=200)
# if you need anything that you might think you need another model for,
# just think this way, add a BooleanField.
# for example if you have a setting available only for admins:
only_admin = models.BooleanField(default=False)
# then when you're going to make a from, just filter the options;
# options = setting.objects.filter(only_admin=False)
class Meta:
verbose_name = "Settings"
verbose_name_plural = "Settings"
def __str__(self):
return self.name
2 - And for Circuits model you just need a simple ForeignKey field:
class Circuits(models.Model):
site_data = models.ForeignKey(SiteData, verbose_name="Site", on_delete=models.PROTECT)
order_no = models.CharField(max_length=200, verbose_name="Order No")
expected_install_date = models.DateField()
install_date = models.DateField(blank=True, null=True)
circuit_type = models.ForeignKey(Setting, null=False, blank=False)
Now when you want to make a form for users to fill:
forms.py:
class CircuitsForm(forms.ModelForm):
class Meta:
model = Circuits
fields = ('install_date', 'circuit_type') # or other fields.
# and to filter which choices are available to choose from:
def __init__(self, *args, **kwargs):
super(CircuitsForm, self).__init__(*args, **kwargs)
self.fields["circuit_type"].queryset = setting.objects.filter(only_admin=False)
This way you have a safe and easy way to make forms for both users and your admins.
You can edit the admin panel itself or just make a url just for admin users with a form like this.
Also if you aren't that kind of people who uses django to render their form you can simply get the available choices in your view and pass it to the template like this:
settings = setting.objects.filter(only_admin=False)
and render it in a template like this:
<select name="circuit_type">
{% for setting in settings %}
<option value="{{ setting.pk }}">{{ setting.name }}</option>
{% endfor %}
</select>
Now you will have only choices you want them to show up in form and even if user tries to mess with template code and add more options, form won't allow them to be accepted and it will raise an error for it.

Sum of fields for filtered queryset using django_filters

I have the following view
class AuthorList(FilterView):
model = Author
filterset_class = AuthorFilter
context_object_name = 'authors'
In the template, one of the field is {{ author.value }}, which is an integer.
What I would like to do is to show the sum of all {{ author.value }} in my template, but in a dynamic way (if some filters are used, the sum is updated with the current Queryset).
I have tried adding extra context with get_context_data but I couldn't find out how to make it in a dynamic way.
EDIT
tried this, still not working:
#property
def get_sum_values(self):
sum_values = self.objects.all().aggregate(Sum('value'))['value__sum']
return sum_values
and in the template: {{ authors.get_sum_values }}
I have also tried to add a print statement in the property, but nothing shows up, so I guess the function is not even loaded.
my models:
class Author(models.Model):
name = models.CharField(max_length=50, blank=True, null=True)
value = models.IntegerField(null=True, blank=True)
Have you tried doing the sum in the model as a function ?
#property
def wallet_amount_guests(self):
data_payments = self.user.earnings_set.filter(transaction_type='g').aggregate(Sum('amount'))['amount__sum']
if data_payments == None:
data_payments = 0
return data_payments
The above is just an example I have used before. You can then call in the html as blah.function_name

Categories