Am I doing Django forms wrong? - django

I keep getting the feeling that I'm benefitting from maybe half of the features of Django forms, but suffering greatly at the other half of the "features."
Here's an interesting use case. I have a form that allows a user to edit their "profile." This contains a few objects, namely the following:
class UserProfile(models.Model):
default_address = models.ForeignKey("Address")
default_phone_number = models.ForeignKey("PhoneNumber")
class Address(models.Model):
name = models.CharField()
street_address = models.CharField()
street_address_2 = models.CharField()
city = models.CharField()
country = models.ForeignKey("locality.Country")
territory = models.ForeignKey("locality.Territory", blank=True, null=True)
postal_code = models.CharField()
class PhoneNumber(models.Model):
name = models.CharField()
number = models.CharField()
The "locality.*" models are from another project I wrote called django-locality, and are viewable here.
(I wrote django-locality as there simply wasn't a way of doing what I wanted at the time. I was looking to simply create this form, which included a country and a territory. As there wasn't anything that gave me database access to countries and their territories, I built something to do the job. I needed to allow users to select a country and only be able to select a territory for that country if the country had territories. Pretty simple, but it evidently hadn't been done before.)
So here's where things get a bit more complicated. My form edits django.contrib.auth.models.User's first_name and last_name fields, as well as creates or updates Address and PhoneNumber instances owned by the UserProfile class.
Validation gets really complicated really quickly. I need to make sure that 1. if a country has territories, a territory must be selected, and 2. if a territory is selected, it must belong to the selected country. Also, I ended up essentially providing a blank select control in my template, as territories have to be dynamically fetched based on the selected country. It would have been nice to simply have a form field like a "ModelOptgroupChoiceField" which would have allowed me to group my territories by their country's abbreviation, in a select control with optgroups for each country then filter these out in JavaScript, but whatever. I was able to at least get it working after much deliberation and experimentation.
Another complication in validation comes with validation of phone numbers and postal-codes: how am I supposed to validate them? Sure, django.contrib.localflavors provides controls, but provides basically no single auto-localizing control to drop in. I could write some crazy logic which would use an input country's abbreviation to look things up in the django.contrib.localflavors package and dynamically set my phone_number and postal_code fields in my form to the right values, but seriously? Do I need to go to a hack at that extreme of a length to get things working? I basically just gave up entirely on validation/formatting for these fields.
class ProfileEditForm(forms.Form):
default_error_messages = {
'invalid_territory': _("Please select a territory."),
'invalid_country': _("Please select a country."),
}
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=30)
street_address = forms.CharField(max_length=128)
street_address_2 = forms.CharField(max_length=128, required=False)
city = forms.CharField(max_length=128)
country = forms.ModelChoiceField(Country.objects.all().order_by('name'),
empty_label=u'', to_field_name='iso2')
territory = forms.ModelChoiceField(Territory.objects.all().order_by(
'country__name', 'name'), empty_label=u'', to_field_name='pk')
zipcode = forms.CharField(max_length=12)
phone_number = forms.CharField(max_length=16)
def __init__(self, *args, **kwargs):
if 'user' in kwargs:
user = kwargs['user']
del kwargs['user']
kwargs['initial'] = {
'first_name': user.first_name,
'last_name': user.last_name,
'street_address': user.profile.default_address.street_address
if user.profile.default_address != None else '',
'street_address_2': user.profile.default_address.street_address_2
if user.profile.default_address != None else '',
'city': user.profile.default_address.city
if user.profile.default_address != None else '',
'country': user.profile.default_address.country.iso2
if user.profile.default_address != None else None,
'territory': user.profile.default_address.territory.pk
if user.profile.default_address != None else None,
'zipcode': user.profile.default_address.postal_code
if user.profile.default_address != None else '',
'phone_number': user.profile.default_phone_number.number
if user.profile.default_phone_number != None else None,
}
super(ProfileEditForm, self).__init__(*args, **kwargs)
def clean(self):
territory = self.cleaned_data.get('territory', None)
country = self.cleaned_data.get('country', None)
if territory == None or Territory.objects.filter(country__id = country.pk,
pk=territory.pk).count() == 0:
self._errors['territory'] = self.error_class([
self.default_error_messages['invalid_territory']])
if territory != None:
del self.cleaned_data.territory
else:
self.cleaned_data['territory'] = Territory.objects.get(
country__id = country.pk, abbr = territory.abbr)
# format phone-number
if re.match(r'^\d{10}$', self.cleaned_data['phone_number']):
match = re.match(r'^(\d{3})(\d{3})(\d{4})$', self.cleaned_data[
'phone_number'])
self.cleaned_data['phone_number'] = "%s-%s-%s" % (match.group(1),
match.group(2), match.group(3))
return self.cleaned_data
If you think my form is a bit complicated, wait until you see my template in order to output things properly:
<form method="post" action="">
<fieldset>
{% csrf_token %}
<legend>Your Name</legend>
<div class="clearfix{% if form.first_name.errors %} error{% endif %}">
<label for="first_name_input">First Name</label>
<div class="input">
<input id="first_name_input" name="first_name" class="span5" type="text"{% if form.first_name.value %} value="{{form.first_name.value}}"{% endif %}></input>
{{ form.first_name.errors }}
</div>
</div>
<div class="clearfix{% if form.last_name.errors %} error{% endif %}">
<label for="last_name_input">Last Name</label>
<div class="input">
<input id="last_name_input" name="last_name" class="span5" type="text"{% if form.last_name.value %} value="{{form.last_name.value}}"{% endif %}></input>
{{ form.last_name.errors }}
</div>
</div>
</fieldset>
<div class="row">
<div class="span7">
<fieldset>
<legend>Your Address</legend>
<div class="clearfix{% if form.street_address.errors %} error{% endif %}">
<label for="street_address_input">Address Line 1</label>
<div class="input">
<input id="street_address_input" name="street_address" class="span5" type="text"{% if form.street_address.value %} value="{{form.street_address.value}}"{% endif %}></input>
{{ form.street_address.errors }}
</div>
</div>
<div class="clearfix{% if form.street_address_2.errors %} error{% endif %}">
<label for="street_address_2_input">Address Line 2</label>
<div class="input">
<input id="street_address_2_input" name="street_address_2" class="span5" type="text"{% if form.street_address_2.value %} value="{{form.street_address_2.value}}"{% endif %}></input>
{{ form.street_address_2.errors }}
</div>
</div>
<div class="clearfix{% if form.city.errors %} error{% endif %}">
<label for="city_input">City</label>
<div class="input">
<input id="city_input" name="city" data-placeholder="Your City" class="span5"{% if form.city.value %} value="{{form.city.value}}"{% endif %}></input>
{{ form.country.errors }}
</div>
</div>
<div class="clearfix{% if form.country.errors %} error{% endif %}">
<label for="country_input">Country</label>
<div class="input">
<select id="country_input" name="country" data-placeholder="Choose a Country..."
class="chzn-select span5"{% if form.country.value %} data-initialvalue="{{form.country.value}}"{% endif %}>
<option value=""></option>
{% for country in countries %}
<option value="{{country.abbr}}"{% if form.country.value == country.iso2 %} selected{% endif %}>{{country.name}}</option>
{% endfor %}
</select>
{{ form.country.errors }}
</div>
</div>
<div class="clearfix{% if form.territory.errors %} error{% endif %}">
<label for="territory_input">Territory</label>
<div class="input">
<select id="territory_input" name="territory" data-placeholder="Choose a State..."
class="chzn-select span5" {% if form.territory.value %} data-initialvalue="{{form.territory.value}}"{% endif %}>
<option value=""></option>
</select>
{{ form.territory.errors }}
</div>
</div>
<div class="clearfix{% if form.zipcode.errors %} error{% endif %}">
<label for="zipcode_input">Postal Code</label>
<div class="input">
<input id="zipcode_input" name="zipcode" class="span5" text="text"{% if form.zipcode.value %} value="{{form.zipcode.value}}"{% endif %}></input>
{{ form.zipcode.errors }}
</div>
</div>
</fieldset>
</div>
</div>
<fieldset>
<legend>Your Phone Number</legend>
<div class="clearfix{% if form.phone_number.errors %} error{% endif %}">
<label for="phone_input" text="text">Phone Number</label>
<div class="input">
<input id="phone_input" name="phone_number" class="span5" text="text"{% if form.phone_number.value %} value="{{form.phone_number.value}}"{% endif %}></input>
{{ form.phone_number.errors }}
</div>
</div>
</fieldset>
<div class="actions clearfix">
<input type="submit" class="btn primary" style="float:right" value="Save Changes"></input>
</div>
</form>
As if that's not enough, my view is likewise bloated and complicated:
#login_required
def profile_edit(request):
if request.method == "POST":
form = forms.ProfileEditForm(request.POST)
if form.is_valid() == True:
user = request.user
profile = user.profile
user.first_name = form.cleaned_data['first_name']
user.last_name = form.cleaned_data['last_name']
user.save()
address = profile.default_address or models.Address()
address.name = "Default" if address.name == None else address.name
address.street_address = form.cleaned_data['street_address']
address.street_address_2 = form.cleaned_data['street_address_2']
address.city = form.cleaned_data['city']
address.country = form.cleaned_data['country']
address.territory = form.cleaned_data['territory']
address.postal_code = form.cleaned_data['zipcode']
address.user_profile = profile
address.save()
phone_number = profile.default_phone_number or models.PhoneNumber()
phone_number.name = "Default" if phone_number.name == None else phone_number.name
phone_number.number = form.cleaned_data['phone_number']
phone_number.user_profile = profile
phone_number.save()
profile.default_address = address
profile.default_phone_number = phone_number
profile.save()
return redirect("/me/profile/")
else:
form = forms.ProfileEditForm(user=request.user)
return dto(request, "desktop/profile/edit.html", {"form": form,
"countries": Country.objects.all().order_by('name'),
"territories": Territory.objects.all().order_by('country__iso2')})
All-in-all, it's taken well over 12 hours to write this form, excluding the amount of time I've spent working on django-locality.
This seems just wrong to me. I was convinced when I was introduced to Django that it would speed up my development tenfold. Somehow, I'm a little less than impressed. Surely, I must be doing something terribly wrong here. Am I doing Django forms wrong?

I think this would make for an excellent wiki discussion.
Validation gets really complicated really quickly. I need to make sure
that 1. if a country has territories, a territory must be selected,
and 2. if a territory is selected, it must belong to the selected
country. Also, I ended up essentially providing a blank select control
in my template, as territories have to be dynamically fetched based on
the selected country. It would have been nice to simply have a form
field like a "ModelOptgroupChoiceField" which would have allowed me to
group my territories by their country's abbreviation, in a select
control with optgroups for each country then filter these out in
JavaScript, but whatever. I was able to at least get it working after
much deliberation and experimentation.
When I ran into this problem, I used client side validation with javascript to solve the "if this selected, then make sure that is selected" problem.
As for grouping, I usually employ the multiselect widget from jquery.
Another complication in validation comes with validation of phone
numbers and postal-codes: how am I supposed to validate them? Sure,
django.contrib.localflavors provides controls, but provides basically
no single auto-localizing control to drop in. I could write some crazy
logic which would use an input country's abbreviation to look things
up in the django.contrib.localflavors package and dynamically set my
phone_number and postal_code fields in my form to the right values,
but seriously? Do I need to go to a hack at that extreme of a length
to get things working? I basically just gave up entirely on
validation/formatting for these fields.
For pre-filling/masking fields, use javascript; and for lookups, use ajax calls. It is a lot easier that way.
As for back end validation; I find that custom fields and validators go a long way.
django-uni-form is an elegant approach to form rendering that will clean up your templates somewhat.

Do you really need one huge form for your three models? What about three seperate forms?
Why not to use ModelForm to generate forms from models? You won't need to set initials manually.
You can place phone validation in validator. I don't think you can get rid of territory validation but it's better to place in model's clean.
You template can be rewritten to be more DRY. Use custom template tag (inclusion one) to output form. Or is there a problem with it?
Using ModelForm will make your view more clean. You will just need to override it save methods sometimes. It should look like this.
#login_required
def profile_edit(request):
user_form = forms.UserForm(request.POST or None, prefix='user', instance=request.user)
address_form = forms.AddressForm(request.POST or None, prefix='address', instance=request.user.profile.default_address)
phone_form = forms.PhoneForm(request.POST or None, prefix='phone', instance=request.user.profile.default_phone_number)
if user_form.is_valid() and address_form.is_valid() and \
phone_form.is_valid():
user = user_form.save()
address = address_form.save(commit=False)
address.user_profile = user.profile
address.save()
phone_number = phone_form.save(commit=False)
phone_number.user_profile = user.profile
phone_number.save()
user.profile.default_address = address
user.profile.default_phone_number = phone_number
user.profile.save()
return redirect("/me/profile/")
return dto(request, "desktop/profile/edit.html", {"form": form,
"countries": Country.objects.all().order_by('name'),
"territories": Territory.objects.all().order_by('country__iso2')})
I don't think your model organization is good. Why not to add default field to Phone and Address? Something like this (for phone).
class UserProfile(models.Model):
... your fields here ...
#property
def default_phone(self):
return self.phones.filter(default=True)[0]
class PhoneNumber(models.Model):
profile = models.ForeignKey(UserProfile, related_name='phones')
default = models.BooleanField()
name = models.CharField()
number = models.CharField()

Related

Django - In a ModelForm, display data coming from a model linked with M2M relationship

I would like to add information in a form, coming from the model linked with a M2M relationship to the model I'm updating.The form works properly, but I'm not able to add any information.
Here is what I get:
Here is the expected result:
My solution: finally, I updated __str__() mthod in UserGroup model to display what I expect (but, at this stage, I lost the dynamic part and my view does not work anymore :-/)
The main model is Event and it's linked to Groups thanks to this relationship; in the form, all groups are listed and displayed with checkboxes, but I'm only able to display the groups' name, no other information.
It looks like I miss some data / information: the group name is displayed only because I use {{grp}}} (see below) but it has no attribute / filed available, even if it is initialized with a query from the group model.
I envisaged a workaround (see below) because my first tries made me consider this kind of solution, but I'm not able to reproduce what I did :-/
Any idea of what I did wrong, or any advice to manage this? Thanks in advance.
Here are related code parts.
Models:
class UserGroup(models.Model):
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
users = models.ManyToManyField(UserComp, verbose_name="utilisateurs", blank=True)
group_name = models.CharField("nom", max_length=100)
weight = models.IntegerField("poids", default=0)
hidden = models.BooleanField(default=False)
def __str__(self):
return self.group_name
class Event(models.Model):
company = models.ForeignKey(
Company, on_delete=models.CASCADE, verbose_name="société"
)
groups = models.ManyToManyField(UserGroup, verbose_name="groupes", blank=True)
rules = [("MAJ", "Majorité"), ("PROP", "Proportionnelle")]
event_name = models.CharField("nom", max_length=200)
event_date = models.DateField("date de l'événement")
slug = models.SlugField()
current = models.BooleanField("en cours", default=False)
quorum = models.IntegerField(default=33)
rule = models.CharField(
"mode de scrutin", max_length=5, choices=rules, default="MAJ"
)
class Meta:
verbose_name = "Evénement"
constraints = [
models.UniqueConstraint(fields=["company_id", "slug"], name="unique_event_slug")
]
def __str__(self):
return self.event_name
Form:
class EventDetail(forms.ModelForm):
groups = forms.ModelMultipleChoiceField(
label = "Liste des groupes",
queryset = None,
widget = forms.CheckboxSelectMultiple,
required = False
)
class Meta:
model = Event
fields = ['event_name', 'event_date', 'quorum', 'rule', 'groups']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get('instance', None)
self.fields['groups'].queryset= UserGroup.objects.\
filter(company=instance.company).\
order_by('group_name')
View:
#user_passes_test(lambda u: u.is_superuser or (u.id is not None and u.usercomp.is_admin))
def adm_event_detail(request, comp_slug, evt_id=0):
'''
Manage events creation and options
'''
company = Company.get_company(request.session['comp_slug'])
# all_groups = list(UserGroup.objects.filter(company=company, hidden=False).order_by('group_name').values())
if evt_id > 0:
current_event = Event.objects.get(id=evt_id)
event_form = EventDetail(request.POST or None, instance=current_event)
else:
event_form = EventDetail(request.POST or None)
event_form.fields['groups'].queryset = UserGroup.objects.\
filter(company=company, hidden=False).\
order_by('group_name')
if request.method == 'POST':
if event_form.is_valid():
if evt_id == 0:
# Create new event
event_data = {
"company": company,
"groups": event_form.cleaned_data["groups"],
"event_name": event_form.cleaned_data["event_name"],
"event_date": event_form.cleaned_data["event_date"],
"quorum": event_form.cleaned_data["quorum"],
"rule":event_form.cleaned_data["rule"]
}
new_event = Event.create_event(event_data)
else:
new_event = event_form.save()
else:
print("****** FORMULAIRE NON VALIDE *******")
print(event_form.errors)
return render(request, "polls/adm_event_detail.html", locals())
HTML (I did not put each parts of the 'accordion' widget, I do not think they have anything to do with the problem):
{% if evt_id %}
<form action="{% url 'polls:adm_event_detail' company.comp_slug evt_id %}" method="post">
{% else %}
<form action="{% url 'polls:adm_create_event' company.comp_slug %}" method="post">
{% endif %}
{% csrf_token %}
<!-- Hidden field where the referer is identified to go back to the related page after validation -->
<input type="hidden" name="url_dest" value="{{ url_dest }}" />
<br>
<!-- Accordion -->
<div id="eventDetails" class="accordion shadow">
<div class="card">
<div class="card-header bg-white shadow-sm border-0">
<h6 class="mb-0 font-weight-bold">
Evénement
</h6>
</div>
<div class="card-body p-5">
<p>Nom : {{event_form.event_name}} </p>
<p>Date : {{event_form.event_date}} </p>
</div>
</div>
<!-- Accordion item 2 - Event's groups -->
<div class="card">
<div id="headingTwo" class="card-header bg-white shadow-sm border-0">
<h6 class="mb-0 font-weight-bold">
<a href="#" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo" class="d-block position-relative collapsed text-dark collapsible-link py-2">
Groupes d'utilisateurs
</a>
</h6>
</div>
<div id="collapseTwo" aria-labelledby="headingTwo" data-parent="#eventDetails" class="collapse show">
<div class="card-body p-5">
<p>Sélectionnez le(s) groupe(s) d'utilisateurs participants à l'événement :</p>
<ul>
{% for grp in event_form.groups %}
<li>{{ grp }}
{{ grp.weight }}
{{ grp.hidden }}
{{ grp.nb_users }}
</li>
{% endfor %}
</ul>
<p></p>
</div>
</div>
</div>
</div> <!-- Accordion end -->
<button class="btn btn-success mt-5" type="submit">{% if evt_id %}Mettre à jour{% else %}Créer{% endif %}</button>
&nbsp &nbsp &nbsp
<a class="btn btn-secondary back_btn mt-5" href="*">Annuler</a>
<div class="row">
<div hidden>
<!-- List of groups in event -->
{{ event_form.group_list }}
</div>
</div>
</form>
Workaround
If it's not possible to achieve this directly, I thought to a workaround that would be implemented in several parts:
Create a list almost like the queryset: group_list = UserGroup.objects.filter(company=instance.company).order_by('group_name').values()
I already know I can display each group with its details and a checkbox
on client side (javascript), I manage an hidden list that would be part of the form, with the ID of each selected group. That means that the list will be dynamically updated when a box is checked on unchecked
on the POST request, read the list to update the group attribute of updated event.
I would have prefered the users actions having effect directly to the form, but I know this could work
You're accessing the form's groups field, not the model instance. The form field doesn't have any relationship to other models, it's just a field. You can access the underlying instance of the model form using form.instance.
Also note that you get a relationship manager object when querying related models. Hence, use .all to query all groups.
Try
<ul>
{% for grp in event_form.instance.groups.all %}
<li>{{ grp }}
{{ grp.weight }}
{{ grp.hidden }}
{{ grp.nb_users }}
</li>
{% endfor %}
</ul>

Django objection creation failing due to FOREIGN KEY constraint failure (IntegrityError )

My website has a comment section where user can leave comments on a product . The comments on the product page will be stored in a model called 'ProductReview'. Here is the code for the model :
class ProductReview(models.Model):
product = models.ForeignKey(Product, related_name='reviews', on_delete=models.CASCADE)
name = models.CharField(blank=True,max_length=20)
stars = models.IntegerField()
content = models.TextField(blank=True)
date_added = models.DateTimeField(auto_now_add=True)
created_by = models.OneToOneField(User, on_delete=models.CASCADE)
Now the view associated with the model are as follows Note:The entire view isnt relevant to the error. The part relevant to the saving comment is the second 'request.POST' which I have denoted with a python
comment using # :
def product(request, category_slug, product_slug):
cart = Cart(request)
product = get_object_or_404(Product, category__slug=category_slug, slug=product_slug)
if request.method == 'POST':
form = AddToCartForm(request.POST)
if form.is_valid():
quantity = form.cleaned_data['quantity']
cart.add(product_id=product.id, quantity=quantity, update_quantity=False)
messages.success(request, 'The product was added to the cart')
return redirect('product', category_slug=category_slug, product_slug=product_slug)
similar_products = list(product.category.products.exclude(id=product.id))
# this part is for saving of the user comments to productreview model
if request.method == 'POST':
stars = request.POST.get('stars', 3)
content = request.POST.get('content', '')
name = request.POST.get('name', '')
created_by = request.user
review = ProductReview.objects.create(product=product, name=name, stars=stars, content=content, created_by=created_by)
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
# this marks the end of the code relevant to saving the user comment
if len(similar_products) >= 4:
similar_products = random.sample(similar_products, 4)
user_type = 0
if request.user.is_authenticated:
user_type = UserType.objects.filter(created_by=request.user.id).values_list('user_type', flat=True)
user_type = int(user_type[0])
return render(request, 'product.html', {'product': product, 'similar_products': similar_products, 'user_type': user_type})
And finally the relevant part of the template 'product.html' which is referenced in the view
{% if request.user.is_authenticated %}
{% if user_type == 2 %}
<div class="notification space-below">
<form method="post" action=".">
{% csrf_token %}
<div class="field">
<label>Name</label>
<div class="control">
<input class="text" name="name" value="{{ request.user }}" readonly>
</div>
</div>
<div class="field">
<label>Stars</label>
<div class="control">
<div class="select">
<select name="stars">
<option value="1">1</option>
<option value="2">2</option>
<option value="3" selected>3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
</div>
</div>
<div class="field">
<label>Content</label>
<div class="control">
<textarea class="textarea" name="content"></textarea>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-success">Submit</button>
</div>
</div>
</form>
</div>
{% else %}
<div>sign in with a buyer account to leave review</div>
{% endif %}
{% endif %}
Now when I try to fill the form in the product.html page and try to submit it ,I get the following error:
FOREIGN KEY constraint failed
IntegrityError at /smartwatch/apple-watch/
Can anyone tell what exactly is the issue with my code?
Pretty sure this all comes to your model, especially to this single line:
created_by = models.OneToOneField(User, on_delete=models.CASCADE)
You are using OneToOneField. This means a single user will be able to leave a single review in your entire application. You are getting integrity errors probably because this user you've selected already has a review on a different product.
If this isn't what you want, use normal ForeignKey instead.
I think your intention was to ensure that a single user can leave only one review per product. In that case, you should try setting UniqueConstraint between the product and created_by fields

Customizing (style) ModelMultipleChoiceField in a on ManyToManyFields in Django

Am trying to customize my checkbox inputs to look like this [what i want to archive]
so i tried this...
profile.html
<ul class="wt-accountinfo">
{% for key, value in form.interests.field.choices %}
<li>
<div class="wt-on-off pull-right">
<input type="checkbox" id="{{ value }}" value="{{ key }}" name="interests">
<label for="{{ value }}"><i></i></label>
</div>
<span>{{ value | title }}</span>
</li>
{% endfor %}
</ul>
which renders the html fine but highlight the select fields from the database
but using {{ form.interest }} highlights the selected checked boxes from the database
here is the forms.py
class ProfileForm(forms.ModelForm):
interests = forms.ModelMultipleChoiceField(
queryset=JobsCategories.objects.all(), widget=forms.CheckboxSelectMultiple(),
required=False
)
class Meta:
model = Profile
fields = ['interests']
and here is the models.py
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE,
interests = models.ManyToManyField(Categories, related_name='interests', null=True, blank=True)
def __str__(self):
return f'{self.user.first_name} {self.user.last_name}'
in the views.py
def dashboard_profile(request):
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=request.user.profile)
account_form = AccountForm(request.POST, instance=request.user)
if form.is_valid() and account_form.is_valid():
f_interests = form.save(commit=False)
for i in request.POST.getlist('interest'):
f_interests.interest.update(i)
f_interests.save()
form.save_m2m()
account_form.save()
return redirect('index')
else:
form = ProfileForm(instance=request.user.profile)
account_form = AccountForm(instance=request.user)
context = {
'form': form,
'account_form': account_form,
}
return render(request, 'dashboard_profile.html', context)
NOTE!!! if i select the options i want and click save, it saves the options i checked to the database
this is it
this is it in the admins section
admin section
admin section 2
and also when i use {{ form.interests }} in the the template it renders fine and highlights the checked option from the database but its not styled
[how it looks like when i use {{ form.interests }}]
i know am missing somtehing in the profile.html so please help me out Thanks.
You're missing logic within your input tag to apply the existing value of the field choice.
<ul class="wt-accountinfo">
{% for key, value in form.interests.field.choices %}
<li>
<div class="wt-on-off pull-right">
<input type="checkbox"
id="{{ value }}"
value="{{ key }}"
name="interests"
{% if value %}checked{% endif %}>
<label for="{{ value }}"><i></i></label>
</div>
<span>{{ value | title }}</span>
</li>
{% endfor %}
</ul>

How to create a django formset or form add/edit page in order to edit several records

For one of my open source projects, I need to create ONE add/edit page in order to make possible to edit several records with one save.
The repo is an IMDB clone formed for learning purpose. A user can add her/his favorite genres in her/his profile. Then an edit page is formed to show the list of those favored genres and the movies within that genre. (A for loop here) User can add notes, watch list options and so on to those movies. (NOT a FORMSET)
However, the code doesn't work as expected. The page cannot be saved and only the first checkbox of the list can be changed.
There is no error.
NOTE:
You can install repo with dummy data.
(https://github.com/pydatageek/imdb-clone)
Then after logging in, select your favorite genres. (http://localhost:8000/users/profile/)
Then (I wish it can be solved here) you can see the movies with your selected genres. Add notes, to watch list... (http://localhost:8080/users/profile/movies2/)
# users/templates/user-movies-with_loop.html
{% extends 'base.html' %}{% load crispy_forms_tags %}
<!-- Title -->
{% block htitle %}Your movies from favorite genres{% endblock %}
{% block title %}Your movies from favorite genres{% endblock %}
{% block content %}
<div class="card card-signin">
{% include 'users/profile-menu.html' %}
<h3 class="card-title text-center my-4">Take notes for your movies <small></small></h3>
<hr class="mb-1">
<div class="card-body">
<form method="POST">
{% csrf_token %}
{% for genre in user.genres.all %}
<h2 for="genre" name="genre" value="{{ genre.id }}">{{ genre.name }}</h2>
{% for movie in genre.movies.all %}
<div class="ml-5">
<h4>{{ movie.title }}</h4>
{{ form|crispy }}
</div>
<input type="hidden" name="user" value="{{ user.id }}">
<input type="hidden" name="movie" value="{{ movie.id }}">
{% empty %}
<p class="alert alert-danger">The genre you have selected on your profile doesn't have any movies!</p>
{% endfor %}
{% empty %}
<p class="alert alert-danger">You should select genres with movies from your profile to edit here!</p>
{% endfor %}
<input class="btn btn-lg btn-primary btn-block text-uppercase" type="submit" value="Submit">
</form>
</div>
</div>
{% endblock %}
# users.forms.py
...
class UserMovieFormWithLoop(ModelForm):
genre = forms.HiddenInput(attrs={'disabled': True})
class Meta:
model = UserMovie
fields = ('user', 'movie', 'note', 'watched', 'watch_list')
widgets = {
'user': forms.HiddenInput,
'movie': forms.HiddenInput,
'watched': forms.CheckboxInput(),
}
...
# users.models.py
...
class UserMovie(models.Model):
"""
Users have notes about their favorite movies.
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
movie = models.ForeignKey('movies.Movie', default=1, on_delete=models.CASCADE)
note = models.CharField(max_length=250, null=True, blank=True)
watched = models.BooleanField(default=False, verbose_name='Have you seen before?')
watch_list = models.BooleanField(default=False, verbose_name='Add to Watch List?')
def __str__(self):
return f'{self.user.username} ({self.movie.title})'
...
# users.views.py
...
class UserMovieViewWithLoop(LoginRequiredMixin, CreateView):
model = UserMovie
template_name = 'users/user-movies-with_loop.html'
form_class = UserMovieFormWithLoop
success_message = 'your form has been submitted.'
success_url = reverse_lazy('users:user_movies2')
def form_valid(self, form):
user = self.request.user
movie_counter = Movie.objects.filter(genres__in=user.genres.all()).count()
f = form.save(commit=False)
f.user = user
for i in range(movie_counter):
f.pk = None
f.save()
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super(UserMovieViewWithLoop, self).get_context_data(**kwargs)
context['form'] = self.form_class
return context
def get_object(self):
user = self.request.user
return UserMovie.objects.get(user=user)
...

Why is Django widgets for TimeInput not showing

I'm trying to create a TimeInput field in a form and noticed that the widget isn't showing correctly. But when I check the localhost:8000/admin, I see the widget showing up correctly.
My code is as follows. For models.py,
class TimeLimit(models.Model):
before = models.TimeField(blank=True, default=time(7, 0)) # 7AM
after = models.TimeField(blank=True, default=time(23, 0)) # 11PM
For views.py,
class UpdateTimeLimitView(LoginRequiredMixin, FormView):
model = TimeLimit
template_name = 'accounts/update_time_limit.html'
form_class = UpdateTimeLimitForm
def get_success_url(self):
return reverse_lazy('accounts:user_profile') + '?username=' + self.request.GET['username']
def get_context_data(self, **kwargs):
data = super(UpdateTimeLimitView, self).get_context_data(**kwargs)
data['username'] = self.request.GET['username']
return data
For forms.py,
class UpdateTimeLimitForm(forms.Form):
time_error = {'required': 'This field is required.',
'invalid': 'Please enter valid Hour:Minute values.'}
before = forms.TimeField(widget=forms.TimeInput(format='%H:%M'))
after = forms.TimeField(widget=TimeInput(format='%H:%M'))
class Meta:
model = TimeLimit
Finally, the relevant part for fields in update_time_limit.html,
<div class="container">
<form method="post">
{% csrf_token %}
<p>
{% for field in form %}
{{ field.errors }}
<label for="{{ field.id_for_label }}">{{ field.label }}({{ field.help_text }}):</label>
<br />
{{ field }}<br /><br /> and
{% endfor %}
</p>
<input class="btn btn-primary done-btn" type="submit" value="Update Time Limit">
</form>
</div>
Is there anything that I'm missing or doing wrong? Thank you.
The Django admin uses AdminTimeWidget to display time fields, not the TimeInput widget that you are using in your code.
There isn't a documented way to reuse the AdminTimeWidget outside of the Django admin. Getting it to work is very hacky (see the answer on this question, which is probably out of date), so it's probably better to use a different widget.
convert datetime.time(7, 0) to string work for me.
data['before'] = data['before'].strftime('%H:%M:%S')