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
Related
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>
     
<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>
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)
...
how can i save many courses to the student table .I want to keep my design like this.This code is not saving the many to many field(courses) through AddStudentForm.It returns an error with courses variable.If i used CharField instead of ManyToManyField in models for courses then the code works perfectly,but when i use ManyToManyField then it is not working.
it throws courses when i used form.errors .If i didn't use form.errors then it doesn't give any error neither saves the data.
how can i save many courses to the student table .I want to keep my design like this.This code is not saving the many to many field(courses) through AddStudentForm.It returns an error with courses variable.
models.py
class Course(models.Model):
title = models.CharField(max_length=250)
price = models.IntegerField(default=0)
duration = models.CharField(max_length=50)
def __str__(self):
return self.title
class Student(models.Model):
name = models.CharField(max_length=100)
courses = models.ManyToManyField(Course)
email = models.EmailField()
image = models.ImageField(upload_to='Students',blank=True)
def __str__(self):
return self.name
forms.py
class AddStudentForm(forms.ModelForm):
# courses = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Course.objects.all())
class Meta:
model = Student
fields = ['name','courses','email','image']
def __init__(self, *args, **kwargs):
super(AddStudentForm, self).__init__(*args, **kwargs)
self.fields["courses"].widget = CheckboxSelectMultiple()
self.fields["courses"].queryset = Course.objects.all()
views.py
def addstudent(request):
courses = Course.objects.all()
if request.method == 'POST':
form = AddStudentForm(request.POST,request.FILES)
if form.is_valid():
student = form.save(commit=False)
course = form.cleaned_data['courses']
student.courses = course
student.save()
# student.courses.add(course)
# student.save_m2m()
# student.courses.set(course) # this method also didn't helped me
messages.success(request, 'student with name {} added.'.format(student.name))
return redirect('students:add_student')
else:
# messages.error(request,'Error in form.Try again')
return HttpResponse(form.errors) # this block is called and returns courses
else:
form = AddStudentForm()
return render(request,'students/add_student.html',{'form':form,'courses':courses})
add_student.html
<form action="{% url 'students:add_student' %}"
method="post"
enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<h5>Full Name <span class="text-danger">*</span>
</h5>
<div class="controls">
<input type="text" name="name" class="form-
control" > </div>
</div>
<div class="form-group">
<h5>Courses<span class="text-danger">*</span>
</h5>
<div class="controls">
{% for course in courses %}
<input name ="courses" type="checkbox" id="course-
{{course.id}}" value="{{course.title}}">
<label for="course-{{course.id}}">{{course.title}}
</label>
{% endfor %} # i think the problem is here.
</div>
</div>
<div class="form-group">
<h5>Email <span class="text-danger">*</span></h5>
<div class="controls">
<input type="text" name="email" class="form-
control" required> </div>
</div>
</div>
<div class="form-group">
<h5>Image <span class="text-danger">*</span></h5>
<div class="controls">
<input type="file" name="image" class="form-control" > </div>
</div>
<div class="text-xs-right">
<button type="submit" class="btn btn-info">Add</button>
</div>
</form>
You need to save first before you can assign m2m, the system needs the primary key of the Student model before it can insert into the m2m table.
if form.is_valid():
student = form.save(commit=False)
course = form.cleaned_data['courses']
student.save()
# this will save by itself
student.courses.set(course)
When the user selects the following option values--it POSTs successfully. However, I am trying to send an error message when the user clicks "submit" without selecting any. When it "submits" without selecting, it gives an error: MultiKeyValueDictKeyError: 'venue'. How do I code it properly for error messages to show when the user submits without selecting? I appreciate your thorough feedback!
views.py
def add(request):
if not 'user_id' in request.session:
return redirect('/chrisgrafil')
if request.method!='POST':
messages.error(request, 'Please select the following options')
return redirect('/dashboard')
else:
Ticket.objects.create(venue=request.POST['venue'], quantity=request.POST['quantity'], loop=request.POST['loop'], purchaser=User.objects.get(id=request.session['user_id']))
return redirect ('/confirmation')
dashboard.html
<form action="/add" method="POST">
{% csrf_token %}
<div class="text-center">
{% if messages %}
{% for message in messages %}
<div class="alert alert-danger p-2 pb-3">
<a class="close font-weight-normal initialism" data-dismiss="alert" href="#"><samp>×</samp></a>
{{message}}
</div>
{% endfor %}
{% endif %}
<label><strong>Location of Venue:</strong></label>
<select class="form-control" name="venue">
<option value="" selected disabled>Please select</option>
<option value="San Bernardino">San Bernardino</option>
<option value="Los Angeles">Los Angeles</option>
<option value="Riverside">Riverside</option>
</select>
</form>
models.py
class User(models.Model):
first_name=models.CharField(max_length=100)
last_name=models.CharField(max_length=100)
email=models.CharField(max_length=100)
password=models.CharField(max_length=100)
created_at=models.DateTimeField(auto_now_add=True)
updated_at=models.DateTimeField(auto_now=True)
class Ticket(models.Model):
venue=models.CharField(max_length=100)
quantity=models.PositiveIntegerField()
price=models.DecimalField(default=25.00, max_digits=5, decimal_places=2, null=True, blank=True)
loop=models.CharField(max_length=100)
purchaser = models.ForeignKey(User, related_name="purchases", on_delete=models.PROTECT)
created_at=models.DateTimeField(auto_now_add=True)
updated_at=models.DateTimeField(auto_now=True)
You should just add the required attribute to the <select> element. It is better to handle frontend issues on the frontend.
Example:
<select class="form-control" name="venue" required>
...
</select>
Also, it seems you are structuring your view incorrectly. I would still handle the error I showed you above, but for future reference you should structure your views like so:
def add(request):
if request.method == 'POST':
user_id = request.session.get('user_id', '')
if not user_id:
return redirect('/chrisgrafil')
purchaser = User.objects.get(id=user_id)
venue = request.POST.get('venue', '')
quantity = request.POST.get('quantity', '')
loop = request.POST.get('loop', '')
if not venue:
messages.error(request, 'Please select the following options')
return redirect('/dashboard')
Ticket.objects.create(venue=venue, quantity=quantity, loop=loop, purchaser=purchaser)
return redirect('/confirmation')
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()