When I render the empty form it is not attaching any media to it. i.e. the CKEditor is not displayed. The element looks like it is missing the css/js - it's like it doesn't get set up properly.
Note : the other sections are displayed correctly.
Where to start? Problem with Django's Empty Form method? Problem with CKEditor? Me :)
<div class="container">
<button type ="button" class="btn-info btn-lg" id="add_section">Add Section</button>
{{form.media }}
{{form|crispy }}
{{sections.media}}
<div>
{{sections.empty_form}}
</div>
<div id = 'section_management'> {{ sections.management_form }} </div>
{% for section in sections %}
{{ section|crispy }}
{% endfor %}
<button class="btn btn-info ml-2" type="submit">Update</button>
Cancel
</div>
Here's my Forms
class SectionForm(forms.ModelForm):
content = RichTextFormField()
class Meta:
model = Section
fields = ('content',)
empty_permitted=True
def __init__(self, *args, **kwargs):
print('section form called')
super().__init__(*args, **kwargs)
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ('title','category','span')
def __init__(self, *args, **kwargs):
self.is_superuser = kwargs.pop('is_superuser', None)
super().__init__(*args, **kwargs)
if self.is_superuser == False:
self.fields.pop("span")
view code
class ArticleUpdateView(LoginRequiredMixin,UserPassesTestMixin,UpdateView):
template_name = 'articles/ArticleUpdate.html'
form_class = ArticleForm
model = Article
SectionFormSet = inlineformset_factory(Article, Section, form=SectionForm, extra=0, can_delete=False, fields=('content',))
#if i always pass back at least 1 extra section form, I can grab the html for it in Jquery */
#if i do not pass back extra=0 how would i get the html in jquery for the extra form?
def test_func(self):
article = self.get_object()
if self.request.user == article.author or self.request.user.is_superuser :
return True
else:
return False
def get_context_data(self, **kwargs):
print('get context data called update view')
'''
section_form
'''
context = super().get_context_data(**kwargs)
if self.request.POST:
context['sections'] = self.SectionFormSet(self.request.POST,instance=self.object)
else:
context['sections'] = self.SectionFormSet(instance=self.object)
return context
def get_section_form(self): #we know we can access this in the template
return SectionForm()
def save_sections(self):
print('save sections called update view')
try:
context = self.get_context_data()
section_form = context['sections']
if section_form.is_valid():
# section_form.instance = self.object #if im passing instance in the factory, do I need it here to?
section_form.save()
except Exception as e:
print('failed to save section: ' + str(e))
def form_valid(self, form):
print('form valid called update view')
form.instance.author = self.request.user
response = super().form_valid(form) #save article form
self.save_sections()
return response
def get_success_url(self):
return reverse_lazy('index')
Basically, what I've done so far to overcome this problem is by accessing the form directly from the template, bypassing the inlineFormSet to get an empty form....(hope that makes sense).
I go directly to the view :
{{view.get_section_form}}
with this method in the view
def get_section_form(self): #we know we can access this in the template
return SectionForm()
I have subsequently found out I can do this in the template as well :
{{sections.media}}
{{sections.form}}
The above also passes an empty form - with the media filled in- as long as you pass the model form into the factory to start of with.
These are work-arounds for me currently, but would appreciate a proper answer as to why empty_form doesn't work properly.
My further investigation into this was basically comparing what is returned via accessing the formset to return an empty form, or using the modelForm directly.
Django docs :
empty_form¶
BaseFormSet provides an additional attribute empty_form which returns a form instance with a prefix of __prefix__ for easier use in dynamic forms with JavaScript.
If you replace prefix on the generated html -- everything works. No idea why. You can replace it with anything, i.e. prefix1
at which point CKEditor starts to display the formset correctly.
I was looking in django source-code to understand super(ExampleView, self).get_context_data(**kwargs) and why I used it in my view:
class ExampleView(TemplateView):
# ... atributes
def get_context_data(self, **kwargs):
context = super(ExampleView, self).get_context_data(**kwargs)
context['key'] = 'value'
return context
I've found:
class ContextMixin(object):
"""
A default context mixin that passes the keyword arguments received by
get_context_data as the template context.
"""
def get_context_data(self, **kwargs):
if 'view' not in kwargs:
kwargs['view'] = self
return kwargs
I can't figure it out what that condition or kwargs['view'] = self does.
I've tried in my view to overwrite get_context_data() without that default condition:
class ExampleView(TemplateView):
# .. atributes
def get_context_data(self, **kwargs):
kwargs['key'] = 'value'
return kwargs
and it worked the same as the first code I've written.
Those 2 lines of code add the view as variable to the context if it was not already present. Most people never use this, but you could do something like this:
class SomeView(TemplateView):
template_name = "something.html"
title = "My list of books"
def books(self): #custom method
return Book.objects.all()
And then in your template you could reference the books method and title attribute through the view variable:
<h1>{{ view.title }}</h1>
<ul>
{% for book in view.books %}
<li>{{ book }}</li>
{% enfor %}
<ul>
Ah yes, and note that you don't even need a custom get_context_data() method in this case
My Django project uses Bootstrap v4.
I iterate through all the fields to add the HTML attribute class="form-control". I also need to add an HTML attribute aria-describedby="{}Help" where {} is the id_for_label for the widget.
In the template language I could print that value easily with {{ form.my_field.id_for_label }} but programmatically I don't know how to do it.
I tried it with ModelForm().fields['…'].widget.id_for_label() but that method expects an argument. And by lookind at the Django source code, the return value is literally the argument.
Any ideas?
class AbonnentForm(ModelForm):
class Meta:
model = Abonnent
fields = '__all__'
def __init__(self, *args, **kwargs):
super(AbonnentForm, self).__init__(*args, **kwargs)
# Add Bootstrap class to all <input> elements.
for key, value in self.fields.items():
value.widget.attrs.update({'class': 'form-control'})
if value.help_text:
id_for_label = "{}Help".format(value.widget.id_for_label())
value.widget.attrs.update({'aria-describedby': id_for_label})
self.fields['erste_ausgabe'].widget.attrs.update({'placeholder':'MM/JJJJ'})
self.fields['letzte_ausgabe'].widget.attrs.update({'placeholder':'MM/JJJJ'})
You can contruct it yourself like this:
for key, value in self.fields.items():
value.widget.attrs.update({'class': 'form-control'})
if value.help_text:
django_id_for_label = self.auto_id % key # self.auto_id == 'id_%s'
my_id_for_label = '{}Help'.format(django_id_for_label)
value.widget.attrs.update({'aria-describedby': my_id_for_label})
There is a possibility in Django templates to refer to attributes of the field directly in a template. For example to .help_text or .label. Like that:
{% for field in form %}
{{field.label|safe}}
{{field}}
{% endfor %}
What is the right way to refer to a custom defined field property?
For example:
{{field.myproperty}}
I use ModelForm and in models.py I use my own ModelField. Everything works perfectly but any attempts to add my own property fail. In a sense that everything is ok but if I refer to this property in a template it just doesn't get it :-(
Models.py:
class MyFormField(forms.IntegerField):
MyProperty = 'whatever'
def __init__(self,active1='default',*args, **kwargs):
self.MyProperty = 'whatever'
super(MyFormField, self).__init__(*args, **kwargs)
class MyOwnField(models.IntegerField):
def __init__(self, active1='asdf',*args, **kwargs):
super(MyOwnField, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
defaults = {'form_class': MyFormField}
defaults.update(kwargs)
return super().formfield(**defaults)
class MyModel(Model):
ns6_1 = MyOwnField()
Firstly, case is significant; field.myproperty is not the same as field.MyProperty.
Secondly, when you iterate through a form you don't directly get the form fields, you get BoundField instances. Each of these has a field property which is the field itself. So:
{{ field.field.MyProperty }}
I have a django Formset that I'd like to layout in the middle of another form. I'm using django-crispy-forms to set the layout in the parent form's __init__:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Field, Div
def __init__(self, *args, **kwargs):
self.helper = FormHelper()
self.helper.layout = Layout(
Div(
Div(Field('foo'), css_class='span3'),
Div(Field('bar'), css_class='span4'),
css_class='row'
),
Field('baz', css_class='span1'),
...
)
self.helper.add_input(Submit('submit', 'Submit', css_class='btn btn-primary offset4'))
My template simply renders the form using the {% crispy %} tag.
I'd like to know how I should incorporate the formset. Should I instantiate it in the above init function? How do I refer to it there?
There are other examples of form and formset combos online that have one render after the other serially, but I'm wondering whether I can have more control over how they fit together with crispy's layout.
I solved this without modifying Crispy Forms, by creating a new field type that renders a formset:
from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
class Formset(LayoutObject):
"""
Layout object. It renders an entire formset, as though it were a Field.
Example::
Formset("attached_files_formset")
"""
template = "%s/formset.html" % TEMPLATE_PACK
def __init__(self, formset_name_in_context, template=None):
self.formset_name_in_context = formset_name_in_context
# crispy_forms/layout.py:302 requires us to have a fields property
self.fields = []
# Overrides class variable with an instance level variable
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, Context({'wrapper': self,
'formset': formset}))
It needs a template to render the formset, which gives you control over exactly how it's rendered:
{% load crispy_forms_tags %}
<div class="formset">
{% crispy formset %}
<input type="button" name="add" value="Add another" />
</div>
You can use it to embed a formset in your layouts just like any other Crispy layout element:
self.helper.layout = Layout(
MultiField(
"Education",
Formset('education'),
),
A slight modification to the earlier answer by qris.
This update (as suggested by Alejandro) will allow for our custom Formset Layout Object to use a FormHelper object to control how the formset's fields are rendered.
from crispy_forms.layout import LayoutObject
from django.template.loader import render_to_string
class Formset(LayoutObject):
"""
Renders an entire formset, as though it were a Field.
Accepts the names (as a string) of formset and helper as they
are defined in the context
Examples:
Formset('contact_formset')
Formset('contact_formset', 'contact_formset_helper')
"""
template = "forms/formset.html"
def __init__(self, formset_context_name, helper_context_name=None,
template=None, label=None):
self.formset_context_name = formset_context_name
self.helper_context_name = helper_context_name
# crispy_forms/layout.py:302 requires us to have a fields property
self.fields = []
# Overrides class variable with an instance level variable
if template:
self.template = template
def render(self, form, form_style, context, **kwargs):
formset = context.get(self.formset_context_name)
helper = context.get(self.helper_context_name)
# closes form prematurely if this isn't explicitly stated
if helper:
helper.form_tag = False
context.update({'formset': formset, 'helper': helper})
return render_to_string(self.template, context.flatten())
Template (used to render formset):
{% load crispy_forms_tags %}
<div class="formset">
{% if helper %}
{% crispy formset helper %}
{% else %}
{{ formset|crispy }}
{% endif %}
</div>
Now it can be used in any layout just like any other crispy forms layout object.
self.helper.layout = Layout(
Div(
Field('my_field'),
Formset('my_formset'),
Button('Add New', 'add-extra-formset-fields'),
),
)
# or with a helper
self.helper.layout = Layout(
Div(
Field('my_field'),
Formset('my_formset', 'my_formset_helper'),
Button('Add New', 'add-extra-formset-fields'),
),
)
This is currently not supported in crispy-forms. Your only option would be to use |as_crispy_field filter (not documented yet, sorry).
I have started development of this feature for {% crispy %} tag and in a feature branch, it's all explained here: https://github.com/maraujop/django-crispy-forms/issues/144
I'm looking for feedback, so if you are still interested, feel free to post.
Basing on above solution Formset(LayoutObject), you would combine django-dynamic-formset & crispy.
On my order page I have:
order's section part 1
order's inline formset with dynamic-add forms
order's section part N
Now it is simple and clear, ModelForms are:
class OrderTestForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(OrderTestForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_tag = True
self.helper.html5_required = True
self.helper.form_action = 'test_main'
self.helper.layout = Layout(
'product_norms', #section 1
'reference_other', #section 1
# rest of the section 1 fields
Formset('samples', 'helper'), # inline dynamic forms
'checkbox_is_required' # start of section N
# other order sections fields
)
self.helper.add_input(Submit("submit", "Save order"))
Formset helper layout:
class SamplesFormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super(SamplesFormSetHelper, self).__init__(*args, **kwargs)
self.form_method = 'post'
self.html5_required = True
self.layout = Layout(
Fieldset('',
'description',
'product', # foreign key
'DELETE', # delete django-dynamic-formset
css_class="formset_row"), # add-rows
)
self.form_tag = False
self.render_required_fields = False
Add/delete inlines, saving order with formset operations work as expected.