Django and fieldsets on ModelForm - django

I know you can specify fieldsets in django for Admin helpers. However, I cannot find anything useful for ModelForms. Just some patches which I cannot use. Am I missing something? Is there a way I could achieve something like fieldsets without manually writing out each field on my template in the appropriate tag.
I would ideally like to iterate through a set of BoundFields. However, doing something like this at the end of my ModelForm:
fieldsets = []
fieldsets.append(('Personal Information',
[username,password,password2,first_name,last_name,email]),) # add a 2 element tuple of string and list of fields
fieldsets.append(('Terms & Conditions',
[acceptterms,acceptprivacy]),) # add a 2 element tuple of string and list of fields
fails as the items contained in my data structure are the raw fields, not the BoundFields. t looks like BoundFields are generated on the fly... this makes me sad. Could I create my own subclass of forms.Form which contains a concept of fieldsets (even a rough one that is not backward compatible... this is just for my own project) and if so, can you give any pointer? I do not want to mess with the django code.

I think this snippet does exactly what you want. It gives you a Form subclass that allows you to declaratively subdivide your form into fieldsets and iterate through them in your template.
Update: that snippet has since become part of django-form-utils

Fieldsets in modelforms are still in "design" stage. There's a ticket in Django trac with low activity.
It's something I've been interested in researching myself in the near future, but since I haven't done it yet the best I can offer are these snippets:
Form splitting/Fieldset templatetag
Sectioned Form
Forms splitted in fieldsets
Edit: I just noticed this question again and I realize it needs an edit to point out Carl's project django-form-utils which contains a BetterForm class which can contain fieldsets. If you like this project give him a +1 for his answer below :)

One thing you can do is break your logical fieldsets into separate model form classes.
class PersonalInfoForm (forms.ModelForm):
class Meta:
model=MyModel
fields=('field1', 'field2', ...)
class TermsForm (forms.ModelForm):
class Meta:
model=MyModel
fields=('fieldX', 'fieldY', ...)
Pass them to your template in different variables and break up the formsets:
<form ...>
<fieldset><legend>Personal Information</legend>
{{ personal_info_form }}
</fieldset>
<fieldset><legend>Terms and Conditions</legend>
{{ terms_form }}
</fieldset>
</form>
In that sense each of your form classes is just a fragment of the actual HTML form.
It introduces a touch of complexity when you call save on the form. You'll probably want to pass commit=False and then merge the resultant objects. Or just avoid using ModelForm.save altogether and populate your model object by hand with 'cleaned_data'

Daniel Greenfelds django-uni-form solves this with a the Layout helper class. I'm trying it out right now and it looks pretty clean to me.
Uniform helpers can use layout objects. A layout can consist of fieldsets, rows, columns, HTML and fields.
I originally picked Django-uni-form because it complies with section 508.

You can use this package: https://pypi.org/project/django-forms-fieldset/
pip install django-forms-fieldset
Add forms_fieldset to your INSTALLED_APPS setting like this:
INSTALLED_APPS = [
...
'forms_fieldset',
]
Add fieldsets in your form
from django.forms import ModelForm
from .models import Student
class StudentForm(ModelForm):
fieldsets = [
("Student Information", {'fields': [
('first_name', 'last_name'),
('email', 'adress'),
]}),
("Parent Information", {'fields': [
'mother_name',
'father_name',
]}),
]
class Meta:
model = Student
fields = '__all__'
In your views
def home(request):
form = StudentForm()
if request.method == 'POST':
form = Form(request.POST, request.FILES)
#save...
context = {
'form': form,
}
return render(request, 'home.html', context)
in your template
{% load forms_fieldset static %}
<link rel="stylesheet" type="text/css" href="{% static 'forms_fieldset/css/main.css' %}">
<form>
{{ form|fieldset:'#42945c' }}
</form>

This was the code that I developed in order to understand custom tags (with links). I applied it to create a fieldset.
Disclaimer: I encourage the use of any of the above answers, this was just for the sake of learning.
templatetags/myextras.py:
from django import template
from django.template import Context
register = template.Library()
class FieldsetNode(template.Node):
""" Fieldset renderer for 'fieldset' tag """
def __init__(self, nodelist, fieldset_name):
""" Initialize renderer class
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#writing-the-renderer
:param nodelist: a list of the template nodes inside a block of 'fieldset'
:param fieldset_name: the name of the fieldset
:return: None
"""
self.nodelist = nodelist
self.fieldset_name = fieldset_name
def render(self, context):
""" Render the inside of a fieldset block based on template file
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#auto-escaping-considerations
:param context: the previous template context
:return: HTML string
"""
t = context.template.engine.get_template('myapp/fieldset.html')
return t.render(Context({
'var': self.nodelist.render(context),
'name': self.fieldset_name,
}, autoescape=context.autoescape))
#register.tag
def fieldset(parser, token):
""" Compilation function for fieldset block tag
Render a form fieldset
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#writing-the-compilation-function
https://docs.djangoproject.com/en/1.8/howto/custom-template-tags/#parsing-until-another-block-tag
:param parser: template parser
:param token: tag name and variables
:return: HTML string
"""
try:
tag_name, fieldset_name = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires a single argument" % token.contents.split()[0])
if not (fieldset_name[0] == fieldset_name[-1] and fieldset_name[0] in ('"', "'")):
raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name)
nodelist = parser.parse(('endfieldset',))
parser.delete_first_token()
return FieldsetNode(nodelist, fieldset_name[1:-1])
templates/myapp/fieldset.html:
<div class="fieldset panel panel-default">
<div class="panel-heading">{{ name }}</div>
<div class="panel-body">{{ var }}</div>
</div>
templates/myapp/myform.html:
<form action="{% url 'myapp:myurl' %}" method="post">
{% csrf_token %}
{% fieldset 'General' %}
{{form.myfield1 }}
{% endfieldset %}
{# my submit button #}
</form>

Related

Validating dynamically created ModelForm field in Django 2

I am using Django 2.0 and I have a model for Articles and a model for Storylines. A storyline contains many related articles.
class Article(models.Model):
headline_text = models.CharField(max_length=255, verbose_name='Headline')
storylines = models.ManyToManyField(Storyline, verbose_name='Add to Storylines')
I have a ModelForm that will allow you to choose an article to add to the Storyline. That ModelForm class looks like this:
class StorylineAddArticleForm(forms.Form):
articleSearchBox = forms.CharField(label="Search to narrow list below:")
include_articles = [article.id for article in Article.objects.order_by('-sub_date')[:5]]
articles = forms.ModelMultipleChoiceField(queryset=Article.objects.filter(id__in=include_articles).order_by('-sub_date'))
def __init__(self, *args, **kwargs):
super(StorylineAddArticleForm, self).__init__(*args, **kwargs)
self.fields['articleSearchBox'].required = False
self.helper = FormHelper(self)
self.helper.layout = Layout(
Field('articleSearchBox'),
Field('articles'),
ButtonHolder(
Submit('submit', 'Add', css_class='button white')
)
)
So far so good, if I submit any article in the queryset, the form validates and saves as needed.
The live site will have many more articles than will be practical to display in the ModelMultipleChoice field, so I do some JQuery to allow the user to use articleSearchBox to replace the ModelMultipleChoice field. This works brilliantly and you can do a search for any article, including those not in the original queryset. Here's that:
{% block content %}
<h2>Add Article</h2>
Add an existing article to <strong>{{ storyline.headline_text }}</strong> storyline:<br>
Did you want to add a new article instead?<br>
<hr>
{% crispy form %}
{% endblock %}
{% block pagescripts %}
<script>
$(document).ready(function(){
$("#id_articleSearchBox").on('input propertychange paste', function(){
$.ajax({
url:'/webproxy/a/?q=' + $("#id_articleSearchBox").val(),
type:'get',
dataType:'html',
crossDomain:true,
success:function(data)
{
$("#id_articles").empty().append(data);
},
error: function(data) {
$("#id_articles").empty().append("<option value=\"-1\">No results</option>");
}
});
}); // end article search box
});
</script>
{% endblock %}
THE PROBLEM:
If I do a search and get an article that was not in the original queryset, the validation fails and I am told that it is not a valid choice. I need a validator that will allow any article or articles, as long as they are actually in the database.
WHAT I HAVE TRIED:
I tried creating a validator that looks like this:
def clean_article(self):
art_ID = self.cleaned_data.get('articles', False)
if(art_ID):
try:
art = Article.objects.get(pk=art_ID)
except ObjectDoesNotExist:
return None
else:
return None
# if we are here, we have an article.
return art
This produced no change in behavior. I have looked and looked for a validator that would even allow any value or just check if it exists, but I am not having a lot of luck.
Your custom validator doesn't have any effect as it will be called after the field's validation. For more information about the order validations are run in refer to the django docs.
What you can do instead is overriding said field validation by inheriting from Django's MultipleChoiceField:
from django import forms
class ArticleMultipleChoiceField(forms.MultipleChoiceField):
def validate(self, value):
pass # your custom validation
You will then of course have to use your custom ArticleMultipleChoiceField in your StorylineAddArticleForm for the articles field.

Django 1.11 CreateView adding datepicker for datefields

So I am trying to change my form's model Datefield output to the Datepicker similar to DatepickerWidget in CreateView
The forms are generated using a html template:
{% for field in form %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<span class="text-danger small">{{ field.error }}</span>
</div>
<label class="control-label col-sm-2">{{field.label_tag}}</label>
<div class="col-sm-10">{{field}}</div>
</div>
{% endfor %}
Here is the Views with what I tried:
class newenv_form(generic.CreateView):
model = Environment
fields =['name', 'description', 'creation_date', 'status','status_update_date']
template_name = 'catalogue/new_env.html'
#Does not work
def get_form(self, form):
form = super(newenv_form, self)
form.fields['creation_date','status_update_date'].widget = forms.DateInput(attrs={'class':'datepicker'})
return form
Here is what worked but it is a dropdown datepicker that is limited in choices
def get_form(self):
'''add date picker in forms'''
from django.forms.extras.widgets import SelectDateWidget
form = super(EnvironmentCreateView, self).get_form()
form.fields['creation_date'].widget = SelectDateWidget()
return form
Note that I remove form_class which was causing problems
UPDATE: On Django 3.1, you can find SelectDateWidget within django.forms.widgets
Try to change the following line in the method get_form:
form = super(newenv_form, self)
to:
form = super(newenv_form, self).get_form(form)
And please follow the conventions and use PascalCase for class names in python.
You could call this class EnvironmentCreateView. Further generic view classes could be called for example EnvironmentListView, EnvironmentDetailView, EnvironmentUpdateView, EnvironmentDeleteView.
Using the same pattern for all your model classes will produce comprehensible code.
EDIT (2017-10-24):
Regarding your comment here is a further explanation. Although it is hard to give a correct remote diagnosis, I'd suggest the following changes:
class EnvironmentCreateView(generic.CreateView):
# class attributes ...
def get_form(self, form_class=None):
form = super(EnvironmentCreateView, self).get_form(form_class)
# further code ...
The essential changes are in bold. The class name is changed to meet the conventions. Also the parameter form is changed to form_class to meet the convetions, too. I emphasise conventions in particular, because it makes the code very comprehensible to other people familiar with the framework.
The important change is that form_class has the initial value None.
That should solve the problem with the error.
In the body of the method you call the parent method with super and write after that your custom code.
Please check the documentation for generic.CreateView. It inherits, among others, from generic.FormMixin. That is the class with the method get_form.

Flexible number of form fields in Django forms

I would like to create a form that contains several fields like this
class MainForm(forms.Form):
name = forms.CharField(label='name')
comment = forms.CharField(label='comment')
Right now we have one field "comment". Now I want to have the option that a user can add more comment fields to the form through the website.
So I need to:
1) Define the form more flexible such that there can be N comment fields. (Default is N=1)
2) Define a method to add new comment fields.
3) Define utility functions, like cleaning form fields, so flexible that they can handle the dynamic number of comment fields.
I would like get some ideas of how to set this up in a clean way.
Formsets are a good solution for this:
https://docs.djangoproject.com/en/1.6/topics/forms/formsets/
It will handle 1 and 3 cleanly. I have had to write javascript in the past to inject extra fields in the form when clicking add. Then after submit django takes care of the rest.
So I found something that works for me and I want to at least post the rough draft of what I did.
First, I indeed found percent20's suggestion of using django formsets very helpful. My forms.py looks roughly like this:
from django import forms
from django.forms.formsets import formset_factory
from django.forms.forms import NON_FIELD_ERRORS
class CommentForm(forms.Form):
comment = forms.CharField(label='comment')
class MainForm(forms.Form):
name = forms.CharField(label='name')
def __init__(self):
self.commentFormset = formset_factory(CommentForm, extra=1)
self.comments = self.commentFormset()
def clean(self):
cleaned_data = super(MainForm, self).clean()
for nForm, commentForm in enumerate(self.comments):
if commentForm.is_valid():
pass
else:
self._errors[NON_FIELD_ERRORS] = self.error_class(['Invalid Comments'])
Here is the relevant part of my views.py
from forms import MainForm
def commentView(request):
if request.method == 'POST':
form = MainForm(data=request.POST)
form.comments = form.commentFormset(data=request.POST)
if form.is_valid():
# ...
for commentForm in form.comments:
if commentForm.is_valid():
# do some stuff with each comment
else:
form = MainForm()
template = loader.get_template('main.html')
context = RequestContext(request, { 'form': form })
return HttpResponse(template.render(context))
I found the following great jquery plugin: http://blog.stanisla.us/2009/10/10/jquery-plugin-django-dynamic-formset/.
This enables me to give the user the opportunity to add and delete comment fields. No extra code needed in view.py or forms.py.
Here is the relevant snippet from my template.
<script type="text/javascript" src="{{ STATIC_URL }}jquery.formset.js"></script>
<script type="text/javascript">
$(function() {
$('tr.comment-form').formset( { prefix: 'form' });
})
</script>
<div id="commentSection">
Comments:
<table>
<tbody>
{{ form.comments.management_form }}
<th>Comment</th><th></th>
{% for commentForm in form.comments %}
<tr class="comment-form">
<td>{{ commentForm.comment.errors }}{{ commentForm.comment }}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

How edit data from form django admin

I'm learning Django Framework, and I have a question. To help you understand I will try and explain using the example below:
Suppose that we have some table in db as is:
CREATE TABLE names (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100));
And I have the form in Django Admin as is:
<form>
<textarea name="names"></textarea>
<input type="submit" name="sbt" value="Submit">
</form>
User entered something in the input names in the form and submitted it. Then a script catches this data and splits it into an array (str.split("\n")) and in cycle adding to table names!
And I many quetion:
How i can add form to Django Admin?
How i can catch form data and add this data to somethink table in database?
Thanks.
First of all you must create a django model.
Put this code in models.py.
class Names(models.Model):
name = models.CharField(max_length = 100)
Then you must create the admin model.
Put this code in admin.py.
class NamesAdmin(admin.ModelAdmin):
list_display = ['name']
# whatever you want in your admin panel like filter, search and ...
admin.site.register(Names, NamesAdmin)
I think it meet your request. And for split the names you can override save model method and split the names in there. But if you want to have an extra form, you can easily create a django model form.
Put the code somewhere like admin.py, views.py or forms.py
class NamesForm(forms.ModelForm)
class Meta:
model = Names
That's your model and form. So, if your want to add the form to django admin panel you must create a view for it in django admin. For do this create a view as common.
Put the code in your admin.py or views.py.
def spliter(req):
if req.method == 'POST':
form = NamesForm(req.POST)
if form.is_valid():
for name in form.cleaned_data['names'].split(' '):
Names(name = name).save()
return HttpResponseRedirect('') # wherever you want to redirect
return render(req, 'names.html', {'form': form})
return render(req, 'names.html', {'form': NamesForm()})
Be aware you must create the names.html and put the below code in you html page.
{% extends 'admin/base_site.html' %}
{% block content %}
<!-- /admin/names/spliter/ is your url in admin panel (you can change it whatever you want) -->
<form action="/admin/names/spliter/" method="post" >{% csrf_token %}
{{ form }}
<input type="submit" value="'Send'" >
</form>
{% endblock %}
This is your view and your can use it everywhere. But if you want only the admin have permission to see this page you must add this method too your NamesAdmin class.
def get_urls(self):
return patterns(
'',
(r'^spliter/$', self.admin_site.admin_view(spliter)) # spliter is your view
) + super(NamesAdmin, self).get_urls()
That's It. I hope this can help you.

field's verbose_name in templates

Suppose I have a model:
from django.db import models
class Test(models.Model):
name=models.CharField(max_length=255, verbose_name=u'custom name')
How do I get my model's field's verbose name in templates? The following doesn't work:
{{ test_instance.name.verbose_name }}
I would very much appreciate the solution, something on lines as we do when using forms, using label attribute in template:
{{ form_field.label }}
You can use following python code for this
Test._meta.get_field("name").verbose_name.title()
If you want to use this in template then it will be best to register template tag for this. Create a templatetags folder inside your app containing two files (__init__.py and verbose_names.py).Put following code in verbose_names.py:
from django import template
register = template.Library()
#register.simple_tag
def get_verbose_field_name(instance, field_name):
"""
Returns verbose_name for a field.
"""
return instance._meta.get_field(field_name).verbose_name.title()
Now you can use this template tag in your template after loading the library like this:
{% load verbose_names %}
{% get_verbose_field_name test_instance "name" %}
You can read about Custom template tags in official django documentation.
The method in the accepted answer is awesome!
And maybe you'll like this if you want to generate a field list.
Adding an iterable to the class Test makes it convenient to list fields' verbose name and value.
Model
class Test(models.Model):
...
def __iter__(self):
for field in self._meta.fields:
yield (field.verbose_name, field.value_to_string(self))
Template
{% for field, val in test_instance %}
<div>
<label>{{ field }}:</label>
<p>{{ val }}</p>
</div>
{% endfor %}
based on this answer https://stackoverflow.com/a/14498938 .in Django Model i added
class Meta:
app_name = 'myapp'
in listview i have
from django.core import serializers
context['data'] = serializers.serialize( "python", self.get_queryset() )
inside mylist.html i have
{% for field, value in data.0.fields.items %}
<th style="text-align:center;">{% get_verbose_field_name data.0.model field %}</th>
{% endfor %}
in filter:
from django import template
register = template.Library()
from .models import Mymodel
#register.simple_tag
def get_verbose_field_name(instance, field_name):
"""
Returns verbose_name for a field.
"""
myinstance = eval(instance.split('.')[1].title())
return myinstance._meta.get_field(field_name).verbose_name.title()
instance in the abbove filter for the specific example is myapp.mymodel i evalute instance into model object and the i return field verbose name
it works in django 1.9
It's probably too late for an answer but I had the same issue until I realised that I caused the problem by overriding the fields in the form.py (self.fields['fieldname'] = ..). If you do that you also need to set a label otherwise it uses a label derived from the fieldname.
Hope this quick reply makes sense.