GitLab will be upgraded on 30 Jan 2023 from 2.00 pm (AEDT) to 3.00 pm (AEDT). During the update, GitLab and Mattermost services will not be available. If you have any concerns with this, please talk to us at N110 (b) CSIT building.

Commit bf1c0911 authored by Lincoln Smith's avatar Lincoln Smith
Browse files

Modified table_inline_formsets to support full Task form.

- Modified crispy form table_inline_formset templates to make them more easily re-usable.
- Updated bootstrap version to fix IE rendering errors and added fontawesome js/css.
- Removed redundant template files, updated CSS and JS to work with collapsible formset forms.
- Changed crispy forms helper classes so default helper better matches default behaviour of templates.
- Added simple tests for simple template filters.
- Fixed fallback javascript time/date picker application and value formatting.
parent f593b2f5
......@@ -47,27 +47,29 @@ class CIMSBaseModelForm(PFPModelForm):
class BaseFormHelper(FormHelper):
"""
Since we often have multiple forms on a page by default disable rendering of form tags and the
CSRF block.
CSRF block. Disable HTML5 required attributes by default as they don't play nicely in dynamic
formsets.
"""
form_tag = False
disable_csrf = True
html5_required = True
html5_required = False
class DefaultFormHelper(BaseFormHelper):
"""
The default formhelper is the base class form rendering the form for the core object for the
view, so make it render the CSRF block by default so we don't forget it.
The default formhelper for rendering the form for the core object of the
view, so make it render the CSRF block by default so we don't forget it. Turn on HTML5 required
attributes for this part of the form.
"""
disable_csrf = False
label_class = 'col-2 col-form-label'
field_class = 'col-6'
html5_required = True
class VerticalFormHelper(BaseFormHelper):
"""The default formhelper to use if you need a vertical form i.e. labels above inputs."""
disable_csrf = False
field_template = 'bootstrap4/field_vertical.html'
class HorizontalFormHelper(DefaultFormHelper):
"""The default formhelper to use if you need a horizontal form i.e. labels beside inputs."""
form_class = 'form-horizontal'
label_class = 'col-2'
field_class = 'col-6'
class ListgroupFormsetHelper(BaseFormHelper):
......@@ -83,7 +85,11 @@ class ListgroupFormHelper(BaseFormHelper):
class TableInlineFormSetHelper(BaseFormHelper):
"""Formset that lays out forms in a table."""
template = 'bootstrap4/table_inline_formset.html'
html5_required = False
class TableInlineCollapseFormSetHelper(BaseFormHelper):
"""Formset that lays out forms as collapsible regions in a table."""
template = 'bootstrap4/table_inline_collapse_formset.html'
class TableInlineFormHelper(BaseFormHelper):
......@@ -107,7 +113,7 @@ class CourseInstanceForm(CIMSBaseModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Setup Crispyforms config
self.helper = DefaultFormHelper()
self.helper = HorizontalFormHelper()
self.helper.layout = Layout(
Field('courseversion', tabindex='1'), 'instance_descriptor', 'start_date',
'end_date', 'taught_by', 'conveners', 'class_number', 'teachingsession',
......@@ -152,7 +158,7 @@ class CourseVersionForm(CIMSBaseModelForm):
objectives.queryset = self.instance.get_allowed_standardobjectives()
# Setup Crispyforms config
self.helper = DefaultFormHelper()
self.helper = HorizontalFormHelper()
self.helper.layout = Layout(
Field('course', tabindex='1'), 'version_descriptor', 'status', 'status_freetext',
'approval_date', 'approval_desc', 'title', 'description', 'delivery_mode',
......@@ -181,7 +187,7 @@ class LearningOutcomeForm(CIMSBaseModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Setup Crispyforms config
self.helper = DefaultFormHelper()
self.helper = HorizontalFormHelper()
self.helper.layout = Layout('courseversion', 'outcome')
......@@ -194,7 +200,7 @@ class ScheduleForm(CIMSBaseModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Setup Crispyforms config
self.helper = DefaultFormHelper()
self.helper = HorizontalFormHelper()
self.helper.layout = Layout(
Field('courseinstance', tabindex='1'), 'session_description', 'session_order',
'session_number', 'summary', 'assessment'
......@@ -212,7 +218,7 @@ class TaskForm(CIMSBaseModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Setup Crispyforms config
self.helper = DefaultFormHelper()
self.helper = HorizontalFormHelper()
self.helper.layout = Layout(
Field('courseinstance', tabindex='1'), 'title', 'description', 'weighting',
'hurdle', 'exam', 'task_url', 'presentation_reqs', 'due_date', 'due_time',
......@@ -249,7 +255,7 @@ class CourseInstanceImportForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
self.helper = VerticalFormHelper()
self.helper = DefaultFormHelper()
self.helper.layout = Layout('source_courseinstance', 'import_courserelatedrole',
'import_schedule', 'import_task')
# Enable the related model checkboxes if the user has sufficient permissions
......
......@@ -9,13 +9,14 @@ from django.db.models import Q
from django.forms.formsets import formset_factory
from django.forms.models import BaseInlineFormSet, inlineformset_factory
from django.utils.encoding import force_text
from crispy_forms.layout import Field, Layout
from crispy_forms.layout import Div, Field, Layout
from itertools import chain
from cims import models
from cims.forms import (CIMSBaseModelForm, InlineCourseInstanceForm,
ListgroupFormHelper, ListgroupFormsetHelper, TableInlineFormSetHelper)
from cims.widgets import DateInput
from cims.forms import (BaseFormHelper, CIMSBaseModelForm, InlineCourseInstanceForm,
ListgroupFormHelper, ListgroupFormsetHelper, TableInlineFormSetHelper,
TableInlineCollapseFormSetHelper)
from cims.widgets import DateInput, TimeInput
def formset_change_message(formset, change_message):
......@@ -240,6 +241,10 @@ class BaseFormSetManager(object):
-----------
crispy_helper : crispy_forms.helper.FormHelper, optional
Helper class to assist with rendering the formset.
empty_form_crispy_helper : crispy_forms.helper.FormHelper, optional
Helper class to use with the formset empty_form as the empty_form is
generated dynamically, and so won't have the layout from the formset
helper applied to it.
"""
initial = []
......@@ -255,6 +260,7 @@ class BaseFormSetManager(object):
can_delete = False
prefix = None
crispy_helper = None
empty_form_crispy_helper = None
def construct_formset(self):
"""
......@@ -262,8 +268,14 @@ class BaseFormSetManager(object):
"""
FormSet = self.get_formset()
formset = FormSet(**self.get_formset_kwargs())
# Attach our helper to the formset. If we're using a custom layout then attach that to the
# helper.
if self.crispy_helper:
formset.helper = self.crispy_helper()
formset.helper = self.get_layout(self.crispy_helper())
# Because formset.empty_form is generated dynamically we need to provide a helper for it as
# the formset helper won't attach a layout to it.
if self.empty_form_crispy_helper:
formset.empty_form_helper = self.get_layout(self.empty_form_crispy_helper())
return formset
def get_initial(self):
......@@ -341,6 +353,13 @@ class BaseFormSetManager(object):
return kwargs
def get_layout(self, helper):
"""
Get the Crispy Forms Layout object for this formset. By default do nothing and use the
auto-generated layout.
"""
return helper
class BaseInlineModelFormSetManager(BaseFormSetManager):
"""
......@@ -667,9 +686,41 @@ class ScheduleManager(CIMSInlineFormSetManager):
class TaskManager(CIMSInlineFormSetManager):
model = models.Task
fields = ['title', 'description', 'weighting']
fields = ['title', 'description', 'weighting', 'due_date', 'task_url', 'exam', 'hurdle',
'due_time', 'presentation_reqs', 'return_date', 'hurdle_reqs', 'individual_in_group']
can_delete = True
crispy_helper = TableInlineFormSetHelper
crispy_helper = TableInlineCollapseFormSetHelper
empty_form_crispy_helper = BaseFormHelper
widgets = {'due_date': DateInput, 'due_time': TimeInput}
def get_layout(self, helper):
helper.layout = Layout(
Div(
Field('title', wrapper_class='col-md-6'),
Field('description', wrapper_class='col-md-6'),
css_class='form-row'
),
Div(
Field('weighting', wrapper_class='col-md-4'),
Field('due_date', wrapper_class='col-md-4'),
Field('due_time', wrapper_class='col-md-4'),
css_class='form-row'
),
Div(
Field('return_date', css_class='col-md-4'),
Field('task_url', wrapper_class='col-md-4'),
css_class='form-row'
),
'exam',
'hurdle',
Div(
Field('presentation_reqs', wrapper_class='col-md-4'),
Field('hurdle_reqs', wrapper_class='col-md-4'),
Field('individual_in_group', wrapper_class='col-md-4'),
css_class='form-row'
),
)
return helper
# LearningOutcome <-> StandardObjective related classes
......
......@@ -3,3 +3,4 @@
.formset-empty-form { display: none }
.table-auto { width: auto }
div.alert-block > ul { margin-bottom: 0 }
.requiredField { font-weight: bold }
......@@ -11,21 +11,44 @@ function applySelect2(el, style) {
// HTML5 date picker fallback to js
var hasDatePicker = (function () {
var test = document.createElement('input');
test.type = 'date';
try {
test.type = 'date';
}
catch(error){}
return test.type !== 'text';
})();
function fallbackDatePick(el) {
if (!hasDatePicker) el.find('input.dateinput').datepicker({format: 'yyyy-mm-dd'});
if (!hasDatePicker) {
// Avoid template forms
var inputs = el.find('input.dateinput').not("[name*='__prefix__']");
inputs.datepicker({format: 'yyyy-mm-dd'});
}
}
// HTML5 time picker fallback to js
var hasTimePicker = (function () {
var test = document.createElement('input');
try {
test.type = 'time';
}
catch(error) {}
return test.type !== 'text';
})();
function fallbackTimePick(el) {
if (!hasTimePicker) el.find('input.timeinput').timepicker();
if (!hasTimePicker) {
// Avoid template forms
var inputs = el.find('input.timeinput').not("[name*='__prefix__']");
inputs.timepicker({timeFormat: 'G:i'});
}
}
$(function () {
$('a.fa-toggle-collapse').on('click', function () {
$(this)
.find('[data-fa-i2svg]')
.toggleClass('fa-minus')
.toggleClass('fa-plus');
});
});
......@@ -13,12 +13,13 @@ $(document).ready(function() {
fallbackTimePick(doc);
// Resize some inputs...
$('div.form-group input[type=number], div.form-group input[type=date], div.form-group input[type=time]').parent().removeClass().addClass('col-2');
//$('div.form-group input[type=number], div.form-group input[type=date], div.form-group input[type=time]').parent().removeClass().addClass('col-2');
// Reposition check boxes in tables...
$('td[class=form-check]').children('input[type=checkbox]').removeClass('form-check-input');
$('input[type=checkbox]').parent('td[class=form-check]').removeClass('form-check');
// Remove 'required' attribute from empty template form fields so we don't break browser pre-validation
$('.formset-empty-form :input[required]').removeAttr('required');
// Formsets no longer set 'required' attribute on fields, should be able to remove this shortly
// $('.formset-empty-form :input[required]').removeAttr('required');
});
......@@ -30,9 +30,10 @@
updateElementIndex = function(elem, prefix, ndx) {
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
replacement = prefix + '-' + ndx + '-';
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
var attributes = ['for', 'id', 'name', 'href', 'aria-controls'];
attributes.forEach( function(element) {
if (elem.attr(element)) elem.attr(element, elem.attr(element).replace(idRegex, replacement));
});
},
hasChildElements = function(row) {
......
{% if form.non_field_errors %}
<div class="alert alert-block alert-danger">
{% if form_error_title %}<h4 class="alert-heading">{{ form_error_title }}</h4>{% endif %}
<ul>
{{ form.non_field_errors|unordered_list }}
</ul>
</div>
{% endif %}
{% if formset.non_form_errors %}
<div class="alert alert-block alert-danger">
{% if formset_error_title %}<h4 class="alert-heading">{{ formset_error_title }}</h4>{% endif %}
<ul>
{{ formset.non_form_errors|unordered_list }}
</ul>
</div>
{% endif %}
{% load crispy_forms_field %}
{% load cims_filters %}
{% with form_horizontal=form_class|contains:"form-horizontal" %}
{% if field.is_hidden %}
{{ field }}
{{ field }}
{% else %}
{% comment %}
{% if field|is_checkbox %}
<div class="form-check row">
{% if label_class %}
<div class="col-{{ bootstrap_device_type }}-offset-{{ label_size }} {{ field_class }}">
{% endif %}
{% if field|is_checkbox and form_horizontal %}
<div class="form-group row">
<div class="{{ label_class }}"></div>
<div class="{{ field_class }}">
{% endif %}
<{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" class="{% if not field|is_checkbox %}form-group{% if form_horizontal %} row{% endif %}{% else %}form-check{% endif %}{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
{% if field.label and not field|is_checkbox and form_show_labels %}
<label for="{{ field.id_for_label }}" class="{% if form_horizontal %}col-form-label {% endif %}{{ label_class }}{% if field.field.required %} requiredField{% endif %}">
{{ field.label }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</label>
{% endif %}
{% endcomment %}
<{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" class="form-group row{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if form_show_errors%}{% if field.errors %} is-invalid{% endif %}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
{% if field.label and not field|is_checkbox and form_show_labels %}
<label for="{{ field.id_for_label }}" class="{{ label_class }}{% if field.field.required %} font-weight-bold requiredField{% endif %}">{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</label>
{% endif %}
{% if field|is_checkboxselectmultiple %}
{% include 'bootstrap4/layout/checkboxselectmultiple.html' %}
{% endif %}
{% if field|is_checkboxselectmultiple %}
{% include 'bootstrap4/layout/checkboxselectmultiple.html' %}
{% endif %}
{% if field|is_radioselect %}
{% include 'bootstrap4/layout/radioselect.html' %}
{% endif %}
{% if field|is_radioselect %}
{% include 'bootstrap4/layout/radioselect.html' %}
{% endif %}
{% if not field|is_checkboxselectmultiple and not field|is_radioselect %}
{% if not field|is_checkboxselectmultiple and not field|is_radioselect %}
{% if field|is_checkbox and form_show_labels %}
<div class="{{ label_class }}"></div>
<div class="{{ field_class }}">
<div class="form-check">
<label for="{{ field.id_for_label }}" class="form-check-label{% if field.field.required %} font-weight-bold requiredField{% endif %}">
{% crispy_field field %}
{{ field.label|safe }}
</label>
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
</div>
</div>
{% else %}
<div class="{{ field_class }}">
{% crispy_field field %}
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
</div>
{% endif %}
{% if field|is_checkbox and form_show_labels %}
{% crispy_field field 'class' 'form-check-input' %}
<label for="{{ field.id_for_label }}" class="form-check-label{% if field.field.required %} requiredField{% endif %}">{{ field.label }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}</label>
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
{% else %}
{% if field_class %}
<div class="{{ field_class }}">
{% endif %}
</{% if tag %}{{ tag }}{% else %}div{% endif %}>
{% comment %}
{% if field|is_checkbox %}
{% if label_class %}
</div>
{% crispy_field field %}
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
{% if field_class %}
</div>
{% endif %}
</div>
{% endif %}
{% endif %}
{% endcomment %}
</{% if tag %}{{ tag }}{% else %}div{% endif %}>
{% if field|is_checkbox and form_horizontal %}
</div>
</div>
{% endif %}
{% endif %}
{% endwith %}
{% load crispy_forms_field %}
{% if field.is_hidden %}
{{ field }}
{% else %}
{% comment %}
{% if field|is_checkbox %}
<div class="form-check row">
{% if label_class %}
<div class="col-{{ bootstrap_device_type }}-offset-{{ label_size }} {{ field_class }}">
{% endif %}
{% endif %}
{% endcomment %}
<{% if tag %}{{ tag }}{% else %}div{% endif %} id="div_{{ field.auto_id }}" class="form-group{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if form_show_errors%}{% if field.errors %} is-invalid{% endif %}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
{% if field.label and not field|is_checkbox and form_show_labels %}
<label for="{{ field.id_for_label }}" class="{{ label_class }}{% if field.field.required %} font-weight-bold requiredField{% endif %}">{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</label>
{% endif %}
{% if field|is_checkboxselectmultiple %}
{% include 'bootstrap4/layout/checkboxselectmultiple.html' %}
{% endif %}
{% if field|is_radioselect %}
{% include 'bootstrap4/layout/radioselect.html' %}
{% endif %}
{% if not field|is_checkboxselectmultiple and not field|is_radioselect %}
{% if field|is_checkbox and form_show_labels %}
<div class="{{ label_class }}"></div>
<div class="{{ field_class }}">
<div class="form-check">
<label for="{{ field.id_for_label }}" class="form-check-label{% if field.field.required %} font-weight-bold requiredField{% endif %}">
{% crispy_field field %}
{{ field.label|safe }}
</label>
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
</div>
</div>
{% else %}
{% crispy_field field %}
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
{% endif %}
{% endif %}
</{% if tag %}{{ tag }}{% else %}div{% endif %}>
{% comment %}
{% if field|is_checkbox %}
{% if label_class %}
</div>
{% endif %}
</div>
{% endif %}
{% endcomment %}
{% endif %}
{% if inputs %}
<div class="form-group row">
{% if label_class %}
<div class="aab {{ label_class }}"></div>
{% endif %}
<div class="{{ field_class }}">
{% for input in inputs %}
{% include "bootstrap4/layout/baseinput.html" %}
{% endfor %}
</div>
</div>
{% endif %}
<div {% if div.css_id %}id="{{ div.css_id }}"{% endif %}
{% if div.css_class %}class="{{ div.css_class }}"{% endif %} {{ div.flat_attrs|safe }}>
{{ fields|safe }}
</div>
<div{% if formactions.attrs %} {{ formactions.flat_attrs|safe }}{% endif %} class="form-group row">
{% if label_class %}
<div class="aab {{ label_class }}"></div>
{% endif %}
<div class="{{ field_class }}">
{{ fields_output|safe }}
</div>
</div>
{% if field.help_text %}
{% if help_text_inline %}
<span id="hint_{{ field.auto_id }}" class="text-muted">{{ field.help_text|safe }}</span>
{% else %}
<small id="hint_{{ field.auto_id }}" class=" form-text text-muted">{{ field.help_text|safe }}</small>
{% endif %}
{% endif %}
{% if help_text_inline and not error_text_inline %}
{% include 'bootstrap4/layout/help_text.html' %}
{% endif %}
{% if error_text_inline %}
{% include 'bootstrap4/layout/field_errors.html' %}
{% else %}
{% include 'bootstrap4/layout/field_errors_block.html' %}
{% endif %}
{% if not help_text_inline %}
{% include 'bootstrap4/layout/help_text.html' %}
{% endif %}
{% load crispy_forms_field %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<div id="div_{{ field.auto_id }}" class="form-group row{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if form_show_errors and field.errors %} is-invalid{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
{% if field.label and form_show_labels %}
<label for="{{ field.id_for_label }}" class="form-control-label {{ label_class }}{% if field.field.required %} font-weight-bold requiredField{% endif %}">
{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}
</label>
{% endif %}
<div class="{{ field_class }}">
{% if field|is_select %}
{% if crispy_prepended_text %}<span class="input-group{% if active %} active{% endif %}{% if input_size %} {{ input_size }}{% endif %}">{{ crispy_prepended_text|safe }}</span>{% endif %}
{% crispy_field field %}
{% if crispy_appended_text %}<span class="input-group{% if active %} active{% endif %}{% if input_size %} {{ input_size }}{% endif %}">{{ crispy_appended_text|safe }}</span>{% endif %}
{% else %}
<div class="input-group">
{% if crispy_prepended_text %}<span class="input-group-addon{% if active %} active{% endif %}{% if input_size %} {{ input_size }}{% endif %}">{{ crispy_prepended_text|safe }}</span>{% endif %}
{% crispy_field field %}
{% if crispy_appended_text %}<span class="input-group-addon{% if active %} active{% endif %}{% if input_size %} {{ input_size }}{% endif %}">{{ crispy_appended_text|safe }}</span>{% endif %}
</div>
{% endif %}
{% include 'bootstrap4/layout/help_text_and_errors.html' %}
</div>
</div>
{% endif %}
{% load crispy_forms_field %}
<div id="div_{{ field.auto_id }}" class="form-group row{% if form_show_errors and field.errors %} error{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}">
<label class="form-control-label {{ label_class }}{% if field.field.required %} font-weight-bold requiredField{% endif %}">{{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %}</label>
<div class="{{ field_class }}">
{% crispy_field field 'disabled' 'disabled' %}
{% include 'bootstrap4/layout/help_text.html' %}
</div>
</div>
{% extends "bootstrap4/table_inline_form.html" %}
{% load crispy_forms_tags %}
{% load cims_filters %}
{% block form_fields %}
<td>
<p>
<a href="#div-{{ form.prefix }}-wrapper" class="fa-toggle-collapse" data-toggle="collapse" role="button" aria-expanded="false" aria-controls="div-{{ form.prefix }}-wrapper"><i class="fas fa-plus"></i> Edit {% if form.visible_fields.0.value %}{{ form.visible_fields.0.value }}{% endif %}</a>
</p>
<div id="div-{{ form.prefix }}-wrapper" class="collapse">
<div class="card card-body">
{% comment %}
If it's the empty form we need to use the crispy tag to render the form with a specific helper, otherwise pass the form to a template as it's already been rendered by the formset helper.
{% endcomment %}
{% if form.prefix == formset.prefix|suffix:"-__prefix__" %}
{% crispy form formset.empty_form_helper %}
{% else %}
{% include "bootstrap4/display_form.html" %}
{% endif %}
</div>
</div>
</td>
{% if form.DELETE %}
{% with field=form.DELETE %}
{% include "bootstrap4/table_inline_field.html" %}
{% endwith %}
{% endif %}
{% endblock %}
{% extends "bootstrap4/table_inline_formset.html" %}
{% load cims_filters %}
{% block table_head %}
<tr>
<th class="">View</th>
<th class="col">Edit</th>
{% if formset.can_delete %}
{% with field=formset.empty_form.DELETE %}