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()] + mbf_form = MBFForm(form, mbf_fields.split(','), *args, **kwargs) + return mbf_form.render() - 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 - ) - ) - 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 - ) - else : - content = hidden_tag( f_bound, f_name ) + mbf_js( - f_name, - f_value, - f_bound, - multiple, - choices, - engine, - match_func, - update_on - ) - html += render_tag( - 'div', - content = content, - attrs = { 'id': custom_div_id( f_bound ) } - ) - - else: - html += render_field( - f_value.get_bound_field( form, f_name ), - *args, - **kwargs - ) - - return mark_safe( html ) - -def input_id( f_bound ) : - """ The id of the HTML input element """ - return f_bound.auto_id - -def hidden_id( f_bound ): - """ The id of the HTML hidden input element """ - return input_id( f_bound ) + '_hidden' - -def custom_div_id( f_bound ): - """ The id of the HTML div element containing values and script """ - return input_id( f_bound ) + '_div' - -def hidden_tag( f_bound, f_name ): - """ The HTML hidden input element """ - return render_tag( - 'input', - attrs={ - 'id': hidden_id( f_bound ), - 'name': f_bound.html_name, - 'type': 'hidden', - 'value': f_bound.value() or "" - } - ) - -def mbf_js( f_name, f_value, f_bound, multiple, - choices_, engine_, match_func_, update_on_ ) : - """ The whole script to use """ - - choices = ( mark_safe( choices_[f_name] ) if f_name in choices_.keys() - else default_choices( f_value ) ) - - engine = ( mark_safe( engine_[f_name] ) if f_name in engine_.keys() - else default_engine ( f_name ) ) - - match_func = ( mark_safe( match_func_[f_name] ) - if f_name in match_func_.keys() else default_match_func( f_name ) ) - - update_on = update_on_[f_name] if f_name in update_on_.keys() else [] - - if multiple : - js_content = ( - 'var choices_{f_name} = {choices};' - 'var engine_{f_name};' - 'var setup_{f_name} = function() {{' - 'engine_{f_name} = {engine};' - '$( "#{input_id}" ).tokenfield( "destroy" );' - '$( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});' - '}};' - '$( "#{input_id}" ).bind( "tokenfield:createtoken", {create} );' - '$( "#{input_id}" ).bind( "tokenfield:edittoken", {edit} );' - '$( "#{input_id}" ).bind( "tokenfield:removetoken", {remove} );' - '{updates}' - '$( "#{input_id}" ).ready( function() {{' - 'setup_{f_name}();' - '{init_input}' - '}} );' - ).format( - f_name = f_name, - choices = choices, - engine = engine, - input_id = input_id( f_bound ), - datasets = default_datasets( f_name, match_func ), - create = tokenfield_create( f_name, f_bound ), - edit = tokenfield_edit( f_name, f_bound ), - remove = tokenfield_remove( f_name, f_bound ), - updates = ''.join( [ ( - '$( "#{u_id}" ).change( function() {{' - 'setup_{f_name}();' - '{reset_input}' - '}} );' - ).format( - u_id = u_id, - reset_input = tokenfield_reset_input( f_bound ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = tokenfield_init_input( f_name, f_bound ), - ) - else : - js_content = ( - 'var choices_{f_name} = {choices};' - 'var engine_{f_name};' - 'var setup_{f_name} = function() {{' - 'engine_{f_name} = {engine};' - '$( "#{input_id}" ).typeahead( "destroy" );' - '$( "#{input_id}" ).typeahead( {datasets} );' - '}};' - '$( "#{input_id}" ).bind( "typeahead:select", {select} );' - '$( "#{input_id}" ).bind( "typeahead:change", {change} );' - '{updates}' - '$( "#{input_id}" ).ready( function() {{' - 'setup_{f_name}();' - '{init_input}' - '}} );' - ).format( - f_name = f_name, - choices = choices, - engine = engine, - input_id = input_id( f_bound ), - datasets = default_datasets( f_name, match_func ), - select = typeahead_select( f_bound ), - change = typeahead_change( f_bound ), - updates = ''.join( [ ( - '$( "#{u_id}" ).change( function() {{' - 'setup_{f_name}();' - '{reset_input}' - '}} );' - ).format( - u_id = u_id, - reset_input = typeahead_reset_input( f_bound ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = typeahead_init_input( f_name, f_bound ), - ) - - return render_tag( 'script', content=mark_safe( js_content ) ) - -def typeahead_init_input( f_name, f_bound ) : - """ The JS script to init the fields values """ - init_key = f_bound.value() or '""' - return ( - '$( "#{input_id}" ).typeahead("val", {init_val});' - '$( "#{hidden_id}" ).val( {init_key} );' - ).format( - input_id = input_id( f_bound ), - init_val = '""' if init_key == '""' else - 'engine_{f_name}.get( {init_key} )[0].value'.format( - f_name = f_name, - init_key = init_key - ), - init_key = init_key, - hidden_id = hidden_id( f_bound ) - ) - -def typeahead_reset_input( f_bound ) : - """ The JS script to reset the fields values """ - return ( - '$( "#{input_id}" ).typeahead("val", "");' - '$( "#{hidden_id}" ).val( "" );' - ).format( - input_id = input_id( f_bound ), - hidden_id = hidden_id( f_bound ) - ) - -def tokenfield_init_input( f_name, f_bound ) : - """ The JS script to init the fields values """ - init_key = f_bound.value() or '""' - return ( - '$( "#{input_id}" ).tokenfield("setTokens", {init_val});' - ).format( - input_id = input_id( f_bound ), - init_val = '""' if init_key == '""' else ( - 'engine_{f_name}.get( {init_key} ).map(' - 'function(o) {{ return o.value; }}' - ')').format( - f_name = f_name, - init_key = init_key - ), - init_key = init_key, - ) - -def tokenfield_reset_input( f_bound ) : - """ The JS script to reset the fields values """ - return ( - '$( "#{input_id}" ).tokenfield("setTokens", "");' - ).format( - input_id = input_id( f_bound ), - ) - -def default_choices( f_value ) : - """ The JS script creating the variable choices_ """ - return '[{objects}]'.format( - objects = ','.join( - [ '{{key:{k},value:"{v}"}}'.format( - k = choice[0] if choice[0] != '' else '""', - v = choice[1] - ) for choice in f_value.choices ] - ) - ) - -def default_engine ( f_name ) : - """ The JS script creating the variable engine_ """ - return ( - 'new Bloodhound({{' - 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' - 'queryTokenizer: Bloodhound.tokenizers.whitespace,' - 'local: choices_{f_name},' - 'identify: function(obj) {{ return obj.key; }}' - '}})' - ).format( - f_name = f_name - ) - -def default_datasets( f_name, match_func ) : - """ The JS script creating the datasets to use with typeahead """ - return ( - '{{' - 'hint: true,' - 'highlight: true,' - 'minLength: 0' - '}},' - '{{' - 'display: "value",' - 'name: "{f_name}",' - 'source: {match_func}' - '}}' - ).format( - f_name = f_name, - match_func = match_func - ) - -def default_match_func ( f_name ) : - """ The JS script creating the matching function to use with typeahed """ - return ( - 'function ( q, sync ) {{' - 'if ( q === "" ) {{' - 'var first = choices_{f_name}.slice( 0, 5 ).map(' - 'function ( obj ) {{ return obj.key; }}' - ');' - 'sync( engine_{f_name}.get( first ) );' - '}} else {{' - 'engine_{f_name}.search( q, sync );' - '}}' - '}}' - ).format( - f_name = f_name - ) - -def typeahead_select( f_bound ): - """ The JS script creating the function triggered when an item is - selected through typeahead """ - return ( - 'function(evt, item) {{' - '$( "#{hidden_id}" ).val( item.key );' - '$( "#{hidden_id}" ).change();' - 'return item;' - '}}' - ).format( - hidden_id = hidden_id( f_bound ) - ) - -def typeahead_change( f_bound ): - """ The JS script creating the function triggered when an item is changed - (i.e. looses focus and value has changed since the moment it gained focus +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. """ - return ( - 'function(evt) {{' - 'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{' - '$( "#{hidden_id}" ).val( "" );' - '$( "#{hidden_id}" ).change();' - '}}' - '}}' - ).format( - input_id = input_id( f_bound ), - hidden_id = hidden_id( f_bound ) - ) -def tokenfield_create( f_name, f_bound ): - """ The JS script triggered when a new token is created in tokenfield. """ - return ( - 'function(evt) {{' - 'var k = evt.attrs.key;' - 'if (!k) {{' - 'var data = evt.attrs.value;' - 'var i = 0;' - 'while ( i