Wagtail form file upload - django

I have an issue when I add file upload field to Wagtail Forms Builder I get this error:
Exception Type: TypeError
Exception Value: Object of type InMemoryUploadedFile is not JSON serializable
This is my code:
class FormField(AbstractFormField):
CHOICES = FORM_FIELD_CHOICES + (('fileupload', 'File Upload'),)
page = ParentalKey('FormPage', on_delete=models.CASCADE, related_name='form_fields')
field_type = models.CharField(
verbose_name='field type',
max_length=16,
# use the choices tuple defined above
choices=CHOICES
)
api_fields = [
APIField('page'),
]
class CustomFormBuilder(FormBuilder):
def create_fileupload_field(self, field, options):
return forms.FileField(**options)
class FormPage(AbstractEmailForm):
form_builder = CustomFormBuilder
intro = RichTextField(blank=True)
thank_you_text = RichTextField(blank=True)
content_panels = AbstractEmailForm.content_panels + [
FieldPanel('intro', classname="full"),
InlinePanel('form_fields', label="Form fields"),
FieldPanel('thank_you_text', classname="full"),
MultiFieldPanel([
FieldRowPanel([
FieldPanel('from_address', classname="col6"),
FieldPanel('to_address', classname="col6"),
]),
FieldPanel('subject'),
], "Email"),
]
# Export fields over the API
api_fields = [
APIField('intro'),
APIField('thank_you_text'),
]
This is my template:
{% load wagtailcore_tags %}
<html>
<head>
<title>{{ page.title }}</title>
</head>
<body>
<h1>{{ page.title }}</h1>
{{ page.intro|richtext }}
<form action="{% pageurl page %}" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<input type="submit">
</form>
</body>
</html>
Wagtail version 2.8.1
Django version 3.0.5
any idea with this issue ?

The core issue here is that you are attempting to store an uploaded file as JSON. Wagtail's FormBuilder does not store the submission data parts as their own DB models but instead bundles is up as json (e.g. {'field-a': 'value'}) and stores that as a string in the database.
The reason for this is that the data stored is flexible on a per page basis and can change over time based on the page's settings.
So, to fully implement a file upload field, you need to store those files somewhere, plus solve a few other problems.
1. Where to store the file
Depending on your Django setup, you will need to get a basic understanding of how to Store files in Django
You will need to create a new model that will store these files, see FormUploadedFile in the example below
Depending on your use case, you will need to consider multiple files uploaded in each form submission, as the FormPage UI enables users to create multiple of any field type, hence it might be good to keep a reference to the field name it is stored under.
2. What to save in the JSON as a reference to the file
This could be a simple pk (primary key) reference, as per the code example below.
You may want to add some more advanced linking between the file upload model and the FormSubmission model for better data integrity
You will need to override the process_form_submission on your FormPage model, you can see the original code here https://github.com/wagtail/wagtail/blob/master/wagtail/contrib/forms/models.py#L195
3. Reading the file and what to represent as this file in the form submissions list
You may want to modify the get_data output from the FormSubmission records, you can do this by adding a custom FormSubmission model (see code below), however this will be in place of your existing model (so your existing submissions will no longer be visible without some sort of migration or other workaround).
You can see the original get_data method here https://github.com/wagtail/wagtail/blob/master/wagtail/contrib/forms/models.py#L48
The Wagatil docs section has a good part about customising the submissions list
Example Code
Here is a rough working POC to get you started, hope this helps.
import json
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django import forms
from modelcluster.fields import ParentalKey
from wagtail.contrib.forms.models import (
AbstractEmailForm, AbstractFormField, AbstractFormSubmission, FORM_FIELD_CHOICES)
from wagtail.contrib.forms.forms import FormBuilder
from wagtail.contrib.forms.views import SubmissionsListView
class FormField(AbstractFormField):
page = ParentalKey('FormPage', related_name='form_fields', on_delete=models.CASCADE)
field_type = models.CharField(
verbose_name='field type',
max_length=16,
choices=FORM_FIELD_CHOICES + (('fileupload', 'File Upload'),)
)
class CustomFormBuilder(FormBuilder):
def create_fileupload_field(self, field, options):
return forms.FileField(**options)
class CustomSubmissionsListView(SubmissionsListView):
"""
further customisation of submission list can be done here
"""
pass
class CustomFormSubmission(AbstractFormSubmission):
# important - adding this custom model will make existing submissions unavailable
# can be resolved with a custom migration
def get_data(self):
"""
Here we hook in to the data representation that the form submission returns
Note: there is another way to do this with a custom SubmissionsListView
However, this gives a bit more granular control
"""
file_form_fields = [
field.clean_name for field in self.page.specific.get_form_fields()
if field.field_type == 'fileupload'
]
data = super().get_data()
for field_name, field_vale in data.items():
if field_name in file_form_fields:
# now we can update the 'representation' of this value
# we could query the FormUploadedFile based on field_vale (pk)
# then return the filename etc.
pass
return data
class FormUploadedFile(models.Model):
file = models.FileField(upload_to="files/%Y/%m/%d")
field_name = models.CharField(blank=True, max_length=254)
class FormPage(AbstractEmailForm):
form_builder = CustomFormBuilder
submissions_list_view_class = CustomSubmissionsListView
# ... other fields (image, body etc)
content_panels = AbstractEmailForm.content_panels + [
# ...
]
def get_submission_class(self):
"""
Returns submission class.
Important: will make your existing data no longer visible, only needed if you want to customise
the get_data call on the form submission class, but might come in handy if you do it early
You can override this method to provide custom submission class.
Your class must be inherited from AbstractFormSubmission.
"""
return CustomFormSubmission
def process_form_submission(self, form):
"""
Accepts form instance with submitted data, user and page.
Creates submission instance.
You can override this method if you want to have custom creation logic.
For example, if you want to save reference to a user.
"""
file_form_fields = [field.clean_name for field in self.get_form_fields() if field.field_type == 'fileupload']
for (field_name, field_value) in form.cleaned_data.items():
if field_name in file_form_fields:
uploaded_file = FormUploadedFile.objects.create(
file=field_value,
field_name=field_name
)
# store a reference to the pk (as this can be converted to JSON)
form.cleaned_data[field_name] = uploaded_file.pk
return self.get_submission_class().objects.create(
form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder),
page=self,
)

Related

Django Admin: Add Hyperlink to related model

I would like to add a hyperlink to the related model Training
It would be nice to have declarative solution, since I want to use
this at several places.
The "pencil" icon opens the related model in a popup window. That's not what I want. I want a plain hyperlink to the related model.
BTW, if you use "raw_id_fields", then the result is exactly what I was looking for: There is a hyperlink to the corresponding admin interface of this ForeignKey.
Update Jan 4, 2023
From Django 4.1, this becomes a part of the official build (related PR).
Related widget wrappers now have a link to object’s change form
Result
Previous Answer
The class named RelatedFieldWidgetWrapper is showing the icons on the Django Admin page and thus you need to override the same. So, create a custom class as below,
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
class CustomRelatedFieldWidgetWrapper(RelatedFieldWidgetWrapper):
template_name = 'admin/widgets/custom_related_widget_wrapper.html'
#classmethod
def create_from_root(cls, root_widget: RelatedFieldWidgetWrapper):
# You don't need this method of you are using the MonkeyPatch method
set_attr_fields = [
"widget", "rel", "admin_site", "can_add_related", "can_change_related",
"can_delete_related", "can_view_related"
]
init_args = {field: getattr(root_widget, field) for field in set_attr_fields}
return CustomRelatedFieldWidgetWrapper(**init_args)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
rel_opts = self.rel.model._meta
info = (rel_opts.app_label, rel_opts.model_name)
context['list_related_url'] = self.get_related_url(info, 'changelist')
return context
See, the context variable list_related_url is the relative path that we need here. Now, create an HTML file to render the output,
#File: any_registered_appname/templates/admin/widgets/custom_related_widget_wrapper.html
{% extends "admin/widgets/related_widget_wrapper.html" %}
{% block links %}
{{ block.super }}
- Link To Related Model -
{% endblock %}
How to connect?
Method-1 : Monkey Patch
# admin.py
# other imports
from ..widgets import CustomRelatedFieldWidgetWrapper
from django.contrib.admin import widgets
widgets.RelatedFieldWidgetWrapper = CustomRelatedFieldWidgetWrapper # monket patch
Method-2 : Override ModelAdmin
# admin.py
class AlbumAdmin(admin.ModelAdmin):
hyperlink_fields = ["related_field_1"]
def formfield_for_dbfield(self, db_field, request, **kwargs):
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
if db_field.name in self.hyperlink_fields:
formfield.widget = CustomRelatedFieldWidgetWrapper.create_from_root(
formfield.widget
)
return formfield
Result
There are several ways to go. Here is one.
Add some javascript that changes the existing link behavior. Add the following script at the end of the overridden admin template admin/widgets/related_widget_wrapper.html. It removes the class which triggers the modal and changes the link to the object.
It will only be triggered for id_company field. Change to your needs.
{% block javascript %}
<script>
'use strict';
{
const $ = django.jQuery;
function changeEditButton() {
const edit_btn = document.getElementById('change_id_company');
const value = edit_btn.previousElementSibling.value;
const split_link_template = edit_btn.getAttribute('data-href-template').split('?');
edit_btn.classList.remove('related-widget-wrapper-link');
edit_btn.setAttribute('href', split_link_template[0].replace('__fk__', value));
};
$(document).ready(function() {
changeEditButton();
$('body').on('change', '#id_company', function(e) {
changeEditButton();
});
});
}
</script>
{% endblock %}
This code can also be modified to be triggered for all edit buttons and not only for the company edit button.

How do you expose a bulk create to django admin page users?

Let's say that, as part of an administrative process, thousands of values are created offline. These values are entered into a dead-simple model:
class Foo(models.Model):
value = models.CharField(max_length=32)
I'd like to have a field on the model creation page that allows the user to enter (copy-paste) 1000 values in, and as a result, 1000 rows would be created in the table.
If I can add a text field to the model creation, all I have to do is parse out the values and call Foo.create for each. How would I add this free-form field, and how would I go about processing it when the user hits the Save button? ...or is there entirely a different way that I should be going about this?
I realize my comment is more of an answer now.
Sure, why not? You hardly even need django for this. You could just create a <textarea name="foo"></textarea>, and in your view parse the data by line break.
Create a custom admin view via Admin.get_urls and write a custom view for your bulk create page.
https://docs.djangoproject.com/en/1.8/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_urls
Here's a copy and paste out of the live example for get_urls
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = [
url(r'^my_view/$', self.my_view),
]
return my_urls + urls
def my_view(self, request):
# ...
context = dict(
# Include common variables for rendering the admin template.
self.admin_site.each_context(request),
# Anything else you want in the context...
)
if request.method == 'POST':
for line in request.POST['bulk-create-paste'].split('\n'):
Foo.objects.create(myfield=line)
return TemplateResponse(request, "sometemplate.html", context)
sometemplate.html
<form method="POST">
<p>Paste bulk create info.</p>
<textarea name="bulk-create-paste">
</textarea>
</form>

How can i customize the html output of a widget in Django?

I couldn't find this in the docs, but think it must be possible. I'm talking specifically of the ClearableFileInput widget. From a project in django 1.2.6 i have this form:
# the profile picture upload form
class ProfileImageUploadForm(forms.ModelForm):
"""
simple form for uploading an image. only a filefield is provided
"""
delete = forms.BooleanField(required=False,widget=forms.CheckboxInput())
def save(self):
# some stuff here to check if "delete" is checked
# and then delete the file
# 8 lines
def is_valid(self):
# some more stuff here to make the form valid
# allthough the file input field is empty
# another 8 lines
class Meta:
model = SocialUserProfile
fields = ('image',)
which i then rendered using this template code:
<form action="/profile/edit/" method="post" enctype="multipart/form-data">
Delete your image:
<label> {{ upload_form.delete }} Ok, delete </label>
<button name="delete_image" type="submit" value="Save">Delete Image</button>
Or upload a new image:
{{ upload_form.image }}
<button name="upload_image" type="submit" value="Save">Start Upload</button>
{% csrf_token %}
</form>
As Django 1.3.1 now uses ClearableFileInput as the default widget, i'm pretty sure i can skip the 16 lines of my form.save and just shorten the form code like so:
# the profile picture upload form
class ProfileImageUploadForm(forms.ModelForm):
"""
simple form for uploading an image. only a filefield is provided
"""
class Meta:
model = SocialUserProfile
fields = ('image',)
That would give me the good feeling that i have less customized formcode, and can rely on the Django builtins.
I would, of course, like to keep the html-output the same as before. When just use the existing template code, such things like "Currently: somefilename.png" pop up at places where i do not want them.
Splitting the formfield further, like {{ upload_form.image.file }} does not seem to work. The next thing coming to my mind was to write a custom widget. Which would work exactly against my efforts to remove as many customized code as possible.
Any ideas what would be the most simple thing to do in this scenario?
Firstly, create a widgets.py file in an app. For my example, I'll be making you an AdminImageWidget class that extends AdminFileWidget. Essentially, I want a image upload field that shows the currently uploaded image in an <img src="" /> tag instead of just outputting the file's path.
Put the following class in your widgets.py file:
from django.contrib.admin.widgets import AdminFileWidget
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
import os
import Image
class AdminImageWidget(AdminFileWidget):
def render(self, name, value, attrs=None):
output = []
if value and getattr(value, "url", None):
image_url = value.url
file_name=str(value)
# defining the size
size='100x100'
x, y = [int(x) for x in size.split('x')]
try :
# defining the filename and the miniature filename
filehead, filetail = os.path.split(value.path)
basename, format = os.path.splitext(filetail)
miniature = basename + '_' + size + format
filename = value.path
miniature_filename = os.path.join(filehead, miniature)
filehead, filetail = os.path.split(value.url)
miniature_url = filehead + '/' + miniature
# make sure that the thumbnail is a version of the current original sized image
if os.path.exists(miniature_filename) and os.path.getmtime(filename) > os.path.getmtime(miniature_filename):
os.unlink(miniature_filename)
# if the image wasn't already resized, resize it
if not os.path.exists(miniature_filename):
image = Image.open(filename)
image.thumbnail([x, y], Image.ANTIALIAS)
try:
image.save(miniature_filename, image.format, quality=100, optimize=1)
except:
image.save(miniature_filename, image.format, quality=100)
output.append(u' <div><img src="%s" alt="%s" /></div> %s ' % \
(miniature_url, miniature_url, miniature_filename, _('Change:')))
except:
pass
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
Ok, so what's happening here?
I import an existing widget (you may be starting from scratch, but should probably be able to extend ClearableFileInput if that's what you are starting with)
I only want to change the output/presentation of the widget, not the underlying logic. So, I override the widget's render function.
in the render function I build the output I want as an array output = [] you don't have to do this, but it saves some concatenation. 3 key lines:
output.append(u' <div><img src="%s" alt="%s" /></div> %s ' % (miniature_url, miniature_url, miniature_filename, _('Change:'))) Adds an img tag to the output
output.append(super(AdminFileWidget, self).render(name, value, attrs)) adds the parent's output to my widget
return mark_safe(u''.join(output)) joins my output array with empty strings AND exempts it from escaping before display
How do I use this?
class SomeModelForm(forms.ModelForm):
"""Author Form"""
photo = forms.ImageField(
widget = AdminImageWidget()
)
class Meta:
model = SomeModel
OR
class SomeModelForm(forms.ModelForm):
"""Author Form"""
class Meta:
model = SomeModel
widgets = {'photo' : AdminImageWidget(),}
Which gives us:

Upload Image to Blob using GAE + Django

I'm writing an application using GAE and Django in which I want to give to user the ability to upload his image. Also, I want this image be stored as blob on GAE's datastore. I have seen many examples but nothing specific to this scenario. Although, I feel that is a common issue.
All I want, is to create a new product and this new product must have an image.
1st Attempt: I have tried to add an image attribute (db.BlobProperty()) in product's model, and obviously django does not include it on the presented form.
2nd Attempt: I have created a new entity with two attributes (product as db.ReferenceProperty() and image as db.BlobProperty()). With this I tried to work parallel with django form modifying the django HTML form (including an |input type='file' name='img' /|) expecting that I could take the image from the request object but I failed once more.
This is the Product Class:
class Product(db.Model):
id = db.IntegerProperty()
desc = db.StringProperty()
prodCateg = db.ReferenceProperty(ProductCategory)
price = db.FloatProperty()
details = db.StringProperty()
image = db.BlobProperty()
This is the Django Form (HTML):
<form action="{%url admin.editProduct product.key.id%}" enctype="multipart/form-data" method="post">
<table>
{{form}}
<tr><td><input type="file" name="img" /></td></tr>
<tr><td><input type="submit" value="Create or Edit Product"></td></tr>
</table>
</form>
This is the Django Form (python):
class ProductForm(djangoforms.ModelForm):
class Meta:
model = Product
exclude = ['id']
This is the request handler:
def editProduct(request, product_id):
user = users.GetCurrentUser()
#if user is None:
# return http.HttpResponseForbidden('You must be signed in to add or edit a gift')
product = None
if product_id:
product = Product.get(db.Key.from_path(Product.kind(), int(product_id)))
if product is None:
return http.HttpResponseNotFound('No product exists with that key (%r)' %
product)
form = ProductForm(data=request.POST or None, instance=product)
##########################
# Ambitious undertaking! #
##########################
#if not product_id:
# uploadedImage = get("img")
# photo = Image()
# photo.product = product
# uploadedPhoto = request.FILES['img'].read()
# photo.image = db.Blob(uploadedPhoto)
# image.put()
if not request.POST:
return respond(request, user, 'addprod', {'form': form, 'product': product})
errors = form.errors
if not errors:
try:
product = form.save(commit=False)
except ValueError, err:
errors['__all__'] = unicode(err)
if errors:
return respond(request, user, 'addprod', {'form': form, 'product': product})
product.put()
return http.HttpResponseRedirect('/product')
As you can see the request handler is based on the Google's Gift-Tutorial
So, If anyone could put his opinion I would be very thankful!
Thank you in advance!
You may want to look at an example that uses the blobstore API with blobstoreuploadhandler and/or edit your request handler to store the uploaded file as a blobproperty or a blobreferenceproperty depending on if you use the blobstore API or just a blobproperty variable. You can be specific about http post data i.e. `
self.request.post('img').file.read()
I do recommend that you choose the blobstore API and blobstoreuploadhandler since that will do some very good things for you automatically: 1. storing MIM type and 2. storing filename 3. enabling serving via get_serving_url that has several advantages.

Django and fieldsets on ModelForm

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>