I am using django-cripy-forms and I want to make a field be readonly or not, depending on other things that happen in my code. My current code for a form looks like:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, Submit
from django import forms
from .models import Contact
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
exclude = ['author']
contact_email = forms.EmailField()
content = forms.CharField(widget=forms.Textarea(), required=True)
def __init__(self, email_readonly=False, *args, **kwargs):
super(ContactForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'contact-form'
self.helper.layout = Layout(
Field('contact_email', readonly=email_readonly),
Field('content'),
Submit('', 'Send')
)
But this always renders the contact email field as readonly.
In fact, I noticed that the only way that the field is not readonly is if the key readonly doesn't even appear in the Field constructor, i.e.
Field('contact_email', readonly=False),
has the exact same effect as
Field('contact_email', readonly=True),
while I would expect it to have the same effect as
Field('contact_email'),
Is this a bug or am I misusing the code?
Edit:
In my template, I have both the line
{% load crispy_forms_tags %}
and
{% crispy form %}
I am rendering the template with
render(request, 'main_site/contact.html', context={'form':ContactForm(email_readonly=False)})`
or
render(request, 'main_site/contact.html', context={'form':ContactForm(email_readonly=True)})`
with True and False being set somewhere else (I don't think that's relevant for the current question, as the current question only refers to the strange behaviour when email_readonly is set to False)
This is a known issue in Crispy Forms - see #326, and #257 which is the root of the issue.
The readonly attribute is a boolean attribute as opposed to a key-value attribute, i.e., you can use it like this:
<input name='foo' readonly>
The presence of the attribute means that the field is readonly.
Crispy forms does not handle such boolean attributes (with the exception of the required attribute which is has a special case for) and just renders them as it would any other attribute.
This is a problem because as far as your browser is concerned, readonly="true" and readonly="false" are the same thing. The mere presence of that attribute will cause the field to be read-only.
You could do something like this as a workaround:
self.helper.layout = Layout(
Field('contact_email', readonly=True) if email_readonly else Field('contact_email'),
)
Related
I am new to Django and have been doing lots of reading so perhaps this is a noob question.
We have applications that involve many forms that users fill out along the way. One user might fill out the budget page and another user might fill out the project description page. Along the way any data they input will be SAVED but NOT validated.
On the review page only data is shown and no input boxes / forms. At the bottom is a submit button. When the user submits the application I then want validation to be performed on all the parts / pages / forms of the application. If there are validation errors then the application can not be submitted.
My model fields are mostly marked as blank=True or null=True depending on the field type. Some fields are required but most I leave blank or null to allow the users to input data along the way.
Any advice on best practices or do not repeat yourself is greatly appreciated.
There is an app in django called form wizard. Using it you can split form submission process for multiple steps.
After a lot of learning, playing and reading I think I have figured a few things and will share them here. I do not know if this is right, however it is progress for me.
So first comes the models. Everything needs to accept blank or null depending on the field type. This will allow the end user to input data as they get it:
class exampleModel(models.Model):
field_1 = models.CharField(blank=True, max_length=25)
field_2 = models.CharField(blank=True, max_length=50)
.........
Then we create our model form:
from your.models import exampleModel
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column
class exampleForm(ModelForm):
class Meta:
model = exampleModel
fields = ('field_1','field_2')
def __init__(self, *args, **kwargs):
# DID WE GET A VALIDATE ARGUMENT?
self.validate = kwargs.pop('validate', False)
super(ExampleForm, self).__init__(*args, **kwargs)
# SEE IF WE HAVE TO VALIDATE
for field in self.fields:
if self.validate:
self.fields[field].required = True
else:
self.fields[field].required = False
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Column('field_1', css_class='col-lg-4 col-md-4'),
Column('field_2', css_class='col-lg-4 col-md-4')
)
)
def clean(self):
cleaned_data = super(ExampleForm, self).clean()
field_1 = cleaned_data.get('field1')
field_2 = cleaned_data.get('field2')
if self.validate and field_2 != field_2:
self.add_error('field_1', 'Field 1 does not match field2')
return cleaned_data
Here is the important part. I've learned a lot about forms and binding. As I mentioned I needed users to be able to fill out forms and not validate the data till the very end. This is my solution which helped me. I could not find a way to bind a form to the model data, so I created a function in my lib called bind_queryset_to_form which looks like this:
def bind_queryset_to_form(qs, form):
form_data = {}
my_form = form()
for field in my_form.fields:
form_data[field] = getattr(qs, field, None)
my_form = form(data=form_data, validate=True)
return my_form
The view:
from your.models import exampleModel
from your.form import exampleForm
from your.lib.bind_queryset_to_form import bind_queryset_to_form
from django.shortcuts import render, get_object_or_404
def your_view(request, pk):
query_set = get_object_or_404(exampleModel, id=pk)
context = dict()
context['query_set'] = query_set
# SAVE THE FORM (POST)
if request.method == 'POST':
form = exampleForm(request.POST, instance=query_set)
form.save()
context['form'] = form
# GET THE DATA.
if request.method == 'GET':
if request.session.get('validate_data'):
# BIND AND VALIDATE
context['form'] = bind_queryset_to_form(query_set, exampleForm)
else:
# NO BIND, NO VALIDATE
context['form'] = exampleForm(instance=query_set)
return render(request, 'dir/your.html', context)
The template:
{% load crispy_forms_tags %}
<div id="div_some_tab">
<form id="form_some_tab" action="{% url 'xx:xx' query_set.id %}" method="post">
{% crispy form form.helper %}
</form>
</div>
What does all the above allow?
I have many views with many data inputs. The user can visit each view and add data as they have it. On the review page I set the flag / session "validate_data". This causes the app to start validating all the fields. Any errors will all be displayed on the review page. When the user goes to correct the errors for the given view the bind_queryset_to_form(query_set, exampleForm) is called binding the form with data from the queryset and highlighting any errors.
I cut out a lot of the exceptions and permission to keep this as transparent as possible (the goat would hate that). Hope this idea might help someone else or someone else might improve upon it.
To achieve client-side validation making the user to fill out non-null fields before submitting, I use the following code:
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'
if field.required == True:
field.widget.attrs['required'] = ''
This translates to the following html in the template:
<input class="form-control" ........ required="">
Now, when I use formsets, the required HTML attribute does not appear in the tempalte. The question is, how do I make Django formsets inherit this required attribute from the original forms - if it's possible whatsoever?
MyFormSet = modelformset_factory(MyModel, fields=(...))
formset = MyFormSet(queryset = MyModel.objects.filter(...))
How about creating formset from MyForm?
MyFormSet = forms.formset_factory(MyForm)
After spending three hours, I've solved the issue by setting a custom form in modelformset_factory. Maybe it will be useful for someone else
MyFormSet = modelformset_factory(MyModel, MyForm)
formset = MyFormSet(queryset = MyModel.objects.filter(...))
Specifying MyForm effectively tells Django to inherit all widget attributes that you have once declared in the MyForm definition.
Using formset_factory is for some reasons a headache for me, primarily because it accepts values instead of querysets which means I have to bother about foreign key relationships.
I am customizing the RegistrationForm from django-registration-redux with django-crispy-forms. For that I have defined a FormHelper which is working fine:
class MyRegistrationForm(RegistrationForm):
def __init__(self, *args, **kwargs):
super(MyRegistrationForm, self).__init__(*args, **kwargs)
helper = self.helper = FormHelper()
# Moving field labels into placeholders
layout = helper.layout = Layout()
for field_name, field in self.fields.items():
layout.append(Field(field_name, placeholder=field.label))
helper.template_pack = 'bootstrap3'
helper.form_show_labels = False
My form is shown as I want: no labels, bootstrap3 and placeholders derived from label.
Now I would also like to suppress the help_text, which is coming from the Field definition. There is a somehow related flag here (help_text_inline), but that is not intended to disable the display of the help text. I can not find a flag to completely disable the display of the help text in the FormHelper documentation. Is this at all possible?
Removing the help text from the Field definition is not really an option, since I am inheriting the RegistrationForm and I do not want to modify it too much.
Does anybody know if there is a correct way to remove labels in a crispy form?
I got as far as this:
self.fields['field'].label = ""
But it's not a very nice solution.
Just do:
self.helper.form_show_labels = False
To remove all labels.
Works with Boostrap ( see documentation )
In your form :
from crispy_forms.helper import FormHelper
from django import forms
class MyForm(forms.Form):
[...]
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_show_labels = False
In your template:
<form method='POST' action=''>{% csrf_token %}
{% crispy form %}
<input type='submit' value='Submit' class='btn btn-default'>
</form>
You could edit the field.html template:
https://github.com/maraujop/django-crispy-forms/blob/dev/crispy_forms/templates/bootstrap/field.html#L7
Add a FormHelper attribute to your form that controls the label rendering and use it in that template if. Custom FormHelper attributes are not yet officially documented, because I haven't had time, but I talked about them in a keynote I gave, here are the slides:
https://speakerdeck.com/u/maraujop/p/django-crispy-forms
The solution below lets you remove a label from both a regular or crispy control. Not only does the label text disappear, but the space used by the label is also removed so you don't end up with a blank label taking up space and messing up your layout.
The code below works in django 2.1.1.
# this class would go in forms.py
class SectionForm(forms.ModelForm):
# add a custom field for calculation if desired
txt01 = forms.CharField(required=False)
def __init__(self, *args, **kwargs):
''' remove any labels here if desired
'''
super(SectionForm, self).__init__(*args, **kwargs)
# remove the label of a non-linked/calculated field (txt01 added at top of form)
self.fields['txt01'].label = ''
# you can also remove labels of built-in model properties
self.fields['name'].label = ''
class Meta:
model = Section
fields = "__all__"
I'm not clear what the problem the OP had with the code snippet he showed, except that he wasn't putting the line of code in the right place. This seems like the best and simplest solution.
if you are only to remove some labels from input, then explicitly don't give a label name in model definition, i.e:
field = models.IntegerField("",null=True)
To Remove All Labels:
self.helper.form_show_labels = False
To Show Specific Lable when all False :
HTML('<span>Your Label</span>')
To Disable Label for Specific field when all is True
self.fields['fieldName'].label = True
Example:
Row(
HTML('<span> Upolad Government ID (Adhar/PAN/Driving Licence)</span>'),
Column('IdProof',css_class='form-group col-md-12 mb-0'),
css_class='form-row'
),
I have this form field:
email = forms.EmailField(
required=True,
max_length=100,
)
It has the required attribute, but in the html it is not adding the html attribute required. In fact it's not even using email as the field type, it's using text... though it appears to get max_length just fine.
Actual:
<input id="id_email" type="text" name="email" maxlength="100">
Expected:
<input id="id_email" type="email" name="email" maxlength="100" required="true">
How can I get Django to use the correct attributes in html forms?
Django form elements are written against <input /> as it exists in HTML 4, where type="text" was the correct option for e-mail addresses. There was also no required="true".
If you want custom HTML attributes, you need the attrs keyword argument to the widget. It would look something like this:
email = forms.EmailField(
max_length=100,
required=True,
widget=forms.TextInput(attrs={ 'required': 'true' }),
)
You can check out more documentation about widgets here. Discussion of attrs is near the bottom of that page.
Regarding type="email", you might be able to send that to your attrs dictionary and Django will intelligently override its default. If that isn't the result you get, then your route is to subclass forms.TextInput and then pass it to the widget keyword argument.
Combining Daniel and Daniel answers, I usually use this mixin for my forms:
from django.contrib.admin.widgets import AdminFileWidget
from django.forms.widgets import HiddenInput, FileInput
class HTML5RequiredMixin(object):
def __init__(self, *args, **kwargs):
super(HTML5RequiredMixin, self).__init__(*args, **kwargs)
for field in self.fields:
if (self.fields[field].required and
type(self.fields[field].widget) not in
(AdminFileWidget, HiddenInput, FileInput) and
'__prefix__' not in self.fields[field].widget.attrs):
self.fields[field].widget.attrs['required'] = 'required'
if self.fields[field].label:
self.fields[field].label += ' *'
So when i have to create a new form or modelform i just use:
class NewForm(HTML5RequiredMixin, forms.Form):
...
Since Django 1.10, this is built-in.
From the release notes:
Required form fields now have the required HTML attribute. Set the new Form.use_required_attribute attribute to False to disable it.
There's also the template-only solution using a filter. I recommend django-widget-tweaks:
{% load widget_tweaks %}
{{ form.email|attr:'required:true' }}
That was easy.
Monkeypatching Widget is your best bet:
from django.forms.widgets import Widget
from django.contrib.admin.widgets import AdminFileWidget
from django.forms import HiddenInput, FileInput
old_build_attrs = Widget.build_attrs
def build_attrs(self, extra_attrs=None, **kwargs):
attrs = old_build_attrs(self, extra_attrs, **kwargs)
# if required, and it's not a file widget since those can have files
# attached without seeming filled-in to the browser, and skip hidden "mock"
# fileds created for StackedInline and TabbedInline admin stuff
if (self.is_required
and type(self) not in (AdminFileWidget, HiddenInput, FileInput)
and "__prefix__" not in attrs.get("name", "")):
attrs['required'] = 'required'
return attrs
Widget.build_attrs = build_attrs
As you've realized, setting your Field required attribute to True is only for backend validation, as explained in the Django documentation.
What you really want is to add a required attribute to the Widget of the field:
email.widget.attrs["required"] = "required"
But if you really want to write elegant, DRY code, you should make a base form class that dynamically looks for all your required fields and modifies their widget required attribute for you (you can name it whatever you wish, but "BaseForm" seems apt):
from django.forms import ModelForm
class BaseForm(ModelForm):
def __init__(self, *args, **kwargs):
super(BaseForm, self).__init__(*args, **kwargs)
for bound_field in self:
if hasattr(bound_field, "field") and bound_field.field.required:
bound_field.field.widget.attrs["required"] = "required"
And then have all your Form objects descend from it:
class UserForm(BaseForm):
class Meta:
model = User
fields = []
first_name = forms.CharField(required=True)
last_name = forms.CharField(required=True)
email = forms.EmailField(required=True, max_length=100)