Form is invalid but no errors - python

Whenever I submit the form, it is invalid and there is no error message attached to it when I try to read it with form.errors; it's empty. Here is what I have:
models.py
class Project(models.Model):
project = models.CharField(unique=True, max_length=50)
is_active = models.BooleanField(default=False)
forms.py
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column, Submit, Field
class SelectProjectForm(forms.Form):
def __init__(self, active_choices, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['is_active'] = forms.ChoiceField(choices=active_choices, widget=forms.Select)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
Row(
Column(Field('is_active'))
),
Row(
Column(FormActions(Submit('activate', 'Activate Project')))
),
)
views.py
class ProjectSettings(LoginRequiredMixin, TemplateView):
template_name = 'Home/project_settings.html'
def get(self, request, *args, **kwargs):
active_choices = []
for project in Project.objects.all():
active_choices.append((project.id, project.project),)
return render(request, self.template_name, {'form': SelectProjectForm(active_choices)})
def post(self, request, *args, **kwargs):
if 'activate' in request.POST:
form = SelectProjectForm(request.POST)
if form.is_valid():
....
messages.error(request, 'Something went wrong')
return redirect('project_settings')
project_settings.html:
<div>
{% load crispy_forms_tags %}
{% crispy form %}
</div>
I think the problem might be in the POST method in views where I initialize the form, but I don't know how to pass the active_choices parameter in post. If that is not the problem then I am lost.

Related

ValidationError ignored in Custom Django Crispy Form

I have a rather complex Django form that affects 3 models and part of which includes an inline formset. I found a nice solution to building the form at https://dev.to/zxenia/django-inline-formsets-with-class-based-views-and-crispy-forms-14o6. I extended that solution and added a third model in a similar way that the formset was added (using a custom Django Crispy Form and inserting it using the Crispy Forms Layout features).
My problem is that any validation errors raised on either of the two inserted forms (the formset and the small subform) are simply ignored - the main form posts correctly and raised ValidationErrors are displayed in the form as errors allowing the user to correct any mistakes and its data is correctly saved to the database. If the subform and formset are valid, their data gets saved correctly as well. However, if the data in the subform and formset is not valid, the form never shows the errors to give the user a chance to correct their mistake, and the data is simply ignored and never saved to the database - the main model's data saves fine though.
My question is, how do I get the form to refresh with errors displayed in the added subform and formset allowing the user to correct their mistakes?
Most of the code below is from the quite good post referenced above with a third model added
Models:
from django.db import models
from django.contrib.auth.models import User
class Collection(models.Model):
subject = models.CharField(max_length=300, blank=True)
owner = models.CharField(max_length=300, blank=True)
note = models.TextField(blank=True)
created_by = models.ForeignKey(User,
related_name="collections", blank=True, null=True,
on_delete=models.SET_NULL)
def __str__(self):
return str(self.id)
class CollectionTitle(models.Model):
"""
A Class for Collection titles.
"""
collection = models.ForeignKey(Collection,
related_name="has_titles", on_delete=models.CASCADE)
name = models.CharField(max_length=500, verbose_name="Title")
language = models.CharField(max_length=3)
Class CollectionTxn(models.Model):
"""
A Class for Collection transactions.
"""
collection = models.ForeignKey(Collection,
related_name="has_txn", on_delete=models.CASCADE)
number_received= models.IntegerField()
date_received= models.DateField()
class Meta:
'''
If 2 rows are entered with the same information, a validation error is raised, but it just
doesn't save the data at all instead of refreshing the form showing the error.
'''
unique_together = ('number_received', 'date_received')
forms.py:
from django import forms
from .models import Collection, CollectionTitle
from django.forms.models import inlineformset_factory
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Fieldset, Div, Row, HTML, ButtonHolder, Submit
from .custom_layout_object import Formset, Subform
import re
class CollectionTitleForm(forms.ModelForm):
class Meta:
model = CollectionTitle
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
formtag_prefix = re.sub('-[0-9]+$', '', kwargs.get('prefix', ''))
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Field('name'),
Field('language'),
Field('DELETE'),
css_class='formset_row-{}'.format(formtag_prefix)
)
)
CollectionTitleFormSet = inlineformset_factory(
Collection, CollectionTitle, form=CollectionTitleForm,
fields=['name', 'language'], extra=1, can_delete=True
)
class CollectionForm(forms.ModelForm):
class Meta:
model = Collection
exclude = ['created_by', ]
def __init__(self, *args, **kwargs):
super(CollectionForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = True
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3 create-label'
self.helper.field_class = 'col-md-9'
self.helper.layout = Layout(
Div(
Field('subject'),
Field('owner'),
Fieldset('Add titles',
Formset('titles')),
Field('note'),
Subform('transactions'),
HTML("<br>"),
ButtonHolder(Submit('submit', 'Save')),
)
)
class CollectionTxnForm(forms.ModelForm):
class Meta:
model = CollectionTxn
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['collection'].widget = HiddenInput()
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Field('number_received'),
Field('date_received'),
)
)
views.py:
from .models import *
from .forms import *
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy
from django.db import transaction
class CollectionCreate(CreateView):
model = Collection
template_name = 'mycollections/collection_create.html'
form_class = CollectionForm
success_url = None
def get_context_data(self, **kwargs):
data = super(CollectionCreate, self).get_context_data(**kwargs)
if self.request.POST:
data['titles'] = CollectionTitleFormSet(self.request.POST)
data['transactions'] = CollectionTrxForm(self.request.POST)
else:
data['titles'] = CollectionTitleFormSet()
data['transactions'] = CollectionTrxForm()
return data
def form_valid(self, form):
context = self.get_context_data()
titles = context['titles']
transactions = context['transactions']
with transaction.atomic():
form.instance.created_by = self.request.user
self.object = form.save()
if titles.is_valid():
titles.instance = self.object
titles.save()
if transactions.is_valid():
transactions.save()
return super(CollectionCreate, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('mycollections:collection_detail', kwargs={'pk': self.object.pk})
custom_layout_object.py
from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string
class Formset(LayoutObject):
template = "mycollections/formset.html"
def __init__(self, formset_name_in_context, template=None):
self.formset_name_in_context = formset_name_in_context
self.fields = []
if template:
self.template = template
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
formset = context[self.formset_name_in_context]
return render_to_string(self.template, {'formset': formset})
class SubForm(LayoutObject):
template = "mycollections/subform.html"
def __init__(self, subform_name_in_context, template=None):
self.subform_name_in_context = subform_name_in_context
self.fields = []
if template:
self.template = template
def render(self, subform, form_style, context, template_pack=TEMPLATE_PACK):
subform = context[self.subform_name_in_context]
return render_to_string(self.template, {'subform': subform})
formset.html
{% load crispy_forms_tags %}
{% load staticfiles %}
<style type="text/css">
.delete-row {
align-self: center;
}
</style>
{{ formset.management_form|crispy }}
{% for form in formset.forms %}
{% for hidden in form.hidden_fields %}
{{ hidden|as_crispy_field }}
{% endfor %}
{% crispy form %}
{% endfor %}
<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="{% static 'mycollections/libraries/django-dynamic-formset/jquery.formset.js' %}"></script>
<script type="text/javascript">
$('.formset_row-{{ formset.prefix }}').formset({
addText: 'add another',
deleteText: 'remove',
prefix: '{{ formset.prefix }}',
});
</script>
subform.html
{% load crispy_forms_tags %}
{% crispy subform %}
collection_create.html
{% extends "mycollections/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="container">
<div class="card">
<div class="card-header">
Create collection
</div>
<div class="card-body">
{% crispy form %}
</div>
</div>
</div>
{% endblock content %}
Basically, for the fields associated with the formset and subform added to the layout, validation errors are still raised, but they do not bubble up to the form level to show the errors, they are just ignored and the data is never saved. The "main" model works fine and validationerrors are displayed for its fields. If there is no invalid data, the main form, the subform, and the formset's data are all saved correctly. If there is invalid data in the formset or subform, the user never gets a chance to correct that data.
Any help as to where I would add the code needed so that if any invalid data entered in the formset or subform would cause the form to refresh displaying errors instead of just ignoring and not saving invalid data would be appreciated.
After much debugging and analyzing code, both my own and Crispy, I have solved my own problem. It was simply a matter of checking for the subform and formset validations, and if not valid, re-rendering the form.
Here is the new form_valid() method from the view that does this:
def form_valid(self, form):
context = self.get_context_data()
titles = context['titles']
transactions = context['transactions']
with transaction.atomic():
form.instance.created_by = self.request.user
if titles.is_valid() and transactions_is_valid():
self.object = form.save() #only save form if other subforms validate
titles.instance = self.object
# Any other field processing goes here
titles.save()
transactions.save()
else:
# If any subform or subformset is invalid, re-render the form showing errors
context.update({'titles': titles})
context.update({'transactions': transactions})
return self.render_to_response(context)
return super(CollectionCreate, self).form_valid(form)
With this, any clean methods in the formset and subform or if there are any other errors (for example if no two rows of the formset can be the same because unique_together() declared in model),the form will refresh showing all errors on all three combined form/formsets allowing the user to correct those errors.
Now - to do it with Ajax so the page does not refresh :)

Input field of django form not showing

I am fairly new to Django and am following the try django 1.10 series by CodingEntrepreneurs on youtube and therefore, am unable to solve the problem. I am only seeing the submit button, while the input field is not showing. Below is the code that I am working on.
forms.py
from django import forms
class SubmitUrlForm(forms.Form):
url = forms.CharField(label="Submit Url")
views.py
from .forms import SubmitUrlForm
def home_view_fbv(request, *args, **kwargs):
if request.method == 'POST':
print(request.POST)
return render(request, "app/home.html", {})
class HomeView(View):
def get(self, request, *args, **kwargs):
the_form = SubmitUrlForm()
context = {
"title": "Submit Url",
"form": the_form
}
return render(request, "app/home.html", context)
def post(self, request, *args, **kwargs):
form = SubmitUrlForm(request.POST)
if form.is_valid():
print(form.cleaned_data)
return render(request, "app/home.html", {})
app/home.html
<div style= 'width: 800px; margin: 0 auto;'>
<h1> {{ title }} </h1>
<form method = 'POST' action = '.'> {% csrf_token %}
{{form.as_p}}
<input type= 'submit' value= 'Shorten' >
</form>
</div>
models.py
from django.db import models
from .utils import code_generator, create_shortcode
from django.conf import settings
SHORTCODE_MAX = getattr(settings, "SHORTCODE_MAX", 15)
class surlShortManager(models.Manager):
def all(self, *args,**kwargs):
qs_main = super(surlShortManager,self).all(*args, **kwargs)
qs = qs_main.filter(active = True)
return as
def refresh_shortcodes(self, items= 100):
qs = surlShort.objects.filter(id__gte = 1)
if items is not None and isinstance(items, int):
qs = qs.order_by('-id')[:items]
new_codes = 0
for q in qs:
q.shortcode = create_shortcode(q)
print(q.id)
q.save()
new_codes += 1
return "New codes made: {i}".format(i = new_codes)
class surlShort(models.Model):
url = models.CharField(max_length = 500)
shortcode = models.CharField(max_length = SHORTCODE_MAX, unique = True, null
= False, blank = True)
updated = models.DateTimeField(auto_now = True)
timestamp = models.DateTimeField(auto_now_add = True)
active = models.BooleanField(default = True)
objects = surlShortManager()
def save(self, *args, **kwargs):
if self.shortcode is None or self.shortcode == '':
self.shortcode = code_generator()
super(surlShort, self).save(*args, **kwargs)
def __str__(self):
return str(self.url)
Change your HomeView to this
from django.views import View
from django.http import HttpResponseRedirect
from .forms import SubmitUrlForm
class HomeView(View):
form_class = SubmitUrlForm
context = {
"title": "Submit Url",
"form": form_class
}
template_name = 'app/home.html'
def get(self, request, *args, **kwargs):
return render(request, self.template_name, context)
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
print(form.cleaned_data)
return HttpResponseRedirect('/success/')
return render(request, self.template_name, context)

Django - Pass Session Variables From One View To Another ('request' is undefined)

I have looked at this (django variable of one view to another from session), but I believe the desired outcome is quite different.
I have two views in my views.py file: projectcreation and projectconfirm.
After the user fills out a form in the projectcreation view, I want them to be directed to a confirmation page that gives a read-only view of the variables before proceeding with the project creation.
My views.py file looks like this:
from django.shortcuts import render
from django.http import HttpResponse
from .projectform import ProjectForm
from .projectconfirm import ProjectConfirm
def projectcreation(request):
if request.method == 'POST':
form = ProjectForm(request.POST)
if form.is_valid():
request.session['projectname'] = form.cleaned_data['client'] + "-" + form.cleaned_data['stage'] + "-" + form.cleaned_data['purpose']
request.session['computeapi'] = form.cleaned_data['computeapi']
request.session['deploymentmanapi'] = form.cleaned_data['deploymentmanapi']
request.session['storagecompapi'] = form.cleaned_data['storagecompapi']
request.session['monitorapi'] = form.cleaned_data['monitorapi']
request.session['loggingapi'] = form.cleaned_data['loggingapi']
return render(request,'projectconfirm.html')
else:
form = ProjectForm()
return render(request, 'projectform.html', {'form': form})
def projectconfirm(request):
if request.method =='POST':
print("Now beginning deployment...")
else:
form = ProjectConfirm()
return render(request, 'projectconfirm.html', {'form': form})
The problem I'm facing and admittedly not understanding is how to load the session variables in the projectconfirm.py script.
I thought something like the following would work, but it's complaining that 'request' is an undefined variable:
from django import forms
from django.shortcuts import render
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Fieldset
class ProjectConfirm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': request.session['projectname']}))
computeapi = forms.CharField(widget=forms.TextInput(attrs={'placeholder': request.session['computeapi']}))
deploymentmanapi = forms.CharField(widget=forms.TextInput(attrs={'placeholder': request.session['deploymentmanapi']}))
storagecompapi = forms.CharField(widget=forms.TextInput(attrs={'placeholder': request.session['storagecompapi']}))
monitorapi = forms.CharField(widget=forms.TextInput(attrs={'placeholder': request.session['monitorapi']}))
loggingapi = forms.CharField(widget=forms.TextInput(attrs={'placeholder': request.session['loggingapi']}))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset(
'Project Name',
Row(
Column('name', css_class='form-group col-md-4 mb-0', readonly=True),
)
),
Fieldset(
'APIs To Enable',
Row(
Column('computeapi', css_class='form-group col-md-4 mb-0', readonly=True),
Column('deploymentmanapi', css_class='form-group col-md-4 mb-0', readonly=True),
Column('storagecompapi', css_class='form-group col-md-4 mb-0', readonly=True),
Column('monitorapi', css_class='form-group col-md-4 mb-0', readonly=True),
Column('loggingapi', css_class='form-group col-md-4 mb-0', readonly=True)
)
),
Submit('Deploy', 'Deploy', css_class='btn-success')
)
In constructor of Form request can be obtained by:
Passing it through **kwargs, so:
# in your view:
form = ProjectConfirm(request=request)
# in ProjectConfirm
class ProjectConfirm(forms.ModelForm):
name = forms.CharField(widget=forms.TextInput(attrs={}))
# etc
def __init__(self, *args, **kwargs):
request = kwargs.pop("request")
super().__init__(*args, **kwargs)
# ... and then define your widgets inside your __init__
self.fields['name'].widget.attrs['placeholder'] = request.session["projectname"]
# etc
By defining Form as a nested class of your view, but it has to be class-view instead of function.Then you can pass it to your form, still it's not an elegant solution as it's mixing views and forms in the same module. Anyway it will be something like that:
class YourView(FormView):
def get_form_class(self):
request = self.request
class ProjectConfirm(forms.Form):
# your existing form definition
return ProjectConfirm
Let me know if it's helpful for you.

Django form.is_valid() failing class based views - Form, SingleObject, DetailMixins

I have two apps, here we will call them blog and comments.
Comments has a Comment model. Blog has a blog Model. Comments has a CommentForm. Blog has a DetailView.
I want my CommentForm to appear on by Blog DetailView, so people can submit comments from the blog detail page.
The form renders OK - it makes a POST request, it redirects to get_success_url() but (I've added a couple of prints to views.py - see below) in testing in views.py to see if the form data is received I see the form.is_valid() path is not met, and I don't understand why.
I'm essentially trying to follow this, the 'alternative better solution':
https://docs.djangoproject.com/en/2.2/topics/class-based-views/mixins/#using-formmixin-with-detailview
blog/views.py
class CommentLooker(SingleObjectMixin, FormView):
template_name = 'blogs/blog_detail.html'
form_class = CommentForm
model = blog
def get_object(self):
#self.team = get_object_or_404(team, team_id=self.kwargs['team_id'])
#queryset_list = blog.objects.filter(team = self.team)
team_id_ = self.kwargs.get("team_id")
blog_id_ = self.kwargs.get("blog_id")
return get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
return super(CommentLooker, self).post(request, *args, **kwargs)
def get_success_url(self):
return reverse('blogs:teams')
class blogDisplay(View):
def get(self,request,*args,**kwargs):
view = blogFromteamContentView.as_view()
return view(request, *args, **kwargs)
def post(self,request,*args,**kwargs):
view = CommentLooker.as_view()
return view(request,*args,**kwargs)
class blogFromteamContentView(LoginRequiredMixin, DetailView):
model = blog
template_name = 'blogs/blog_detail.html'
# override get_object so we can use blog_id when we use this class in urls.py
# otherwise DetailViews expect 'pk' which defaults to the primary key of the model.
def get_object(self):
team_id_ = self.kwargs.get("team_id")
blog_id_ = self.kwargs.get("blog_id")
return get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
def get_context_data(self, **kwargs):
context = super(blogFromteamContentView, self).get_context_data(**kwargs)
team_id_ = self.kwargs.get("team_id")
blog_id_ = self.kwargs.get("blog_id")
# get the list of blogs for a given blog id and team id combination.
context['queryset'] = get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
# get and set things related to ability to associate comments to a blog.
initial_data = {
"content_type": blog.get_content_type,
"object_id": blog.blog_id
}
comments = blog.comments # uses the #property set in this class.
comment_form = CommentForm(self.request.POST or None, initial=initial_data)
if comment_form.is_valid():
print(comment_form.cleaned_data)
else:
print('invalido!')
context['comment_form'] = comment_form
return context
blog/models.py
class Blog(models.Model):
team= models.ForeignKey(Team, on_delete=CASCADE)
blog_id = models.AutoField(primary_key=True)
blog_name = models.CharField(
max_length=100, verbose_name='Blog Name')
blog/urls.py
path('teams/<int:team_id>/blogs/<int:blog_id>/', blog.blogDisplay.as_view(), name='detail'),
blog_detail.html
<div>
<p class="lead"> Comments </p>
<form method="POST" action="."> {% csrf_token %}
{{ comment_form}}
<input type="submit" value="Post Comment" class="btn btn-primary">
</form>
<hr/>
{% for comment in blog.comments.all %}
<blockquote class="blockquote">
<p>{{ comment.content }}</p>
<footer class="blockquote-footer"> {{ comment.user }} | {{ comment.timestamp|timesince }} ago </footer>
</blockquote>
<hr/>
{% endfor %}
comments/forms.py
from django import forms
class CommentForm(forms.Form):
content_type = forms.CharField(widget=forms.HiddenInput)
object_id = forms.IntegerField(widget=forms.HiddenInput)
parent_id = forms.IntegerField(widget=forms.HiddenInput, required=False)
content = forms.CharField(widget=forms.Textarea)
edit:
after using print(comment_form.errors): object_idEnter a whole number.
suggesting my initial_data might be the problem. In fact both content_type and object_id in my initial_data were problems. I was asking for blog.blog_id - I.e. using the class, not an instance. So I changed
get_context_data:
def get_context_data(self, **kwargs):
context = super(blogFromteamContentView, self).get_context_data(**kwargs)
team_id_ = self.kwargs.get("team_id")
blog_id_ = self.kwargs.get("blog_id")
# get the list of blogs for a given blog id and team id combination.
context['queryset_list_recs'] = get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
instance = get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
initial_data = {
"content_type": instance.get_content_type,
"object_id": blog_id_
}
and to my views.py:
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
comment_form = CommentForm(self.request.POST)
if comment_form.is_valid():
print('valido')
c_type = comment_form.cleaned_data.get("content_type")
content_type = ContentType.objects.get(model=c_type)
obj_id = comment_form.cleaned_data.get('object_id')
content_data = comment_form.cleaned_data.get("content")
new_comment, created = Comment.objects.get_or_create(
user = self.request.user,
content_type = content_type,
object_id = obj_id,
content = content_data
)
else:
print('postinvalido!')
return super(CommentLooker, self).post(request, *args, **kwargs)
This (inappropriate print statements aside) now appears to give intended behaviour.
after using print(comment_form.errors):
object_id
List item
Enter a whole number.
suggesting my initial_data might be the problem. In fact both content_type and object_id in my initial_data were problems. I was asking for blog.blog_id - I.e. using the class, not an instance. So I changed
get_context_data:
def get_context_data(self, **kwargs):
context = super(blogFromteamContentView, self).get_context_data(**kwargs)
team_id_ = self.kwargs.get("team_id")
blog_id_ = self.kwargs.get("blog_id")
# get the list of blogs for a given blog id and team id combination.
context['queryset_list_recs'] = get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
instance = get_object_or_404(blog, blog_id=blog_id_, team=team_id_)
initial_data = {
"content_type": instance.get_content_type,
"object_id": blog_id_
}
and to my views.py:
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
self.object = self.get_object()
comment_form = CommentForm(self.request.POST)
if comment_form.is_valid():
print('valido')
c_type = comment_form.cleaned_data.get("content_type")
content_type = ContentType.objects.get(model=c_type)
obj_id = comment_form.cleaned_data.get('object_id')
content_data = comment_form.cleaned_data.get("content")
new_comment, created = Comment.objects.get_or_create(
user = self.request.user,
content_type = content_type,
object_id = obj_id,
content = content_data
)
else:
print('postinvalido!')
return super(CommentLooker, self).post(request, *args, **kwargs)
This (inappropriate print statements aside) now appears to give intended behaviour. I'm unclear why an instance of CommentForm needs to be created inside the post method - it feels like I'm doing something wrong here.

Django: One model, different forms and widgets?

I try to find the best way to have a field with multiple content types.
What I've done so far is a Contact model with a contacttype CharField:
class Contact(models.Model):
CONTACT_TYPES = (
('email', 'Email'),
('phone', 'Phone'),
('address', 'Address'),
('facebook', 'Facebook'),
('linkedin', 'LinkedIn'),
('youtube', 'Youtube'),
('twitter', 'Twitter'),
('google', 'Google'),
)
teammember = models.ForeignKey(TeamMember)
description = models.CharField(max_length=100, null=True)
contacttype = models.CharField(max_length=100, choices= CONTACT_TYPES, default='email')
contact = models.TextField()
My goal is to let the user add different contact informations, which'll be listed on the profile page, but with only one model.
I was thinking about a class for each ModelForm:
class ContactForm(ModelForm):
def __init__(self, data, *args, **kwargs):
super(ModelForm, self).__init__(data, *args, **kwargs)
self.contacttype = ""
class Meta:
model = Contact
fields = ['description', 'contact']
widgets = {'contact': TextInput()}
def clean_contacttype(self):
return self.contacttype
class ContactEmailForm(ContactForm):
def __init__(self, data, *args, **kwargs):
super(ContactForm, self).__init__(data, *args, **kwargs)
self.contacttype = "email"
class Meta(ContactForm.Meta):
model = Contact
fields = ['description', 'contact']
widgets = {'contact': EmailInput()}
class ContactPhoneForm(ContactForm):
def __init__(self, data, *args, **kwargs):
super(ContactForm, self).__init__(data, *args, **kwargs)
self.contacttype = "phone"
class Meta(ContactForm.Meta):
model = Contact
fields = ['description', 'contact']
widgets = {'contact': TextInput()}
def clean_contact(self):
cleaned_data = super(ContactForm, self).clean()
contact = cleaned_data.get("contact")
# Perform some phone number validations
return contact
Then, in my view, I would choose the correct form depending on the request argument (ex: /contact/add/email or /contact/add/phone).
I'm trying to find the most elegant way to do this, so any help is welcome.
Thanks for reading.
if you want to use one html file, then this is one of solutions.
class YourView(TemplateView):
template_name = 'your_template.html'
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
if context['type'] == 'phone':
context['form'] = ContactPhoneForm
elif ...
if you set your view, then
<form action="{% url "your view" %}" enctype="multipart/form-data" method="POST">{% csrf_token %}
{% if form.contenttype == 'phone' %}
{% include "partials/contact_phone_form.html" with form=form %}
{% elif form.contenttype == 'email' %}
{% include "partials/contact_email_form.html" with form=form %}
{% else %}
do something.
{% endif %}
</form>
and, contact_phone_form.html and contact_email_form is looks like
anything.
<input name='phone'>
anything.
this solution is for one template, multi form.
if you want to do multi template for specific form, then you can use this.
class YourView(TemplateView):
template_name = 'your_template.html'
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
if context['type'] == 'phone':
template_name = 'phone_form_template.html'
elif context['type'] == 'email':
template_name = 'email_form_template.html'
...
I would use forms.Form depending on the request argument like:
/contact/add/?type=email or /contact/add/?type=phone
and so you can use it like (not tested code):
class ContactForm(forms.Form):
def __init__(self, *args, **kwargs):
super(CreateUserquestionnaireForm, self).__init__(*args, **kwargs)
if "email" in self.data:
... do some thing like change contact type or widget
if "phone" in self.data:
... do some thing like change contact type or widget

Categories

Resources