Source code for ajaxviews.forms
import json
from django.core.urlresolvers import reverse, NoReverseMatch
from django.contrib.admin.templatetags.admin_static import static
from django.template import Template, Context
from django.utils.encoding import force_text
from django.template.loader import get_template
from django.forms.models import BaseModelFormSet
from django.forms import Form, ModelForm, CharField, HiddenInput
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import FormActions
from crispy_forms.layout import LayoutObject, Layout, Submit, HTML
from crispy_forms.utils import render_crispy_form, TEMPLATE_PACK
from .helpers import init_chosen_widget, init_dateinput
[docs]class DefaultFormActions(LayoutObject):
"""
Crispy layout object that renders form actions depending on options defined in ``form.opts`` property.
Keyword arguments available in ``opts``:
:ivar str success_url: Url to redirect to on successful form save.
:ivar str delete_url: Delete view that's requested on form delete.
:ivar str delete_success_url: Url to redirect view on successful form deletion.
:ivar str form_actions_template: Template to render form actions in. Default: ``'ajaxviews/_form_controls.html'``
:ivar int preview_stage: If form preview is displayed render a back button.
:ivar bool modal_form: True if form is displayed in a bootrap modal. Default: ``False``
:ivar bool delete_confirmation: Display a `bootstrap confirmation <http://bootstrap-confirmation.js.org/>`_
popover if delete button is clicked.
:ivar dict form_cfg: Additional data needed to process form save passed through a hidden input field. Dictionary
is stringified and automatically parsed again when calling :func:`FormMixin.cleaned_form_cfg`.
"""
# noinspection PyUnusedLocal, PyMethodMayBeStatic
def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
success_url = form.opts.get('success_url', '')
delete_url = form.opts.get('delete_url', '')
if delete_url:
delete_url += '&' if '?' in delete_url else '?'
delete_url += 'success_url=' + force_text(form.opts.get('delete_success_url', success_url))
template = get_template(form.opts.get('form_actions_template', 'ajaxviews/_form_controls.html'))
btn_group = template.render({
'delete_url': delete_url,
'success_url': force_text(success_url),
'modal_form': form.opts.get('modal_form', False),
'form_preview': form.opts.get('preview_stage', False),
'delete_confirmation': form.opts.get('delete_confirmation', False),
'form_cfg': json.dumps(form.form_cfg) if getattr(form, 'form_cfg', None) else None,
})
layout_object = FormActions(
Submit('save', form.opts.get('save_button_name', 'Save')),
HTML(btn_group),
style='margin-bottom: 0;'
)
return layout_object.render(form, form_style, context)
[docs]class DefaultFormHelper(FormHelper):
"""
Crispy form helper used to define default form action control.
A ``data-async`` html property is added to the form tag.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs = {'data-async': ''}
[docs] def append_form_actions(self):
"""
Append form actions to the current layout.
"""
self.layout.append(DefaultFormActions())
[docs] def add_form_actions_only(self):
"""
Disable the form tag and add form actions only to the current layout.
"""
self.form_tag = False
self.add_layout(Layout(DefaultFormActions()))
# noinspection PyUnresolvedReferences
[docs]class FormMixin:
"""
Mixin that handels instantiation of crispy form helper and options passed in the form's init kwargs.
:ivar dict opts:
"""
form_kwargs = [
'success_url',
'form_action',
'delete_url',
'delete_success_url',
'modal_form',
'preview_stage',
'model_data',
'preview_data',
'save_button_name',
'init_chosen_widget',
'init_date_widget',
'delete_confirmation',
'form_actions_template',
]
def __init__(self, *args, **kwargs):
self._helper_instance = None
self.form_cfg = kwargs.pop('form_cfg', {})
self.user = kwargs.pop('user', None)
self.opts = {}
for key in list(kwargs):
if key in self.form_kwargs:
self.opts[key] = kwargs.pop(key)
super().__init__(*args, **kwargs)
@property
def helper(self):
"""
The :class:`DefaultFormHelper` is instantiated only once when this helper property is accessed first.
Assign your own form helper if you want to override the default behavior.
This renders hidden fields and appends form actions by default.
:return: Form helper instance
"""
if self._helper_instance is not None:
return self._helper_instance
if self.form_cfg:
self.fields['form_cfg'] = CharField(widget=HiddenInput(), required=False)
self.fields['form_cfg'].initial = json.dumps(self.form_cfg)
try:
self.init_add_fields()
except AttributeError:
pass
helper = DefaultFormHelper(self)
if 'form_action' in self.opts:
helper.form_action = self.opts['form_action']
helper.render_hidden_fields = True
helper.append_form_actions()
self._helper_instance = helper
return helper
@helper.setter
def helper(self, helper):
self._helper_instance = helper
@property
def cleaned_form_cfg(self):
"""
Loads the stringified ``form_cfg`` in ``cleaned_data`` to return a python dictionary object.
:return: form cfg dictionary
"""
if 'form_cfg' in self.cleaned_data:
return json.loads(self.cleaned_data['form_cfg'])
return {}
@property
def layout(self):
"""
Get or set the crispy form helper layout object. If you set a new layout the form actions are
appended automatically.
"""
return self.helper.layout
@layout.setter
def layout(self, layout):
self.helper.add_layout(layout)
self.helper.append_form_actions()
[docs]class SimpleForm(FormMixin, Form):
"""
Generic form for use without a corresponding model. Also used to display a preview before saving a form.
:ivar object object: Model instance of the preview forms first stage.
:ivar dict model_data: Cleaned data of the preview forms second stage.
"""
def __init__(self, *args, **kwargs):
self.object = kwargs.pop('instance', None)
self.model_data = kwargs.pop('model_data', None)
success_message = kwargs.pop('success_message', None)
super().__init__(*args, **kwargs)
if success_message is not None:
self.form_cfg['success_message'] = success_message
if self.opts.get('init_chosen_widget', True):
init_chosen_widget(self.fields.items())
if self.opts.get('init_date_widget', True):
init_dateinput(self.fields.items())
[docs]class GenericModelForm(FormMixin, ModelForm):
"""
Generic form for use with a corresponding model.
"""
field_label_addon = """<a class="modal-link form-add-link" href="{0}"><img src="{1}" alt="{2}"/></a>"""
def __init__(self, *args, **kwargs):
self.json_cache = kwargs.pop('json_cache', {})
super().__init__(*args, **kwargs)
for key, value in self.form_cfg.get('related_obj_ids', {}).copy().items():
field_name = key.replace('_id', '')
if field_name in self.fields:
self.fields[field_name].initial = value
del self.form_cfg['related_obj_ids'][key]
if self.opts.get('init_chosen_widget', True):
init_chosen_widget(self.fields.items())
if self.opts.get('init_date_widget', True):
init_dateinput(self.fields.items())
def init_add_fields(self):
for field_name, url_name in getattr(self.Meta, 'add_fields', {}).items():
try:
url = reverse(url_name)
except NoReverseMatch:
url = reverse(url_name, args=[self.instance.pk])
# self.fields[field_name].label_suffix = "" # suffix not supported by django-crispy-forms
url += '?auto_select_field=' + field_name
self.fields[field_name].label += self.field_label_addon.format(
url, static('admin/img/icon-addlink.svg'), 'Add'
)
def render_form_actions(self):
form = Form()
form.opts = self.opts
form.helper = DefaultFormHelper(self)
form.helper.add_form_actions_only()
return render_crispy_form(form)
def save(self, commit=True):
instance = super().save(commit=commit)
if commit and 'auto_select_field' in self.cleaned_form_cfg:
self.json_cache['auto_select_choice'] = {
'pk': instance.pk,
'field': self.form_cfg['auto_select_field'],
'text': str(instance),
}
return instance
[docs]class ModelFormSet(BaseModelFormSet):
"""
Use this form to render form actions at the bottom of the formset.
:var str form_actions_template: Template to render save and cancel buttons. Be sure to use
the ``{{ success_url }}`` tag for your cancel button if you want to override this template.
"""
form_actions_template = """
<input name="save" class="btn btn-primary" type="submit" value="Save">
<a role="button" class="btn btn-default cancel-btn" href="{{ success_url }}">Cancel</a>
"""
def __init__(self, *args, **kwargs):
self._success_url = kwargs.pop('success_url', None)
super().__init__(*args, **kwargs)
def render_form_actions(self, **kwargs):
kwargs['success_url'] = self._success_url
return Template(self.form_actions_template).render(Context(kwargs))
# helper.form_tag = False
# helper.layout = Layout(
# TabHolder(
# Tab(
# 'Basic Information',
# 'first_name',
# 'last_name'
# ),
# Tab(
# 'Address',
# 'address1',
# 'address2',
# ),
# Tab(
# 'Contact',
# 'email',
# 'mobile',
# )
# )
# )
# from djmoney.forms.widgets import MoneyWidget
# class CustomMoneyWidget(MoneyWidget):
# def format_output(self, rendered_widgets):
# return ('<div class="row">'
# '<div class="col-xs-6 col-sm-10">%s</div>'
# '<div class="col-xs-6 col-sm-2">%s</div>'
# '</div>') % tuple(rendered_widgets)
# class BookingForm(forms.ModelForm):
# ...
# def __init__(self, *args, **kwargs):
# super(BookingForm, self).__init__(*args, **kwargs)
# amount, currency = self.fields['amount'].fields
# self.fields['amount'].widget = CustomMoneyWidget(
# amount_widget=amount.widget, currency_widget=currency.widget)
# <a class="modal-link pull-right" href="{0}" style="margin-top: -3px; margin-left: 5px;">
# <img src="{1}" width="15" height="15" alt="{2}"/>
# </a>