A Django helper app to add editing capabilities to the frontend using modal forms
Project description
1 django-frontend-forms
A Django helper app to add editing capabilities to the frontend using modal forms.
Bases on my previous research as documented here: Editing Django models in the front end
1.1 Installation
Install the package by running:
pip install django-frontend-forms
or:
pip install git+https://github.com/morlandi/django-frontend-forms
In your settings, add:
INSTALLED_APPS = [
...
'frontend_forms',
]
In your base template, add:
<link rel='stylesheet' href="{% static 'frontend_forms/css/frontend_forms.css' %}">
<script src="{% static 'frontend_forms/js/frontend_forms.jsx' %}" type="text/jsx"></script>
{% include 'frontend_forms/dialogs.html' %}
Also, setup handling of “.jsx” files; for example using Babel:
COMPRESS_PRECOMPILERS = ( ... ('text/jsx', 'cat {infile} | ./node_modules/babel-cli/bin/babel.js --presets babel-preset-es2015 > {outfile}'), )
and for local debugging:
# Remove js tranpiling for easier debugging COMPRESS_PRECOMPILERS = ( ... # !!! ('text/jsx', 'cat {infile} | ./node_modules/babel-cli/bin/babel.js --presets babel-preset-es2015 > {outfile}'), ('text/jsx', 'cat {infile} | ./node_modules/babel-cli/bin/babel.js > {outfile}'), )
then:
npm install babel-cli
npm install babel-preset-es2015
npm install babel-preset-stage-2
1.2 Basic Usage
In the following example, we build a Dialog() object providing some custom options; then, we use it to open a modal dialog and load it from the specified url.
For demonstration purposes, we also subscribe the ‘created’ notification.
<script language="javascript">
$(document).ready(function() {
dialog1 = new Dialog({
html: '<h1>Loading ...</h1>',
url: '{% url 'frontend:j_object' %}',
width: '400px',
min_height: '200px',
title: '<i class="fa fa-calculator"></i> Selezione Oggetto',
footer_text: 'testing dialog ...',
enable_trace: true,
callback: function(event_name, dialog, params) {
switch (event_name) {
case "created":
console.log('Dialog created: dialog=%o, params=%o', dialog, params);
break;
}
}
});
});
</script>
<a href="#" class="btn btn-primary pull-right" onclick="dialog1.open(event); return false;">
<i class="fa fa-plus-circle"></i>
Test Popup
</a>
1.3 Open the Dialog and perform some actions after content has been loaded
In the following example:
we subscribe the ‘loaded’ event
we call open() with show=false, so the Dialog will remain hidden during loading
after loading is completed, our handle is called
in this handle, we show the dialog and hide it after a 3 seconds timeout
Sample usage in a template:
<script language="javascript">
$(document).ready(function() {
dialog2 = new Dialog({
url: "{% url 'frontend:j_object' %}",
width: '400px',
min_height: '200px',
enable_trace: true,
callback: dialog2_callback
});
});
function dialog2_callback(event_name, dialog, params) {
switch (event_name) {
case "loaded":
dialog.show();
setTimeout(function() {
dialog.close();
}, 3000);
break;
}
}
</script>
<a href="#" onclick="dialog2.open(event, show=false); return false;">
<i class="fa fa-plus-circle"></i>
Test Popup (2)
</a> /
1.4 Example: editing a Django Model from a Dialog
TODO: TO BE REFINED … AND VERIFIED ;)
First of all, we need a view for form rendering and submission.
For example:
@login_required
@never_cache
def edit_something(request, id_object=None):
# if not request.user.has_perm('backend.view_something') or not request.is_ajax():
# raise PermissionDenied
if id_object is not None:
object = get_object_or_404(Something, id=id_object)
else:
object = None
template_name = 'frontend_forms/generic_form_inner.html'
if request.method == 'POST':
form = SomethingForm(data=request.POST, instance=object)
if form.is_valid():
object = form.save(request)
if not request.is_ajax():
# reload the page
next = request.META['PATH_INFO']
return HttpResponseRedirect(next)
# if is_ajax(), we just return the validated form, so the modal will close
else:
form = SomethingForm()
return render(request, template_name, {
'form': form,
'object': object, # unused, but armless
})
where:
class SomethingForm(forms.ModelForm):
class Meta:
model = Someghing
exclude = []
...
and an endpoint for Ajax call:
File “urls.py” …
path('j/edit_something/<int:id_object>/', ajax.edit_something, name='j_edit_something'),
We can finally use the form in a Dialog:
$(document).ready(function() {
dialog1 = new Dialog({
dialog_selector: '#dialog_generic',
html: '<h1>Loading ...</h1>',
url: '/j/edit_something/{{ object.id }}/',
width: '400px',
min_height: '200px',
title: '<i class="fa fa-add"></i> Edit',
footer_text: '',
enable_trace: true,
callback: function(event_name, dialog, params) {
switch (event_name) {
case "created":
console.log('Dialog created: dialog=%o, params=%o', dialog, params);
break;
case "submitted":
FrontendForms.hide_mouse_cursor();
FrontendForms.reload_page(true);
break;
}
}
});
});
1.5 Example: generic Form submission from a Dialog
We start by creating a view for form rendering and submission:
file ajax.py:
import time
from django.contrib.auth.decorators import login_required
from django.views.decorators.cache import never_cache
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
@login_required
@never_cache
def select_contract(request):
# if settings.DEBUG:
# time.sleep(0.5);
if not request.user.has_perm('backend.view_contract') or not request.is_ajax():
raise PermissionDenied
#template_name = 'frontend/dialogs/generic_form_inner_with_video.html'
template_name = 'dashboard/dialogs/select_contract.html'
object = None
if request.method == 'POST':
form = SelectContractForm(request=request, data=request.POST)
if form.is_valid():
object = form.save(request)
if not request.is_ajax():
# reload the page
next = request.META['PATH_INFO']
return HttpResponseRedirect(next)
# if is_ajax(), we just return the validated form, so the modal will close
else:
form = SelectContractForm(request=request)
return render(request, template_name, {
'form': form,
'object': object, # unused, but armless
})
and provide an endpoint to it for ajax call:
file urls.py
from django.urls import path
from . import ajax
app_name = 'dashboard'
urlpatterns = [
...
path('j/select_contract/', ajax.select_contract, name='j_select_contract'),
...
]
The Form in this example does a few interesting things:
includes some specific assets declaring an inner Media class
receives the request upon construction
uses it to provide specific initial values to the widgets
provides some specific validations with clean()
encapsulates in save() all actions required after successfull submission
file forms.py:
import json
import datetime
from django import forms
from selectable.forms import AutoCompleteWidget, AutoCompleteSelectWidget, AutoComboboxSelectWidget
from backend.models import Contract
from django.utils.safestring import mark_safe
from .lookups import ContractLookup
class SelectContractForm(forms.Form):
contract = forms.CharField(
label='Contract',
widget=AutoComboboxSelectWidget(ContractLookup, limit=10),
required=True,
help_text=mark_safe(" "),
)
today = forms.BooleanField(label="Oggi", required=False)
date = forms.DateField(widget=forms.DateInput(), label='', required=False)
class Media:
css = {
'screen': ('dashboard/css/select_contract_form.css', )
}
js = ('dashboard/js/select_contract_form.js', )
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date'].widget = forms.DateInput(attrs={'class': 'datepicker'})
assert request.user.is_authenticated and request.user.is_active
self.fields['contract'].initial = request.user.contract_attivo
self.fields['date'].initial = request.user.data_attiva
self.fields['today'].initial = request.user.data_attiva is None
def lookup_contract(self):
try:
contract = Contract.objects.get(
id=self.cleaned_data['contract']
)
except Contract.DoesNotExist:
contract = None
return contract
def clean(self):
cleaned_data = self.cleaned_data
if not cleaned_data['today'] and not cleaned_data['date']:
raise forms.ValidationError({
'date': 'Questo campo è obbligatorio'
})
return cleaned_data
def save(self, request):
user = request.user
assert request.user.is_authenticated and request.user.is_active
user.contract_attivo = self.lookup_contract()
if self.cleaned_data['today']:
user.data_attiva = None
else:
user.data_attiva = self.cleaned_data['date']
user.save(update_fields=['contract_attivo', 'data_attiva', ])
The javascript and css assets are used for specific needs of this form:
function onChangeToday(event) {
var controller = $('#id_today');
var value = controller.is(":checked");
$('#id_date').prop('disabled', value);
$('.field-date .ui-datepicker-trigger').prop('disabled', value);
if (value) {
$('#id_date').datepicker('setDate', null);
}
}
$(document).ready(function() {
$('#id_today').on('change', onChangeToday);
onChangeToday();
});
In the template, remember to include the Form’s assets:
{% load i18n frontend_forms_tags %}
{{ form.media.css }}
<div class="row">
<div class="col-sm-12">
<form action="{{ action }}" method="post" class="form {{form.form_class}}" novalidate autocomplete="off">
{% csrf_token %}
{% if form.errors or form.non_field_errors %}
<p class="errornote">{% trans 'Please correct the error below.' %}</p>
{% endif %}
{% if form.non_field_errors %}
<ul class="errorlist">
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
<fieldset>
{% render_form_field form.contract %}
<div>Data di riferimento:</div>
<div class="data-selection-block">
{% render_form_field form.today %}
{% render_form_field form.date %}
</div>
</fieldset>
<input type="hidden" name="object_id" value="{{ object.id|default:'' }}">
<div class="form-submit-row">
<input type="submit" value="Save" />
</div>
</form>
</div>
</div>
{% if request.is_ajax %}
{{ form.media.js }}
{% endif %}
And finally, the Dialog itself;
please note that we use the loaded event notification to rebind the widgets after form rendering.
{% block extrajs %}
<script language="javascript">
$(document).ready(function() {
dialog1 = new Dialog({
dialog_selector: '#dialog_generic',
html: '',
url: "{% url 'dashboard:j_select_contract' %}",
width: '80%',
max_width: '400px',
min_height: '200px',
button_save_label: 'Salva',
button_close_label: 'Annulla',
title: '<i class="fa fa-file-o"></i> Selezione Contract',
footer_text: '',
enable_trace: true,
callback: function(event_name, dialog, params) {
switch (event_name) {
case "loaded":
bindSelectables();
dialog.element.find(".datepicker").datepicker({});
break;
case "submitted":
FrontendForms.reload_page(show_layer=true);
break;
}
}
});
$('.btn-cambia-contract').off().on('click', function(event) {
event.preventDefault();
dialog1.open();
})
});
</script>
{% endblock extrajs %}
1.6 Dialog class public methods
constructor(options={})
open(event, show=true)
close()
show()
Options (with default values):
self.options = { dialog_selector: '#dialog_generic', html: '', url: '', width: null, min_width: null, max_width: null, height: null, min_height: null, max_height: null, button_save_label: 'Save', button_close_label: 'Cancel', title: '', footer_text: '', enable_trace: false, callback: null };
1.7 Default dialog layout
When contructing a Dialog, you can use the dialog_selector option to select which HTML fragment of the page will be treated as the dialog to work with.
It is advisable to use an HTML structure similar to the default layout:
<div id="dialog_generic" class="dialog draggable">
<div class="dialog-dialog">
<div class="dialog-content">
<div class="dialog-header">
<span class="spinner">
<i class="fa fa-spinner fa-spin"></i>
</span>
<span class="close">×</span>
<div class="title">Title</div>
</div>
<div class="dialog-body ui-front">
</div>
<div class="dialog-footer">
<input type="submit" value="Close" class="btn btn-close" />
<input type="submit" value="Save" class="btn btn-save" />
<div class="text">footer</div>
</div>
</div>
</div>
</div>
Notes:
“.draggable” make the Dialog draggable
adding “.ui-front” to the “.dialog-box” element helps improving the behaviour of the dialog on a mobile client
1.8 Notifications
During it’s lifetime, the Dialog will notify all interesting events to the caller, provided he supplies a suitable callback in the contructor:
self.options.callback(event_name, dialog, params)
Example:
dialog1 = new Dialog({
...
callback: function(event_name, dialog, params) {
console.log('event_name: %o, dialog: %o, params: %o', event_name, dialog, params);
}
});
Result:
event_name: "created", dialog: Dialog {options: {…}, element: …}, params: {options: {…}} event_name: "initialized", dialog: Dialog {options: {…}, element: …}, params: {} event_name: "open", dialog: Dialog {options: {…}, element: …}, params: {} event_name: "shown", dialog: Dialog {options: {…}, element: …}, params: {} event_name: "loading", dialog: Dialog {options: {…}, element: …}, params: {url: "/admin_ex/popup/"} event_name: "loaded", dialog: Dialog {options: {…}, element: …}, params: {url: "/admin_ex/popup/"} event_name: "submitting", dialog: Dialog {options: {…}, element: …}, params: {method: "post", url: "/admin_ex/popup/", data: "text=&number=aaa"} event_name: "submitted", dialog: Dialog {options: {…}, element: …}, params: {method: "post", url: "/admin_ex/popup/", data: "text=111&number=111"} event_name: "closed", dialog: Dialog {options: {…}, element: …}, params: {}
You can also trace all events in the console setting the boolean flag enable_trace.
Event list:
event_name |
params |
---|---|
created |
options |
closed |
|
initialized |
|
shown |
|
loading |
url |
loaded |
url |
open |
|
submitting |
method, url, data |
submitted |
method, url, data |
1.9 Settings
- FRONTEND_FORMS_FORM_LAYOUT_FLAVOR
- Default flavor for form rendering
Default: “generic”
Accepted values: “generic”, “bs4”
1.10 “bs4” flavor
Add the .compact-fields class to the form to modify the layout as in the right picture below:
1.11 Utilities (module FrontendForms)
display_server_error(errorDetails)
redirect(url, show_layer=false)
gotourl(url, show_layer=false)
reload_page(show_layer=false)
overlay_show(element)
overlay_hide(element)
hide_mouse_cursor()
logObject(element, obj)
dumpObject(obj, max_depth, depth)
isEmptyObject(obj)
cloneObject(obj)
lookup(array, prop, value)
adjust_canvas_size(id)
getCookie(name)
confirmRemoteAction(url, options, afterDoneCallback, data=null)
downloadFromAjaxPost(url, params, headers, callback)
querystring_parse(qs, sep, eq, options)
set_datepicker_defaults(language_code)
apply_multiselect(elements)
1.12 Form rendering helpers
A render_form(form, flavor=None) template tag is available for form rendering:
{% load frontend_forms_tags ... %}
<form method="post">
{% csrf_token %}
{% render_form form %}
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary btn-block">{% trans 'Submit' %}</button>
</div>
</form>
For more a more advanced customization, you can use render_form_field(field, flavor=None, extra_attrs=’’) instead:
{% load frontend_forms_tags ... %}
<form method="post">
{% csrf_token %}
{% if form.non_field_errors %}
<ul class="errorlist">
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
<fieldset>
{% render_form_field form.username extra_attrs="autocomplete=^off,autocorrect=off,autocapitalize=none" %}
{% render_form_field form.password extra_attrs="autocomplete=^off,autocorrect=off,autocapitalize=none" %}
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary btn-block">{% trans 'Submit' %}</button>
</div>
</form>
In this second example, we supply extra_attrs attributes to each form field; these will be added to the attributes already derived from the Django Form field definitions.
The special prefix ^ will be removed from the attribute, and interpreted as “replace” instead of “append”.
A generic template is also available:
generic_form_inner.html:
{% load i18n frontend_forms_tags %}
<div class="row">
<div class="col-sm-12">
<form action="{{ action }}" method="post" class="form" novalidate autocomplete="off">
{% csrf_token %}
{% render_form form %}
<input type="hidden" name="object_id" value="{{ object.id|default:'' }}">
<div class="form-submit-row">
<input type="submit" value="Save" />
</div>
</form>
</div>
</div>
Please note that, as a convenience when editing a Django Model, we’ve added an hidden field object_id; in other occasions, this is useless (but also armless, as long as the form doesn’t contain a field called “object”).
1.13 Datepicker support
A basic support is provided for jquery-ui datepicker.
Follow these steps:
Initialize datepicker default by calling FrontendForms.set_datepicker_defaults(language_code) once:
<script language="javascript">
$(document).ready(function() {
moment.locale('it');
FrontendForms.set_datepicker_defaults('{{LANGUAGE_CODE}}'); <-------------
...
In your form, make sure that the datepicker class is assigned to the input element; for example:
class MyForm(forms.Form):
date = forms.DateField(widget=forms.DateInput())
...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['date'].widget = forms.DateInput(attrs={'class': 'datepicker'})
If loading the form in a dialog, rebind as necessary:
dialog1 = new Dialog({
...
callback: function(event_name, dialog, params) {
switch (event_name) {
case "loaded":
bindSelectables();
dialog.element.find(".datepicker").datepicker({}); <-------------
break;
...
}
}
});
1.14 jQuery MultiSelect support
Requirements:
<link rel="stylesheet" type="text/css" href="{% static 'multiselect/css/multi-select.css' %}" /> <script src="{% static 'multiselect/js/jquery.multi-select.js' %}"></script> <script src="{% static 'jquery.quicksearch/dist/jquery.quicksearch.min.js' %}"></script>
Follow these steps:
In your form, add the multiselect class to the SelectMultiple() widget
class MyForm(forms.ModelForm):
...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['operators'].widget.attrs = {'class': 'multiselect'}
Later on, bind the widget using apply_multiselect() helper:
dialog1 = new Dialog({
...
callback: function(event_name, dialog, params) {
switch (event_name) {
case "loaded":
FrontendForms.apply_multiselect(dialog.element.find('.multiselect'));
break;
...
}
}
});
1.15 django-select2 support
Requirements:
<script src="{% static 'select2/dist/js/select2.min.js' %}"></script> <script src="{% static 'select2/dist/js/i18n/it.js' %}"></script> <script src="{% static 'django_select2/django_select2.js' %}"</script> <script language="javascript"> $( document ).ready(function() { $.fn.select2.defaults.set('language', 'it'); }); </script>
Follow these steps:
In your form, add the multiselect class to the SelectMultiple() widget
from django_select2.forms import HeavySelect2Widget
class MyForm(forms.ModelForm):
...
class Meta:
...
widgets = {
'fieldname': HeavySelect2Widget(
data_url='/url/to/json/response'
)
}
Later on, bind the widget using apply_multiselect() helper:
dialog1 = new Dialog({
...
callback: function(event_name, dialog, params) {
switch (event_name) {
case "loaded":
dialog.element.find('.django-select2').djangoSelect2();
break;
...
}
}
});
2 History
2.1 v0.1.5
autofocus_first_visible_input option added
2.2 v0.1.4
generic Form submission from a Dialog example added to Readme
fix horizontal forms for BS4
add even/odd class to form groups
2.3 v0.1.3
Display checkbox fields errors
Adjust errors styles
2.4 v0.1.2
Optionally provide the request to the Form constructor
Add a class attribute ‘form-app_label-model_name’ to the rendered form
django-select2 support
jQuery MultiSelect support
2.5 v0.1.1
ModalForms module renamed as FrontendForms
optional parameter event added to open()
2.6 v0.1.0
Module renamed from “django-modal-forms” to “django-frontend-forms”
2.7 v0.0.14
Fixes for Django 3; support both int and uuid PKs
2.8 v0.0.13
Configurable FRONTEND_FORMS_FORM_LAYOUT_DEFAULT
2.9 v0.0.12
Support for model forms in a Dialog (undocumented)
2.10 v0.0.11
Datepicker support
2.11 v0.0.10
optional extra_attrs added to render_form_field template tag
2.12 v0.0.9
fix confirmRemoteAction()
2.13 v0.0.8
fix
2.14 v0.0.7
add custom widget attrs when rendering a field with render_form_fields()
2.15 v0.0.6
add “has-error” class when appropriate in render_form_field tag, to trigger errors in modal forms
2.16 v0.0.5
“simpletable” fix
2.17 v0.0.4
“simpletable” styles
2.18 v0.0.3
downloadFromAjaxPost helper JS function added
Display non_field_errors in BS4 form
Prepend fields’ class with ‘field-’ prefix, as Django admin does
Radio buttons and Checkboxs rendering for Bootstrap 4
bs4 form rendering
querystring_parse() utility added
Add object_id hidden field to generic form
.ui-front added to .dialog-body for bette behaviour on mobiles
notify “loaded” event in _form_ajax_submit() when approriate
2.19 v0.0.2
First working release
2.20 v0.0.1
Project start
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Hashes for django_frontend_forms-0.1.5-py2.py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | ca1ebfa4cbccf155b847bba701712d6919aeacd23e13f7273e68804647da29d2 |
|
MD5 | 69a924f200a0090db327432d565bd8d7 |
|
BLAKE2b-256 | a3e522a08f5c2fed630db132ea278fe0771f5929de309a51c386d4ffab9d88c5 |