diff --git a/machines/views.py b/machines/views.py index 85c19dde..1271e277 100644 --- a/machines/views.py +++ b/machines/views.py @@ -111,8 +111,6 @@ from .models import ( ) from users.models import User from preferences.models import GeneralOption, OptionalMachine - -from re2o.templatetags.massive_bootstrap_form import hidden_id, input_id from re2o.utils import ( all_active_assigned_interfaces, all_has_access, @@ -192,11 +190,13 @@ def generate_ipv4_mbf_param( form, is_type_tt ): i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) } i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) } i_update_on = { 'ipv4': [f_type_id( is_type_tt )] } + i_gen_select = { 'ipv4': False } i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, - 'update_on': i_update_on + 'update_on': i_update_on, + 'gen_select': i_gen_select } return i_mbf_param diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py index df7edc1f..26a9bcc8 100644 --- a/re2o/templatetags/massive_bootstrap_form.py +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -2,28 +2,35 @@ # Re2o est un logiciel d'administration développé initiallement au rezometz. Il # se veut agnostique au réseau considéré, de manière à être installable en # quelques clics. -# +# # Copyright © 2017 Maël Kervella -# +# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" Templatetag used to render massive django form selects into bootstrap +forms that can still be manipulating even if there is multiple tens of +thousands of elements in the select. It's made possible using JS libaries +Twitter Typeahead and Splitree's Tokenfield. +See docstring of massive_bootstrap_form for a detailed explaantion on how +to use this templatetag. +""" + from django import template from django.utils.safestring import mark_safe from django.forms import TextInput from django.forms.widgets import Select -from bootstrap3.templatetags.bootstrap3 import bootstrap_form from bootstrap3.utils import render_tag from bootstrap3.forms import render_field @@ -113,12 +120,29 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): A dict of list of ids that the values depends on. The engine and the typeahead properties are recalculated and reapplied. Example : - 'addition' : { + 'update_on' : { 'field_A' : [ 'id0', 'id1', ... ] , 'field_B' : ... , ... } + gen_select (optional) + A dict of boolean telling if the form should either generate + the normal select (set to true) and then use it to generate + the possible choices and then remove it or either (set to + false) generate the choices variable in this tag and do not + send any select. + Sending the select before can be usefull to permit the use + without any JS enabled but it will execute more code locally + for the client so the loading might be slower. + If not specified, this variable is set to true for each field + Example : + 'gen_select' : { + 'field_A': True , + 'field_B': ... , + ... + } + See boostrap_form_ for other arguments **Usage**:: @@ -146,6 +170,11 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): [ '': '' [, '': '' [, ... ] ] ] + } ], + [, 'gen_select': { + [ '': '' + [, '': '' + [, ... ] ] ] } ] } ] [ ] @@ -156,417 +185,625 @@ def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): {% massive_bootstrap_form form 'ipv4' choices='[...]' %} """ - fields = mbf_fields.split(',') - param = kwargs.pop('mbf_param', {}) - exclude = param.get('exclude', '').split(',') - choices = param.get('choices', {}) - engine = param.get('engine', {}) - match_func = param.get('match_func', {}) - update_on = param.get('update_on', {}) - hidden_fields = [h.name for h in form.hidden_fields()] - - html = '' - - for f_name, f_value in form.fields.items() : - if not f_name in exclude : - if f_name in fields and not f_name in hidden_fields : - - if not isinstance(f_value.widget, Select) : - raise ValueError( - ('Field named {f_name} from {form} is not a Select and' - 'can\'t be rendered with massive_bootstrap_form.' - ).format( - f_name=f_name, - form=form - ) - ) + mbf_form = MBFForm(form, mbf_fields.split(','), *args, **kwargs) + return mbf_form.render() - multiple = f_value.widget.allow_multiple_selected - f_bound = f_value.get_bound_field( form, f_name ) - f_value.widget = TextInput( - attrs = { - 'name': 'mbf_'+f_name, - 'placeholder': f_value.empty_label - } - ) - html += render_field( - f_value.get_bound_field( form, f_name ), - *args, - **kwargs - ) - if multiple : - content = mbf_js( - f_name, - f_value, - f_bound, - multiple, - choices, - engine, - match_func, - update_on + +class MBFForm(): + """ An object to hold all the information and useful methods needed to + create and render a massive django form into an actual HTML and JS + code able to handle it correctly. + Every field that is not listed is rendered as a normal bootstrap_field. + """ + + + def __init__(self, form, mbf_fields, *args, **kwargs): + # The django form object + self.form = form + # The fields on which to use JS + self.fields = mbf_fields + + # Other bootstrap_form arguments to render the fields + self.args = args + self.kwargs = kwargs + + # Fields to exclude form the form rendering + self.exclude = self.kwargs.get('exclude', '').split(',') + + # All the mbf parameters specified byt the user + param = kwargs.pop('mbf_param', {}) + self.choices = param.get('choices', {}) + self.engine = param.get('engine', {}) + self.match_func = param.get('match_func', {}) + self.update_on = param.get('update_on', {}) + self.gen_select = param.get('gen_select', {}) + self.hidden_fields = [h.name for h in self.form.hidden_fields()] + + # HTML code to insert inside a template + self.html = "" + + + def render(self): + """ HTML code for the fully rendered form with all the necessary form + """ + for name, field in self.form.fields.items(): + if not name in self.exclude: + + if name in self.fields and not name in self.hidden_fields: + mbf_field = MBFField( + name, + field, + field.get_bound_field(self.form, name), + self.choices.get(name, None), + self.engine.get(name, None), + self.match_func.get(name, None), + self.update_on.get(name, None), + self.gen_select.get(name, True), + *self.args, + **self.kwargs ) - else : - content = hidden_tag( f_bound, f_name ) + mbf_js( - f_name, - f_value, - f_bound, - multiple, - choices, - engine, - match_func, - update_on + self.html += mbf_field.render() + + else: + self.html += render_field( + field.get_bound_field(self.form, name), + *self.args, + **self.kwargs ) - html += render_tag( - 'div', - content = content, - attrs = { 'id': custom_div_id( f_bound ) } + + return mark_safe(self.html) + + + + + +class MBFField(): + """ An object to hold all the information and useful methods needed to + create and render a massive django form field into an actual HTML and JS + code able to handle it correctly. + Twitter Typeahead is used for the display and the matching of queries and + in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage + multiple values. + A div with only non visible elements is created after the div containing + the displayed input. It's used to store the actual data that will be sent + to the server """ + + + def __init__(self, name_, field_, bound_, choices_, engine_, match_func_, + update_on_, gen_select_, *args_, **kwargs_): + + # Verify this field is a Select (or MultipleSelect) (only supported) + if not isinstance(field_.widget, Select): + raise ValueError( + ('Field named {f_name} is not a Select and' + 'can\'t be rendered with massive_bootstrap_form.' + ).format( + f_name=name_ ) + ) - else: - html += render_field( - f_value.get_bound_field( form, f_name ), - *args, - **kwargs + # Name of the field + self.name = name_ + # Django field object + self.field = field_ + # Bound Django field associated with field + self.bound = bound_ + + # Id for the main visible input + self.input_id = self.bound.auto_id + # Id for a hidden input used to store the value + self.hidden_id = self.input_id + '_hidden' + # Id for another div containing hidden inputs and script + self.div2_id = self.input_id + '_div' + + # Should the standard select should be generated + self.gen_select = gen_select_ + # Is it select with multiple values possible (use of tokenfield) + self.multiple = self.field.widget.allow_multiple_selected + # JS for the choices variable (user specified or default) + self.choices = choices_ or self.default_choices() + # JS for the engine variable (typeahead) (user specified or default) + self.engine = engine_ or self.default_engine() + # JS for the matching function (typeahead) (user specified or default) + self.match_func = match_func_ or self.default_match_func() + # JS for the datasets variable (typeahead) (user specified or default) + self.datasets = self.default_datasets() + # Ids of other fields to bind a reset/reload with when changed + self.update_on = update_on_ or [] + + # Whole HTML code to insert in the template + self.html = "" + # JS code in the script tag + self.js_script = "" + # Input tag to display instead of select + self.replace_input = None + + # Other bootstrap_form arguments to render the fields + self.args = args_ + self.kwargs = kwargs_ + + + def default_choices(self): + """ JS code of the variable choices_ """ + + if self.gen_select: + return ( + 'function plop(o) {{' + 'var c = [];' + 'for( let i=0 ; i