* Removed InternationalPhoneNumber class, instead using the form creation
callback to provide a RegexpField class. * Added checkbox for submitter contact information being the same as IETF contact information; with javascript support and validation support. * Cleaned out the old split_form() and mk_formatting_form() functions from ietf.utils. - Legacy-Id: 138
This commit is contained in:
parent
8b2e90f212
commit
8d7053fc5c
|
@ -6,30 +6,6 @@ from django import newforms as forms
|
||||||
from ietf.idtracker.views import InternetDraft
|
from ietf.idtracker.views import InternetDraft
|
||||||
from ietf.idtracker.models import Rfc
|
from ietf.idtracker.models import Rfc
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
|
||||||
# New field classes
|
|
||||||
|
|
||||||
phone_re = re.compile(r'^\+?[0-9 ]*(\([0-9]+\))?[0-9 -]+$')
|
|
||||||
class InternationalPhoneNumberField(models.CharField):
|
|
||||||
error_message = 'Phone numbers may have a leading "+", and otherwise only contain numbers [0-9], dash, space, and parentheses. '
|
|
||||||
def validate(self, field_data, all_data):
|
|
||||||
if not phone_re.search(field_data):
|
|
||||||
raise ValidationError, self.error_message + ' "%s" is invalid.' % field_data
|
|
||||||
|
|
||||||
def clean(self, value):
|
|
||||||
if value in EMPTY_VALUES:
|
|
||||||
return u''
|
|
||||||
self.validate(value, {})
|
|
||||||
return smart_unicode(value)
|
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
|
||||||
defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name),
|
|
||||||
'help_text': self.help_text,
|
|
||||||
'error_message': self.error_message + "Enter a valid phone number."}
|
|
||||||
defaults.update(kwargs)
|
|
||||||
return forms.RegexField(phone_re, **defaults)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------
|
# ------------------------------------------------------------------------
|
||||||
# Models
|
# Models
|
||||||
|
|
||||||
|
@ -178,8 +154,8 @@ class IprContact(models.Model):
|
||||||
department = models.CharField(blank=True, maxlength=255)
|
department = models.CharField(blank=True, maxlength=255)
|
||||||
address1 = models.CharField(blank=True, maxlength=255)
|
address1 = models.CharField(blank=True, maxlength=255)
|
||||||
address2 = models.CharField(blank=True, maxlength=255)
|
address2 = models.CharField(blank=True, maxlength=255)
|
||||||
telephone = InternationalPhoneNumberField(maxlength=25, core=True)
|
telephone = models.CharField(maxlength=25, core=True)
|
||||||
fax = InternationalPhoneNumberField(blank=True, maxlength=25)
|
fax = models.CharField(blank=True, maxlength=25)
|
||||||
email = models.EmailField(maxlength=255, core=True)
|
email = models.EmailField(maxlength=255, core=True)
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import re
|
||||||
import models
|
import models
|
||||||
from django.shortcuts import render_to_response as render
|
|
||||||
import django.newforms as forms
|
|
||||||
from django.utils.html import escape, linebreaks
|
|
||||||
import ietf.utils
|
import ietf.utils
|
||||||
import syslog
|
import django.newforms as forms
|
||||||
|
from django.shortcuts import render_to_response as render
|
||||||
|
from django.utils.html import escape, linebreaks
|
||||||
|
from ietf.contrib.form_decorator import form_decorator
|
||||||
|
from ietf.utils import log as log
|
||||||
|
|
||||||
def default(request):
|
def default(request):
|
||||||
"""Default page, with links to sub-pages"""
|
"""Default page, with links to sub-pages"""
|
||||||
|
@ -83,7 +85,6 @@ def show(request, ipr_id=None):
|
||||||
ipr.discloser_identify = linebreaks(escape(ipr.discloser_identify))
|
ipr.discloser_identify = linebreaks(escape(ipr.discloser_identify))
|
||||||
ipr.comments = linebreaks(escape(ipr.comments))
|
ipr.comments = linebreaks(escape(ipr.comments))
|
||||||
ipr.other_notes = linebreaks(escape(ipr.other_notes))
|
ipr.other_notes = linebreaks(escape(ipr.other_notes))
|
||||||
opt = ipr.licensing_option
|
|
||||||
ipr.licensing_option = dict(models.LICENSE_CHOICES)[ipr.licensing_option]
|
ipr.licensing_option = dict(models.LICENSE_CHOICES)[ipr.licensing_option]
|
||||||
ipr.selecttype = dict(models.SELECT_CHOICES)[ipr.selecttype]
|
ipr.selecttype = dict(models.SELECT_CHOICES)[ipr.selecttype]
|
||||||
if ipr.selectowned:
|
if ipr.selectowned:
|
||||||
|
@ -99,21 +100,47 @@ def new(request, type):
|
||||||
"""Make a new IPR disclosure"""
|
"""Make a new IPR disclosure"""
|
||||||
debug = ""
|
debug = ""
|
||||||
|
|
||||||
|
# define callback methods for special field cases.
|
||||||
|
def ipr_detail_form_callback(field, **kwargs):
|
||||||
|
if field.name == "licensing_option":
|
||||||
|
return forms.IntegerField(widget=forms.RadioSelect(choices=models.LICENSE_CHOICES))
|
||||||
|
if field.name in ["selecttype", "selectowned"]:
|
||||||
|
return forms.IntegerField(widget=forms.RadioSelect(choices=((1, "YES"), (2, "NO"))))
|
||||||
|
return field.formfield(**kwargs)
|
||||||
|
|
||||||
|
def ipr_contact_form_callback(field, **kwargs):
|
||||||
|
phone_re = re.compile(r'^\+?[0-9 ]*(\([0-9]+\))?[0-9 -]+$')
|
||||||
|
error_message = """Phone numbers may have a leading "+", and otherwise only contain
|
||||||
|
numbers [0-9]; dash, period or space; parentheses, and an optional
|
||||||
|
extension number indicated by 'x'. """
|
||||||
|
|
||||||
|
if field.name == "telephone":
|
||||||
|
return forms.RegexField(phone_re, error_message=error_message, **kwargs)
|
||||||
|
if field.name == "fax":
|
||||||
|
return forms.RegexField(phone_re, error_message=error_message, required=False, **kwargs)
|
||||||
|
return field.formfield(**kwargs)
|
||||||
|
|
||||||
|
# Get a form class which renders fields using a given template
|
||||||
CustomForm = ietf.utils.makeFormattingForm(template="ipr/formfield.html")
|
CustomForm = ietf.utils.makeFormattingForm(template="ipr/formfield.html")
|
||||||
BaseIprForm = forms.form_for_model(models.IprDetail, form=CustomForm, formfield_callback=detail_field_fixup)
|
|
||||||
BaseContactForm = forms.form_for_model(models.IprContact, form=CustomForm)
|
# Get base form classes for our models
|
||||||
|
BaseIprForm = forms.form_for_model(models.IprDetail, form=CustomForm, formfield_callback=ipr_detail_form_callback)
|
||||||
|
BaseContactForm = forms.form_for_model(models.IprContact, form=CustomForm, formfield_callback=ipr_contact_form_callback)
|
||||||
|
|
||||||
section_list = section_table[type]
|
section_list = section_table[type]
|
||||||
section_list.update({"title":False, "new_intro":False, "form_intro":True, "form_submit":True, })
|
section_list.update({"title":False, "new_intro":False, "form_intro":True, "form_submit":True, })
|
||||||
|
|
||||||
# Some subclassing:
|
# Some subclassing:
|
||||||
|
|
||||||
|
# The contact form will be part of the IprForm, so it needs a widget.
|
||||||
|
# Define one.
|
||||||
class MultiformWidget(forms.Widget):
|
class MultiformWidget(forms.Widget):
|
||||||
def value_from_datadict(self, data, name):
|
def value_from_datadict(self, data, name):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class ContactForm(BaseContactForm):
|
class ContactForm(BaseContactForm):
|
||||||
widget = MultiformWidget()
|
widget = MultiformWidget()
|
||||||
|
|
||||||
def add_prefix(self, field_name):
|
def add_prefix(self, field_name):
|
||||||
return self.prefix and ('%s_%s' % (self.prefix, field_name)) or field_name
|
return self.prefix and ('%s_%s' % (self.prefix, field_name)) or field_name
|
||||||
def clean(self, *value):
|
def clean(self, *value):
|
||||||
|
@ -121,16 +148,24 @@ def new(request, type):
|
||||||
return self.full_clean()
|
return self.full_clean()
|
||||||
else:
|
else:
|
||||||
return self.clean_data
|
return self.clean_data
|
||||||
|
|
||||||
class IprForm(BaseIprForm):
|
class IprForm(BaseIprForm):
|
||||||
holder_contact = None
|
holder_contact = None
|
||||||
rfclist = forms.CharField(required=False)
|
rfclist = forms.CharField(required=False)
|
||||||
draftlist = forms.CharField(required=False)
|
draftlist = forms.CharField(required=False)
|
||||||
stdonly_license = forms.BooleanField(required=False)
|
stdonly_license = forms.BooleanField(required=False)
|
||||||
|
ietf_contact_is_submitter = forms.BooleanField(required=False)
|
||||||
|
if "holder_contact" in section_list:
|
||||||
|
holder_contact = ContactForm(prefix="hold")
|
||||||
|
if "ietf_contact" in section_list:
|
||||||
|
ietf_contact = ContactForm(prefix="ietf")
|
||||||
|
if "submitter" in section_list:
|
||||||
|
submitter = ContactForm(prefix="subm")
|
||||||
def __init__(self, *args, **kw):
|
def __init__(self, *args, **kw):
|
||||||
for contact in ["holder_contact", "ietf_contact", "submitter"]:
|
for contact in ["holder_contact", "ietf_contact", "submitter"]:
|
||||||
if contact in section_list:
|
if contact in section_list:
|
||||||
self.base_fields[contact] = ContactForm(prefix=contact[:4], *args, **kw)
|
self.base_fields[contact] = ContactForm(prefix=contact[:4], *args, **kw)
|
||||||
|
self.base_fields["ietf_contact_is_submitter"] = forms.BooleanField(required=False)
|
||||||
BaseIprForm.__init__(self, *args, **kw)
|
BaseIprForm.__init__(self, *args, **kw)
|
||||||
# Special validation code
|
# Special validation code
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -138,10 +173,24 @@ def new(request, type):
|
||||||
# Submitter form filled in or 'same-as-ietf-contact' marked
|
# Submitter form filled in or 'same-as-ietf-contact' marked
|
||||||
# Only one of rfc, draft, and other info fields filled in
|
# Only one of rfc, draft, and other info fields filled in
|
||||||
# RFC exists or draft exists and has right rev. or ...
|
# RFC exists or draft exists and has right rev. or ...
|
||||||
|
if self.ietf_contact_is_submitter:
|
||||||
|
self.submitter = self.ietf_contact
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = IprForm(request.POST)
|
data = request.POST.copy()
|
||||||
|
if "ietf_contact_is_submitter" in data:
|
||||||
|
for subfield in ["name", "title", "department", "address1", "address2", "telephone", "fax", "email"]:
|
||||||
|
log("Fixing subfield subm_%s ..."%subfield)
|
||||||
|
try:
|
||||||
|
data["subm_%s"%subfield] = data["ietf_%s"%subfield]
|
||||||
|
log("Set to %s"%data["ietf_%s"%subfield])
|
||||||
|
except Exception, e:
|
||||||
|
log("Caught exception: %s"%e)
|
||||||
|
pass
|
||||||
|
form = IprForm(data)
|
||||||
|
if form.ietf_contact_is_submitter:
|
||||||
|
form.ietf_contact_is_submitter_checked = "checked"
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
#instance = form.save()
|
#instance = form.save()
|
||||||
#return HttpResponseRedirect("/ipr/ipr-%s" % instance.ipr_id)
|
#return HttpResponseRedirect("/ipr/ipr-%s" % instance.ipr_id)
|
||||||
|
@ -154,14 +203,9 @@ def new(request, type):
|
||||||
form = IprForm()
|
form = IprForm()
|
||||||
form.unbound_form = True
|
form.unbound_form = True
|
||||||
|
|
||||||
return render("ipr/details.html", {"ipr": form, "section_list":section_list, "debug": ""})
|
# ietf.utils.log(dir(form.ietf_contact_is_submitter))
|
||||||
|
return render("ipr/details.html", {"ipr": form, "section_list":section_list, "debug": debug})
|
||||||
|
|
||||||
def detail_field_fixup(field):
|
|
||||||
if field.name == "licensing_option":
|
|
||||||
return forms.IntegerField(widget=forms.RadioSelect(choices=models.LICENSE_CHOICES))
|
|
||||||
if field.name in ["selecttype", "selectowned"]:
|
|
||||||
return forms.IntegerField(widget=forms.RadioSelect(choices=((1, "YES"), (2, "NO"))))
|
|
||||||
return field.formfield()
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Helper functions ------------------------------------------------------
|
# ---- Helper functions ------------------------------------------------------
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
<form method="post">
|
<form name="form1" method="post">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -348,6 +348,14 @@
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% if ipr.submitter.name %}
|
{% if ipr.submitter.name %}
|
||||||
|
{% if ipr.ietf_contact_is_submitter %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
Same as in Section III above:
|
||||||
|
<input type="checkbox" name="ietf_contact_is_submitter" onChange="toggle_submitter_info(this.checked);" {{ ipr.ietf_contact_is_submitter_checked }}></td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr><td class="fixwidth">Name:</td> <td><b>{{ ipr.submitter.name }}</b></td></tr>
|
<tr><td class="fixwidth">Name:</td> <td><b>{{ ipr.submitter.name }}</b></td></tr>
|
||||||
<tr><td class="fixwidth">Title:</td> <td><b>{{ ipr.submitter.title }}</b></td></tr>
|
<tr><td class="fixwidth">Title:</td> <td><b>{{ ipr.submitter.title }}</b></td></tr>
|
||||||
<tr><td class="fixwidth">Department:</td> <td><b>{{ ipr.submitter.department }}</b></td></tr>
|
<tr><td class="fixwidth">Department:</td> <td><b>{{ ipr.submitter.department }}</b></td></tr>
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
padding:2px;
|
padding:2px;
|
||||||
border-width:1px;
|
border-width:1px;
|
||||||
border-style:solid;
|
border-style:solid;
|
||||||
border-color:305076;
|
border-color:#305076;
|
||||||
}
|
}
|
||||||
.ipr th { border: 0px; margin: 0px; padding: 4px; }
|
.ipr th { border: 0px; margin: 0px; padding: 4px; }
|
||||||
.ipr td { border: 0px; margin: 0px; padding: 4px; }
|
.ipr td { border: 0px; margin: 0px; padding: 4px; }
|
||||||
td.fixwidth { width: 14ex; }
|
td.fixwidth { width: 14ex; }
|
||||||
.ipr ul { padding-left: -2ex; list-style-type: none; }
|
.ipr ul { list-style-type: none; }
|
||||||
h4.ipr { text-align: center; }
|
h4.ipr { text-align: center; }
|
||||||
input { width: 72ex; font-family: sans-serif; font-size: 11pt; font-weight: normal; }
|
input { width: 72ex; font-family: sans-serif; font-size: 11pt; font-weight: normal; }
|
||||||
input[type="radio"] { width: auto; }
|
input[type="radio"] { width: auto; }
|
||||||
|
@ -25,3 +25,38 @@
|
||||||
.required { color: red; float: right; padding-top: 0.7ex; font-size: 130%; }
|
.required { color: red; float: right; padding-top: 0.7ex; font-size: 130%; }
|
||||||
.errorlist { background: red; padding: 0 0 0 2px; border: 0px; margin: 0px; }
|
.errorlist { background: red; padding: 0 0 0 2px; border: 0px; margin: 0px; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
function toggle_submitter_info (checked) {
|
||||||
|
if (checked) {
|
||||||
|
document.form1.subm_name.value = document.form1.ietf_name.value;
|
||||||
|
document.form1.subm_title.value = document.form1.ietf_title.value;
|
||||||
|
document.form1.subm_department.value = document.form1.ietf_department.value;
|
||||||
|
document.form1.subm_telephone.value = document.form1.ietf_telephone.value;
|
||||||
|
document.form1.subm_fax.value = document.form1.ietf_fax.value;
|
||||||
|
document.form1.subm_email.value = document.form1.ietf_email.value;
|
||||||
|
document.form1.subm_address1.value = document.form1.ietf_address1.value;
|
||||||
|
document.form1.subm_address2.value = document.form1.ietf_address2.value;
|
||||||
|
} else {
|
||||||
|
document.form1.subm_name.value = "";
|
||||||
|
document.form1.subm_title.value = "";
|
||||||
|
document.form1.subm_department.value = "";
|
||||||
|
document.form1.subm_telephone.value = "";
|
||||||
|
document.form1.subm_fax.value = "";
|
||||||
|
document.form1.subm_email.value = "";
|
||||||
|
document.form1.subm_address1.value = "";
|
||||||
|
document.form1.subm_address2.value = "";
|
||||||
|
}
|
||||||
|
document.form1.subm_name.disabled = checked;
|
||||||
|
document.form1.subm_title.disabled = checked;
|
||||||
|
document.form1.subm_department.disabled = checked;
|
||||||
|
document.form1.subm_telephone.disabled = checked;
|
||||||
|
document.form1.subm_fax.disabled = checked;
|
||||||
|
document.form1.subm_email.disabled = checked;
|
||||||
|
document.form1.subm_address1.disabled = checked;
|
||||||
|
document.form1.subm_address2.disabled = checked;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -52,111 +52,6 @@ class FKAsOneToOne(object):
|
||||||
setattr(instance, self.field, value)
|
setattr(instance, self.field, value)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def split_form(html, blocks):
|
|
||||||
"""Split the rendering of a form into a dictionary of named blocks.
|
|
||||||
|
|
||||||
Takes the html of the rendered form as the first argument.
|
|
||||||
|
|
||||||
Expects a dictionary as the second argument, with desired block
|
|
||||||
name and a field specification as key:value pairs.
|
|
||||||
|
|
||||||
The field specification can be either a list of field names, or
|
|
||||||
a string with the field names separated by whitespace.
|
|
||||||
|
|
||||||
The return value is a new dictionary, with the same keys as the
|
|
||||||
block specification dictionary, and the form rendering matching
|
|
||||||
the specified keys as the value.
|
|
||||||
|
|
||||||
Any line in the rendered form which doesn't match any block's
|
|
||||||
field list will cause an exception to be raised.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
output = dict([(block,[]) for block in blocks])
|
|
||||||
# handle field lists in string form
|
|
||||||
for block in blocks:
|
|
||||||
if type(blocks[block]) == type(""):
|
|
||||||
blocks[block] = blocks[block].split()
|
|
||||||
|
|
||||||
# collapse radio button html to one line
|
|
||||||
html = re.sub('\n(.*type="radio".*\n)', "\g<1>", html)
|
|
||||||
html = re.sub('(?m)^(.*type="radio".*)\n', "\g<1>", html)
|
|
||||||
|
|
||||||
for line in html.split('\n'):
|
|
||||||
found = False
|
|
||||||
for block in blocks:
|
|
||||||
for field in blocks[block]:
|
|
||||||
if ('name="%s"' % field) in line:
|
|
||||||
output[block].append(line)
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
raise LookupError("Could not place line in any section: '%s'" % line)
|
|
||||||
|
|
||||||
for block in output:
|
|
||||||
output[block] = "\n".join(output[block])
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
def mk_formatting_form(format="<span>%(label)s</span><span><ul>%(errors)s</ul>%(field)s%(help_text)s</span>",
|
|
||||||
labelfmt="%s:", fieldfmt="%s", errfmt="<li>%s</li>", error_wrap="<ul>%s</ul>", helpfmt="%s"):
|
|
||||||
"""Create a form class which formats its fields using the provided format string(s).
|
|
||||||
|
|
||||||
The format string may use these format specifications:
|
|
||||||
%(label)s
|
|
||||||
%(errors)s
|
|
||||||
%(field)s
|
|
||||||
%(help_text)s
|
|
||||||
|
|
||||||
The individual sub-formats must contain "%s" if defined.
|
|
||||||
"""
|
|
||||||
class FormattingForm(forms.BaseForm):
|
|
||||||
_format = format
|
|
||||||
_labelfmt = labelfmt
|
|
||||||
_fieldfmt = fieldfmt
|
|
||||||
_errfmt = errfmt
|
|
||||||
_errwrap = error_wrap
|
|
||||||
_helpfmt = helpfmt
|
|
||||||
def __getitem__(self, name):
|
|
||||||
"Returns a BoundField with the given name."
|
|
||||||
# syslog.syslog("FormattingForm.__getitem__(%s)" % (name))
|
|
||||||
try:
|
|
||||||
field = self.fields[name]
|
|
||||||
except KeyError:
|
|
||||||
# syslog.syslog("Exception: FormattingForm.__getitem__: Key %r not found" % (name))
|
|
||||||
raise KeyError('Key %r not found in Form' % name)
|
|
||||||
|
|
||||||
if not isinstance(field, forms.fields.Field):
|
|
||||||
return field
|
|
||||||
|
|
||||||
try:
|
|
||||||
bf = forms.forms.BoundField(self, field, name)
|
|
||||||
except Exception, e:
|
|
||||||
# syslog.syslog("Exception: FormattingForm.__getitem__: %s" % (e))
|
|
||||||
raise Exception(e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
error_txt = "".join([self._errfmt % escape(error) for error in bf.errors])
|
|
||||||
error_txt = error_txt and self._errwrap % error_txt
|
|
||||||
label_txt = bf.label and self._labelfmt % bf.label_tag(escape(bf.label)) or ''
|
|
||||||
field_txt = self._fieldfmt % unicode(bf)
|
|
||||||
help_txt = field.help_text and self._helpfmt % field.help_text or u''
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
# syslog.syslog("Exception: FormattingForm.__getitem__: %s" % (e))
|
|
||||||
raise Exception(e)
|
|
||||||
|
|
||||||
return self._format % {"label":label_txt, "errors":error_txt, "field":field_txt, "help_text":help_txt}
|
|
||||||
|
|
||||||
def add_prefix(self, field_name):
|
|
||||||
return self.prefix and ('%s_%s' % (self.prefix, field_name)) or field_name
|
|
||||||
|
|
||||||
|
|
||||||
# syslog.syslog("Created new FormattingForm class: %s" % FormattingForm)
|
|
||||||
|
|
||||||
return FormattingForm
|
|
||||||
|
|
||||||
|
|
||||||
def makeFormattingForm(template=None):
|
def makeFormattingForm(template=None):
|
||||||
"""Create a form class which formats its fields using the provided template
|
"""Create a form class which formats its fields using the provided template
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue