diff --git a/docs/changelog.rst b/docs/changelog.rst index 4b2668d..e3fca35 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,9 @@ Changelog ========= +* :feature:`-` implement password change functionality for mailboxes +* :feature:`-` implement creation of new mailboxes for hosting packages +* :support:`-` move common form code to new module gvawebcore.forms * :feature:`-` make it possible to assign domains to a customer * :feature:`-` add hosting packages list for staff users * :feature:`-` allow creation of new hosting packages for staff users without diff --git a/docs/code.rst b/docs/code.rst index adc9570..e334f04 100644 --- a/docs/code.rst +++ b/docs/code.rst @@ -15,6 +15,7 @@ Common code .. toctree:: code/gvacommon + code/gvawebcore Celery task stubs diff --git a/docs/code/gvawebcore.rst b/docs/code/gvawebcore.rst new file mode 100644 index 0000000..7fdd1b0 --- /dev/null +++ b/docs/code/gvawebcore.rst @@ -0,0 +1,11 @@ +:py:mod:`gvawebcore` +==================== + +.. automodule:: gvawebcore + + +:py:mod:`forms ` +---------------------------------- + +.. automodule:: gvawebcore.forms + :members: diff --git a/docs/code/managemails.rst b/docs/code/managemails.rst index 17c1763..42f060d 100644 --- a/docs/code/managemails.rst +++ b/docs/code/managemails.rst @@ -12,14 +12,35 @@ :py:mod:`apps ` ------------------------------------ +--------------------------------- .. automodule:: managemails.apps :members: +:py:mod:`forms ` +----------------------------------- + +.. automodule:: managemails.forms + :members: + + :py:mod:`models ` ------------------------------------- .. automodule:: managemails.models :members: + + +:py:mod:`urls ` +--------------------------------- + +.. automodule:: managemails.urls + :members: + + +:py:mod:`views ` +----------------------------------- + +.. automodule:: managemails.views + :members: diff --git a/gnuviechadmin/gnuviechadmin/urls.py b/gnuviechadmin/gnuviechadmin/urls.py index ebc4157..d8f621d 100644 --- a/gnuviechadmin/gnuviechadmin/urls.py +++ b/gnuviechadmin/gnuviechadmin/urls.py @@ -12,6 +12,7 @@ urlpatterns = patterns( url(r'^accounts/', include('allauth.urls')), url(r'^domains/', include('domains.urls')), url(r'^hosting/', include('hostingpackages.urls')), + url(r'^mail/', include('managemails.urls')), url(r'^osuser/', include('osusers.urls')), url(r'^admin/', include(admin.site.urls)), ) diff --git a/gnuviechadmin/gvawebcore/__init__.py b/gnuviechadmin/gvawebcore/__init__.py new file mode 100644 index 0000000..21b9749 --- /dev/null +++ b/gnuviechadmin/gvawebcore/__init__.py @@ -0,0 +1,5 @@ +""" +This is a collection of modules that can be used by multiple gnuviechadmin +apps. + +""" diff --git a/gnuviechadmin/gvawebcore/forms.py b/gnuviechadmin/gvawebcore/forms.py new file mode 100644 index 0000000..7a72d83 --- /dev/null +++ b/gnuviechadmin/gvawebcore/forms.py @@ -0,0 +1,43 @@ +""" +This module defines form classes that can be extended by other gnuviechadmin +apps' forms. + +""" +from __future__ import absolute_import, unicode_literals + +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +PASSWORD_MISMATCH_ERROR = _("Passwords don't match") +""" +Error message for non matching passwords. +""" + + +class PasswordModelFormMixin(forms.Form): + """ + A form for entering a password in two password fields. The form checks + whether both fields contain the same string. + + """ + password1 = forms.CharField( + label=_('Password'), widget=forms.PasswordInput, + ) + password2 = forms.CharField( + label=_('Password (again)'), widget=forms.PasswordInput, + ) + + def clean_password2(self): + """ + Check that the two password entries match. + + :return: the validated password + :rtype: str or None + + """ + password1 = self.cleaned_data.get('password1') + password2 = self.cleaned_data.get('password2') + if password1 and password2 and password1 != password2: + raise forms.ValidationError(PASSWORD_MISMATCH_ERROR) + return password2 diff --git a/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..e460e21 --- /dev/null +++ b/gnuviechadmin/gvawebcore/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,32 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: gvawebcore\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-25 11:49+0100\n" +"PO-Revision-Date: 2015-01-25 11:49+0100\n" +"Last-Translator: Jan Dittberner \n" +"Language-Team: Jan Dittberner \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.6.10\n" +"X-Poedit-SourceCharset: UTF-8\n" + +#: forms.py:12 +msgid "Passwords don't match" +msgstr "Passwörter stimmen nicht überein" + +#: forms.py:25 +msgid "Password" +msgstr "Passwort: " + +#: forms.py:28 +msgid "Password (again)" +msgstr "Passwortwiederholung" diff --git a/gnuviechadmin/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/locale/de/LC_MESSAGES/django.po index b70640b..d9a7e5a 100644 --- a/gnuviechadmin/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: gnuviechadmin\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-25 00:46+0100\n" -"PO-Revision-Date: 2015-01-25 00:55+0100\n" +"POT-Creation-Date: 2015-01-25 12:46+0100\n" +"PO-Revision-Date: 2015-01-25 12:49+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" "Language: de\n" @@ -793,6 +793,47 @@ msgstr "Diesem Hostingpaket sind noch keine Datenbanken zugeordnet." msgid "Add database" msgstr "Datenbank hinzufügen" +#: templates/managemails/mailbox_create.html:6 +#: templates/managemails/mailbox_create.html:15 +#, python-format +msgid "Add Mailbox to Hosting Package %(package)s" +msgstr "Postfach zum Hostingpaket %(package)s hinzufügen" + +#: templates/managemails/mailbox_create.html:8 +#: templates/managemails/mailbox_create.html:17 +#, python-format +msgid "Add Mailbox to Hosting Package %(package)s of Customer %(full_name)s" +msgstr "" +"Postfach zum Hostingpaket %(package)s des Kunden %(full_name)s hinzufügen" + +#: templates/managemails/mailbox_create.html:23 +msgid "Please specify the password for your new mailbox." +msgstr "Bitte geben Sie das Passwort für Ihr neues Postfach ein." + +#: templates/managemails/mailbox_create.html:23 +msgid "Please specify the password for the new mailbox." +msgstr "Bitte geben Sie das Passwort für das neue Postfach ein." + +#: templates/managemails/mailbox_setpassword.html:6 +#: templates/managemails/mailbox_setpassword.html:15 +#, python-format +msgid "Set Password for Mailbox %(mailbox)s" +msgstr "Passwort für Postfach %(mailbox)s setzen" + +#: templates/managemails/mailbox_setpassword.html:8 +#: templates/managemails/mailbox_setpassword.html:17 +#, python-format +msgid "Set Password for Mailbox %(mailbox)s of Customer %(full_name)s" +msgstr "Passwort für Postfach %(mailbox)s des Kunden %(full_name)s setzen" + +#: templates/managemails/mailbox_setpassword.html:23 +msgid "Please specify the new password for your mailbox." +msgstr "Bitte geben Sie das neue Passwort für Ihr Postfach ein." + +#: templates/managemails/mailbox_setpassword.html:23 +msgid "Please specify the new password for the mailbox." +msgstr "Bitte geben Sie das neue Passwort für das Postfach ein." + #: templates/osusers/user_setpassword.html:5 #: templates/osusers/user_setpassword.html:13 #, python-format @@ -897,5 +938,10 @@ msgstr "" "%(site_name)s zu nutzen. Als letzten Schritt füllen Sie bitte folgendes " "Formular aus:" +#, fuzzy +#~| msgid "Password Reset" +#~ msgid "Password (again)" +#~ msgstr "Passwort zurücksetzen" + #~ msgid "My Profile" #~ msgstr "Mein Profil" diff --git a/gnuviechadmin/managemails/forms.py b/gnuviechadmin/managemails/forms.py new file mode 100644 index 0000000..04829f8 --- /dev/null +++ b/gnuviechadmin/managemails/forms.py @@ -0,0 +1,81 @@ +""" +This module defines form classes for mailbox and mail address editing. + +""" +from __future__ import absolute_import, unicode_literals + +from django import forms +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit + +from .models import Mailbox +from gvawebcore.forms import PasswordModelFormMixin + + +class CreateMailboxForm(PasswordModelFormMixin, forms.ModelForm): + """ + This form is used to create new Mailbox instances. + + """ + class Meta: + model = Mailbox + fields = [] + + def __init__(self, *args, **kwargs): + self.hosting_package = kwargs.pop('hostingpackage') + super(CreateMailboxForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse( + 'create_mailbox', kwargs={'package': self.hosting_package.id}) + self.helper.add_input(Submit('submit', _('Create mailbox'))) + + def save(self, commit=True): + """ + Set the new mailbox's password and osuser. + + :param boolean commit: whether to save the created mailbox + :return: mailbox instance + :rtype: :py:class:`managemails.models.Mailbox` + + """ + osuser = self.hosting_package.osuser + self.instance.osuser = osuser + self.instance.username = Mailbox.objects.get_next_mailbox_name(osuser) + self.instance.set_password(self.cleaned_data['password1']) + return super(CreateMailboxForm, self).save(commit=commit) + + +class ChangeMailboxPasswordForm(PasswordModelFormMixin, forms.ModelForm): + """ + This form is used to set a new password for an existing mailbox. + + """ + class Meta: + model = Mailbox + fields = [] + + def __init__(self, *args, **kwargs): + self.hosting_package = kwargs.pop('hostingpackage') + super(ChangeMailboxPasswordForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_action = reverse( + 'change_mailbox_password', kwargs={ + 'package': self.hosting_package.id, + 'slug': self.instance.username, + }) + self.helper.add_input(Submit('submit', _('Set password'))) + + def save(self, commit=True): + """ + Set the mailbox password. + + :param boolean commit: whether to save the mailbox instance + :return: mailbox instance + :rtype: :py:class:`managemails.models.Mailbox` + + """ + self.instance.set_password(self.cleaned_data['password1']) + return super(ChangeMailboxPasswordForm, self).save(commit=commit) diff --git a/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po index 52f2d8f..449ad57 100644 --- a/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po +++ b/gnuviechadmin/managemails/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: managemails\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-01-25 00:46+0100\n" -"PO-Revision-Date: 2015-01-25 00:48+0100\n" +"POT-Creation-Date: 2015-01-25 12:46+0100\n" +"PO-Revision-Date: 2015-01-25 12:47+0100\n" "Last-Translator: Jan Dittberner \n" "Language-Team: Jan Dittberner \n" "Language: de\n" @@ -47,6 +47,14 @@ msgstr "Deaktivieren" msgid "Mailboxes and Mail Addresses" msgstr "Postfächer und E-Mailadressen" +#: managemails/forms.py:33 +msgid "Create mailbox" +msgstr "Postfach anlegen" + +#: managemails/forms.py:69 +msgid "Set password" +msgstr "Passwort setzen" + #: managemails/models.py:79 msgid "Mailbox" msgstr "Postfach" @@ -70,3 +78,18 @@ msgstr "E-Mailadresse" #: managemails/models.py:142 msgid "mailbox" msgstr "Postfach" + +#: managemails/views.py:63 +msgid "You are not allowed to add more mailboxes to this hosting package" +msgstr "Sie können keine weiteren Postfächer zu diesem Hostingpaket hinzufügen" + +#: managemails/views.py:82 +#, python-brace-format +msgid "Mailbox {mailbox} created successfully." +msgstr "Postfach {mailbox} erfolgreich angelegt." + +#: managemails/views.py:117 +#, python-brace-format +msgid "Successfully set new password for mailbox {mailbox}." +msgstr "" +"Für das Postfach {mailbox} wurde erfolgreich ein neues Passwort gesetzt." diff --git a/gnuviechadmin/managemails/urls.py b/gnuviechadmin/managemails/urls.py new file mode 100644 index 0000000..5e942d2 --- /dev/null +++ b/gnuviechadmin/managemails/urls.py @@ -0,0 +1,21 @@ +""" +This module defines the URL patterns for mailbox and mail address related +views. + +""" +from __future__ import absolute_import, unicode_literals + +from django.conf.urls import patterns, url + +from .views import ( + ChangeMailboxPassword, + CreateMailbox, +) + +urlpatterns = patterns( + '', + url(r'^(?P\d+)/mailbox/create$', + CreateMailbox.as_view(), name='create_mailbox'), + url(r'^(?P\d+)/mailbox/(?P[\w0-9]+)/setpassword$', + ChangeMailboxPassword.as_view(), name='change_mailbox_password'), +) diff --git a/gnuviechadmin/managemails/views.py b/gnuviechadmin/managemails/views.py new file mode 100644 index 0000000..837edd4 --- /dev/null +++ b/gnuviechadmin/managemails/views.py @@ -0,0 +1,121 @@ +""" +This module defines views for mailbox and mail address handling. + +""" +from __future__ import absolute_import, unicode_literals + +from django.http import HttpResponseForbidden +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import ugettext as _ +from django.views.generic.edit import ( + CreateView, + UpdateView, +) +from django.contrib import messages + +from gvacommon.viewmixins import StaffOrSelfLoginRequiredMixin + +from hostingpackages.models import CustomerHostingPackage + +from .forms import ( + CreateMailboxForm, + ChangeMailboxPasswordForm, +) +from .models import Mailbox + + + +class HostingPackageAndCustomerMixin(object): + """ + Mixin for views that gets the hosting package instance from the URL + keyword argument 'package'. + + """ + hosting_package_kwarg = 'package' + """Keyword argument used to find the hosting package in the URL.""" + + def get_hosting_package(self): + return get_object_or_404( + CustomerHostingPackage, + pk=int(self.kwargs[self.hosting_package_kwarg])) + + def get_customer_object(self): + return self.get_hosting_package().customer + + + +class CreateMailbox( + HostingPackageAndCustomerMixin, StaffOrSelfLoginRequiredMixin, CreateView +): + """ + This view is used to setup new mailboxes for a customer hosting package. + + """ + model = Mailbox + context_object_name = 'mailbox' + template_name_suffix = '_create' + form_class = CreateMailboxForm + + def dispatch(self, request, *args, **kwargs): + resp = super(CreateMailbox, self).dispatch(request, *args, **kwargs) + if not self.get_hosting_package().may_add_mailbox(): + resp = HttpResponseForbidden( + _('You are not allowed to add more mailboxes to this' + ' hosting package')) + return resp + + def get_context_data(self, **kwargs): + context = super(CreateMailbox, self).get_context_data(**kwargs) + context['hostingpackage'] = self.get_hosting_package() + context['customer'] = self.get_customer_object() + return context + + def get_form_kwargs(self): + kwargs = super(CreateMailbox, self).get_form_kwargs() + kwargs['hostingpackage'] = self.get_hosting_package() + return kwargs + + def form_valid(self, form): + mailbox = form.save() + messages.success( + self.request, + _('Mailbox {mailbox} created successfully.').format( + mailbox=mailbox.username + ) + ) + return redirect(self.get_hosting_package()) + + +class ChangeMailboxPassword( + HostingPackageAndCustomerMixin, StaffOrSelfLoginRequiredMixin, UpdateView +): + """ + This view is used to set a new password for an existing mailbox. + + """ + context_object_name = 'mailbox' + form_class = ChangeMailboxPasswordForm + model = Mailbox + slug_field = 'username' + template_name_suffix = '_setpassword' + + def get_context_data(self, **kwargs): + context = super(ChangeMailboxPassword, self).get_context_data(**kwargs) + context['hostingpackage'] = self.get_hosting_package() + context['customer'] = self.get_customer_object() + return context + + def get_form_kwargs(self): + kwargs = super(ChangeMailboxPassword, self).get_form_kwargs() + kwargs['hostingpackage'] = self.get_hosting_package() + return kwargs + + def form_valid(self, form): + mailbox = form.save() + messages.success( + self.request, + _('Successfully set new password for mailbox {mailbox}.').format( + mailbox=mailbox.username + ) + ) + return redirect(self.get_hosting_package()) diff --git a/gnuviechadmin/osusers/admin.py b/gnuviechadmin/osusers/admin.py index 7d757be..6fec6e7 100644 --- a/gnuviechadmin/osusers/admin.py +++ b/gnuviechadmin/osusers/admin.py @@ -6,7 +6,7 @@ from django import forms from django.utils.translation import ugettext as _ from django.contrib import admin -from .forms import ( +from gvawebcore.forms import ( PASSWORD_MISMATCH_ERROR ) from .models import ( diff --git a/gnuviechadmin/osusers/forms.py b/gnuviechadmin/osusers/forms.py index 1ed1c82..73faa02 100644 --- a/gnuviechadmin/osusers/forms.py +++ b/gnuviechadmin/osusers/forms.py @@ -11,28 +11,16 @@ from django.utils.translation import ugettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit +from gvawebcore.forms import PasswordModelFormMixin + from .models import User -PASSWORD_MISMATCH_ERROR = _("Passwords don't match") -""" -Error message for non matching passwords. -""" - -class ChangeOsUserPasswordForm(forms.ModelForm): +class ChangeOsUserPasswordForm(PasswordModelFormMixin, forms.ModelForm): """ A form for setting an OS user's password. """ - password1 = forms.CharField( - label=_('Password'), widget=forms.PasswordInput, - required=False, - ) - password2 = forms.CharField( - label=_('Password (again)'), widget=forms.PasswordInput, - required=False, - ) - class Meta: model = User fields = [] @@ -44,20 +32,6 @@ class ChangeOsUserPasswordForm(forms.ModelForm): 'set_osuser_password', kwargs={'slug': self.instance.username}) self.helper.add_input(Submit('submit', _('Set password'))) - def clean_password2(self): - """ - Check that the two password entries match. - - :return: the validated password - :rtype: str or None - - """ - password1 = self.cleaned_data.get('password1') - password2 = self.cleaned_data.get('password2') - if password1 and password2 and password1 != password2: - raise forms.ValidationError(PASSWORD_MISMATCH_ERROR) - return password2 - def save(self, commit=True): """ Save the provided password in hashed format. diff --git a/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html b/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html index 43f0e2d..25c2e3f 100644 --- a/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html +++ b/gnuviechadmin/templates/hostingpackages/customerhostingpackage_detail.html @@ -131,7 +131,7 @@ {{ mailbox.mailaddresses|join:", " }} {% if mailbox.active %}{% trans "Active" %}{% else %}{% trans "inactive" %}{% endif %} - {% trans "Set mailbox password" %} + {% trans "Set mailbox password" %} {% endfor %} @@ -140,7 +140,7 @@

{% trans "There are no mailboxes assigned to this hosting package yet." %}

{% endif %} {% if hostingpackage.may_add_mailbox %} -

{% trans "Add mailbox" %}

+

{% trans "Add mailbox" %}

{% endif %} diff --git a/gnuviechadmin/templates/managemails/base.html b/gnuviechadmin/templates/managemails/base.html new file mode 100644 index 0000000..94d9808 --- /dev/null +++ b/gnuviechadmin/templates/managemails/base.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/gnuviechadmin/templates/managemails/mailbox_create.html b/gnuviechadmin/templates/managemails/mailbox_create.html new file mode 100644 index 0000000..7446d6c --- /dev/null +++ b/gnuviechadmin/templates/managemails/mailbox_create.html @@ -0,0 +1,34 @@ +{% extends "managemails/base.html" %} +{% load i18n crispy_forms_tags %} +{% block title %}{{ block.user }} - {% spaceless %} +{% with full_name=customer.get_full_name package=hostingpackage.name %} +{% if customer == user %} +{% blocktrans %}Add Mailbox to Hosting Package {{ package }}{% endblocktrans %} +{% else %} +{% blocktrans %}Add Mailbox to Hosting Package {{ package }} of Customer {{ full_name }}{% endblocktrans %} +{% endif %} +{% endwith %} +{% endspaceless %}{% endblock title %} +{% block page_title %}{% spaceless %} +{% with full_name=customer.get_full_name package=hostingpackage.name %} +{% if customer == user %} +{% blocktrans %}Add Mailbox to Hosting Package {{ package }}{% endblocktrans %} +{% else %} +{% blocktrans %}Add Mailbox to Hosting Package {{ package }} of Customer {{ full_name }}{% endblocktrans %} +{% endif %} +{% endwith %} +{% endspaceless %}{% endblock page_title %} + +{% block content %} +

{% if customer == user %}{% trans "Please specify the password for your new mailbox." %}{% else %}{% trans "Please specify the password for the new mailbox." %}{% endif %}

+{% crispy form %} +{% endblock content %} + +{% block extra_js %} + +{% endblock extra_js %} diff --git a/gnuviechadmin/templates/managemails/mailbox_setpassword.html b/gnuviechadmin/templates/managemails/mailbox_setpassword.html new file mode 100644 index 0000000..10829b5 --- /dev/null +++ b/gnuviechadmin/templates/managemails/mailbox_setpassword.html @@ -0,0 +1,34 @@ +{% extends "managemails/base.html" %} +{% load i18n crispy_forms_tags %} +{% block title %}{{ block.user }} - {% spaceless %} +{% with full_name=customer.get_full_name mailbox=mailbox.username %} +{% if customer == user %} +{% blocktrans %}Set Password for Mailbox {{ mailbox }}{% endblocktrans %} +{% else %} +{% blocktrans %}Set Password for Mailbox {{ mailbox }} of Customer {{ full_name }}{% endblocktrans %} +{% endif %} +{% endwith %} +{% endspaceless %}{% endblock title %} +{% block page_title %}{% spaceless %} +{% with full_name=customer.get_full_name mailbox=mailbox.username %} +{% if customer == user %} +{% blocktrans %}Set Password for Mailbox {{ mailbox }}{% endblocktrans %} +{% else %} +{% blocktrans %}Set Password for Mailbox {{ mailbox }} of Customer {{ full_name }}{% endblocktrans %} +{% endif %} +{% endwith %} +{% endspaceless %}{% endblock page_title %} + +{% block content %} +

{% if customer == user %}{% trans "Please specify the new password for your mailbox." %}{% else %}{% trans "Please specify the new password for the mailbox." %}{% endif %}

+{% crispy form %} +{% endblock content %} + +{% block extra_js %} + +{% endblock extra_js %}