diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index 11e454f5..f1af2b8b 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load staticfiles%} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de factures{% endblock %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %}

Editer la facture

- {% bootstrap_form_typeahead factureform 'user' %} + {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }}

Articles de la facture

diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 4f68b6ee..86bf7b90 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification de machines{% endblock %} @@ -78,10 +78,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if interfaceform %}

Interface

- {% if i_bft_param %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% if i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %} @@ -98,15 +98,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if extensionform %}

Extension

- {% bootstrap_form_typeahead extensionform 'origin' %} + {% massive_bootstrap_form extensionform 'origin' %} {% endif %} {% if mxform %}

Enregistrement MX

- {% bootstrap_form_typeahead mxform 'name' %} + {% massive_bootstrap_form mxform 'name' %} {% endif %} {% if nsform %}

Enregistrement NS

- {% bootstrap_form_typeahead nsform 'ns' %} + {% massive_bootstrap_form nsform 'ns' %} {% endif %} {% if txtform %}

Enregistrement TXT

@@ -118,7 +118,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if serviceform %}

Service

- {% bootstrap_form serviceform %} + {% massive_bootstrap_form serviceform 'servers' %} {% endif %} {% if vlanform %}

Vlan

diff --git a/machines/views.py b/machines/views.py index 0bfe36f0..3cc906c0 100644 --- a/machines/views.py +++ b/machines/views.py @@ -54,7 +54,7 @@ from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort from users.models import User from preferences.models import GeneralOption, OptionalMachine -from re2o.templatetags.bootstrap_form_typeahead import hidden_id, input_id +from re2o.templatetags.massive_bootstrap_form import hidden_id, input_id from re2o.utils import all_active_assigned_interfaces, all_has_access from re2o.views import form @@ -65,7 +65,7 @@ def f_type_id( is_type_tt ): return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type' def generate_ipv4_choices( form ) : - """ Generate the parameter choices for the bootstrap_form_typeahead tag + """ Generate the parameter choices for the massive_bootstrap_form tag """ f_ipv4 = form.fields['ipv4'] used_mtype_id = [] @@ -92,7 +92,7 @@ def generate_ipv4_choices( form ) : return choices def generate_ipv4_engine( is_type_tt ) : - """ Generate the parameter engine for the bootstrap_form_typeahead tag + """ Generate the parameter engine for the massive_bootstrap_form tag """ return ( 'new Bloodhound( {{' @@ -106,7 +106,7 @@ def generate_ipv4_engine( is_type_tt ) : ) def generate_ipv4_match_func( is_type_tt ) : - """ Generate the parameter match_func for the bootstrap_form_typeahead tag + """ Generate the parameter match_func for the massive_bootstrap_form tag """ return ( 'function(q, sync) {{' @@ -122,20 +122,20 @@ def generate_ipv4_match_func( is_type_tt ) : type_id = f_type_id( is_type_tt ) ) -def generate_ipv4_bft_param( form, is_type_tt ): - """ Generate all the parameters to use with the bootstrap_form_typeahead +def generate_ipv4_mbf_param( form, is_type_tt ): + """ Generate all the parameters to use with the massive_bootstrap_form tag """ i_choices = { 'ipv4': generate_ipv4_choices( form ) } 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_bft_param = { + i_mbf_param = { 'choices': i_choices, 'engine': i_engine, 'match_func': i_match_func, 'update_on': i_update_on } - return i_bft_param + return i_mbf_param @login_required def new_machine(request, userid): @@ -183,8 +183,8 @@ def new_machine(request, userid): reversion.set_comment("Création") messages.success(request, "La machine a été créée") return redirect("/users/profil/" + str(user.id)) - i_bft_param = generate_ipv4_bft_param( interface, False ) - return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface, False ) + return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def edit_interface(request, interfaceid): @@ -223,8 +223,8 @@ def edit_interface(request, interfaceid): reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data)) messages.success(request, "La machine a été modifiée") return redirect("/users/profil/" + str(interface.machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_machine(request, machineid): @@ -282,8 +282,8 @@ def new_interface(request, machineid): reversion.set_comment("Création") messages.success(request, "L'interface a été ajoutée") return redirect("/users/profil/" + str(machine.user.id)) - i_bft_param = generate_ipv4_bft_param( interface_form, False ) - return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request) + i_mbf_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request) @login_required def del_interface(request, interfaceid): diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 610889dd..02f006c1 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification des préférences{% endblock %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} -{% bootstrap_form_typeahead options 'utilisateur_asso' %} +{% massive_bootstrap_form options 'utilisateur_asso' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
diff --git a/re2o/templatetags/bootstrap_form_typeahead.py b/re2o/templatetags/bootstrap_form_typeahead.py deleted file mode 100644 index 4c665361..00000000 --- a/re2o/templatetags/bootstrap_form_typeahead.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- mode: python; coding: utf-8 -*- -# 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. - -from django import template -from django.utils.safestring import mark_safe -from django.forms import TextInput -from bootstrap3.templatetags.bootstrap3 import bootstrap_form -from bootstrap3.utils import render_tag -from bootstrap3.forms import render_field - -register = template.Library() - -@register.simple_tag -def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs): - """ - Render a form where some specific fields are rendered using Typeahead. - Using Typeahead really improves the performance, the speed and UX when - dealing with very large datasets (select with 50k+ elts for instance). - For convenience, it accepts the same parameters as a standard bootstrap - can accept. - - **Tag name**:: - - bootstrap_form_typeahead - - **Parameters**: - - form - The form that is to be rendered - - typeahead_fields - A list of field names (comma separated) that should be rendered - with typeahead instead of the default bootstrap renderer. - - bft_param - A dict of parameters for the bootstrap_form_typeahead tag. The - possible parameters are the following. - - choices - A dict of strings representing the choices in JS. The keys of - the dict are the names of the concerned fields. The choices - must be an array of objects. Each of those objects must at - least have the fields 'key' (value to send) and 'value' (value - to display). Other fields can be added as desired. - For a more complex structure you should also consider - reimplementing the engine and the match_func. - If not specified, the key is the id of the object and the value - is its string representation as in a normal bootstrap form. - Example : - 'choices' : { - 'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]', - 'field_B':..., - ... - } - - engine - A dict of strings representating the engine used for matching - queries and possible values with typeahead. The keys of the - dict are the names of the concerned fields. The string is valid - JS code. - If not specified, BloodHound with relevant basic properties is - used. - Example : - 'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} - - match_func - A dict of strings representing a valid JS function used in the - dataset to overload the matching engine. The keys of the dict - are the names of the concerned fields. This function is used - the source of the dataset. This function receives 2 parameters, - the query and the synchronize function as specified in - typeahead.js documentation. If needed, the local variables - 'choices_' and 'engine_' contains - respectively the array of all possible values and the engine - to match queries with possible values. - If not specified, the function used display up to the 10 first - elements if the query is empty and else the matching results. - Example : - 'match_func' : { - 'field_A': 'function(q, sync) { engine.search(q, sync); }', - 'field_B': ..., - ... - } - - update_on - A dict of list of ids that the values depends on. The engine - and the typeahead properties are recalculated and reapplied. - Example : - 'addition' : { - 'field_A' : [ 'id0', 'id1', ... ] , - 'field_B' : ... , - ... - } - - See boostrap_form_ for other arguments - - **Usage**:: - - {% bootstrap_form_typeahead - form - [ '[,[,...]]' ] - [ { - [ 'choices': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'engine': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'match_func': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'update_on': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - } ] - [ ] - %} - - **Example**: - - {% bootstrap_form_typeahead form 'ipv4' choices='[...]' %} - """ - - t_fields = typeahead_fields.split(',') - params = kwargs.get('bft_param', {}) - exclude = params.get('exclude', None) - exclude = exclude.split(',') if exclude else [] - t_choices = params.get('choices', {}) - t_engine = params.get('engine', {}) - t_match_func = params.get('match_func', {}) - t_update_on = params.get('update_on', {}) - hidden = [h.name for h in django_form.hidden_fields()] - - form = '' - for f_name, f_value in django_form.fields.items() : - if not f_name in exclude : - if f_name in t_fields and not f_name in hidden : - f_bound = f_value.get_bound_field( django_form, f_name ) - f_value.widget = TextInput( - attrs={ - 'name': 'typeahead_'+f_name, - 'placeholder': f_value.empty_label - } - ) - form += render_field( - f_value.get_bound_field( django_form, f_name ), - *args, - **kwargs - ) - form += render_tag( - 'div', - content = hidden_tag( f_bound, f_name ) + - typeahead_js( - f_name, - f_value, - f_bound, - t_choices, - t_engine, - t_match_func, - t_update_on - ) - ) - else: - form += render_field( - f_value.get_bound_field(django_form, f_name), - *args, - **kwargs - ) - - return mark_safe( form ) - -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 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 typeahead_js( f_name, f_value, f_bound, - t_choices, t_engine, t_match_func, t_update_on ) : - """ The whole script to use """ - - choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \ - else default_choices( f_value ) - - engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \ - else default_engine ( f_name ) - - match_func = mark_safe(t_match_func[f_name]) \ - if f_name in t_match_func.keys() else default_match_func( f_name ) - - update_on = t_update_on[f_name] if f_name in t_update_on.keys() 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", {updater} );' - '$( "#{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 ), - updater = typeahead_updater( 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 = reset_input( f_bound ), - f_name = f_name - ) for u_id in update_on ] - ), - init_input = init_input( f_name, f_bound ), - ) - - return render_tag( 'script', content=mark_safe( js_content ) ) - -def 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 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 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_updater( 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 - """ - 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 ) - ) - diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py new file mode 100644 index 00000000..df7edc1f --- /dev/null +++ b/re2o/templatetags/massive_bootstrap_form.py @@ -0,0 +1,572 @@ +# -*- mode: python; coding: utf-8 -*- +# 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. + +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 + +register = template.Library() + +@register.simple_tag +def massive_bootstrap_form(form, mbf_fields, *args, **kwargs): + """ + Render a form where some specific fields are rendered using Twitter + Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the + speed and UX when dealing with very large datasets (select with 50k+ elts + for instance). + When the fields specified should normally be rendered as a select with + single selectable option, Twitter Typeahead is used for a better display + and the matching query engine. When dealing with multiple selectable + options, sliptree's Bootstrap Tokenfield in addition with Typeahead. + For convenience, it accepts the same parameters as a standard bootstrap + can accept. + + **Tag name**:: + + massive_bootstrap_form + + **Parameters**: + + form (required) + The form that is to be rendered + + mbf_fields (optional) + A list of field names (comma separated) that should be rendered + with Typeahead/Tokenfield instead of the default bootstrap + renderer. + If not specified, all fields will be rendered as a normal bootstrap + field. + + mbf_param (optional) + A dict of parameters for the massive_bootstrap_form tag. The + possible parameters are the following. + + choices (optional) + A dict of strings representing the choices in JS. The keys of + the dict are the names of the concerned fields. The choices + must be an array of objects. Each of those objects must at + least have the fields 'key' (value to send) and 'value' (value + to display). Other fields can be added as desired. + For a more complex structure you should also consider + reimplementing the engine and the match_func. + If not specified, the key is the id of the object and the value + is its string representation as in a normal bootstrap form. + Example : + 'choices' : { + 'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]', + 'field_B':..., + ... + } + + engine (optional) + A dict of strings representating the engine used for matching + queries and possible values with typeahead. The keys of the + dict are the names of the concerned fields. The string is valid + JS code. + If not specified, BloodHound with relevant basic properties is + used. + Example : + 'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...} + + match_func (optional) + A dict of strings representing a valid JS function used in the + dataset to overload the matching engine. The keys of the dict + are the names of the concerned fields. This function is used + the source of the dataset. This function receives 2 parameters, + the query and the synchronize function as specified in + typeahead.js documentation. If needed, the local variables + 'choices_' and 'engine_' contains + respectively the array of all possible values and the engine + to match queries with possible values. + If not specified, the function used display up to the 10 first + elements if the query is empty and else the matching results. + Example : + 'match_func' : { + 'field_A': 'function(q, sync) { engine.search(q, sync); }', + 'field_B': ..., + ... + } + + update_on (optional) + A dict of list of ids that the values depends on. The engine + and the typeahead properties are recalculated and reapplied. + Example : + 'addition' : { + 'field_A' : [ 'id0', 'id1', ... ] , + 'field_B' : ... , + ... + } + + See boostrap_form_ for other arguments + + **Usage**:: + + {% massive_bootstrap_form + form + [ '[,[,...]]' ] + [ mbf_param = { + [ 'choices': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'engine': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'match_func': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + [, 'update_on': { + [ '': '' + [, '': '' + [, ... ] ] ] + } ] + } ] + [ ] + %} + + **Example**: + + {% 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 + ) + ) + + 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 + """ + 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= 0) + this._delimiters[whitespace] = '\\s' + + if (dash >= 0) { + delete this._delimiters[dash] + this._delimiters.unshift('-') + } + + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] + $.each(this._delimiters, function (index, character) { + var pos = $.inArray(character, specialCharacters) + if (pos >= 0) _self._delimiters[index] = '\\' + character; + }); + + // Store original input width + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null + , elStyleWidth = element.style.width + , elCSSWidth + , elWidth = this.$element.width() + + if (elRules) { + $.each( elRules, function (i, rule) { + if (rule.style.width) { + elCSSWidth = rule.style.width; + } + }); + } + + // Move original input out of the way + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', + originalStyles = { position: this.$element.css('position') }; + originalStyles[hidingPosition] = this.$element.css(hidingPosition); + + this.$element + .data('original-styles', originalStyles) + .data('original-tabindex', this.$element.prop('tabindex')) + .css('position', 'absolute') + .css(hidingPosition, '-10000px') + .prop('tabindex', -1) + + // Create a wrapper + this.$wrapper = $('
') + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') + + // Create a new input + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) + this.$input = $('') + .appendTo( this.$wrapper ) + .prop( 'placeholder', this.$element.prop('placeholder') ) + .prop( 'id', id + '-tokenfield' ) + .prop( 'tabindex', this.$element.data('original-tabindex') ) + + // Re-route original input label to new input + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$input.prop('id') ) + } + + // Set up a copy helper to handle copy & paste + this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) + + // Set wrapper width + if (elStyleWidth) { + this.$wrapper.css('width', elStyleWidth); + } + else if (elCSSWidth) { + this.$wrapper.css('width', elCSSWidth); + } + // If input is inside inline-form with no width set, set fixed width + else if (this.$element.parents('.form-inline').length) { + this.$wrapper.width( elWidth ) + } + + // Set tokenfield disabled, if original or fieldset input is disabled + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { + this.disable(); + } + + // Set tokenfield readonly, if original input is readonly + if (this.$element.prop('readonly')) { + this.readonly(); + } + + // Set up mirror for input auto-sizing + this.$mirror = $(''); + this.$input.css('min-width', this.options.minWidth + 'px') + $.each([ + 'fontFamily', + 'fontSize', + 'fontWeight', + 'fontStyle', + 'letterSpacing', + 'textTransform', + 'wordSpacing', + 'textIndent' + ], function (i, val) { + _self.$mirror[0].style[val] = _self.$input.css(val); + }); + this.$mirror.appendTo( 'body' ) + + // Insert tokenfield to HTML + this.$wrapper.insertBefore( this.$element ) + this.$element.prependTo( this.$wrapper ) + + // Calculate inner input width + this.update() + + // Create initial tokens, if any + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) + + // Start listening to events + this.listen() + + // Initialize autocomplete, if necessary + if ( ! $.isEmptyObject( this.options.autocomplete ) ) { + var side = this.textDirection === 'rtl' ? 'right' : 'left' + , autocompleteOptions = $.extend({ + minLength: this.options.showAutocompleteOnFocus ? 0 : null, + position: { my: side + " top", at: side + " bottom", of: this.$wrapper } + }, this.options.autocomplete ) + + this.$input.autocomplete( autocompleteOptions ) + } + + // Initialize typeahead, if necessary + if ( ! $.isEmptyObject( this.options.typeahead ) ) { + + var typeaheadOptions = this.options.typeahead + , defaults = { + minLength: this.options.showAutocompleteOnFocus ? 0 : null + } + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] + + args[0] = $.extend( {}, defaults, args[0] ) + + this.$input.typeahead.apply( this.$input, args ) + this.typeahead = true + } + } + + Tokenfield.prototype = { + + constructor: Tokenfield + + , createToken: function (attrs, triggerChange) { + var _self = this + + if (typeof attrs === 'string') { + attrs = { value: attrs, label: attrs } + } else { + // Copy objects to prevent contamination of data sources. + attrs = $.extend( {}, attrs ) + } + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + // Normalize label and value + attrs.value = $.trim(attrs.value.toString()); + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value + + // Bail out if has no value or label, or label is too short + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return + + // Bail out if maximum number of tokens is reached + if (this.options.limit && this.getTokens().length >= this.options.limit) return + + // Allow changing token data before creating it + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) + this.$element.trigger(createEvent) + + // Bail out if there if attributes are empty or event was defaultPrevented + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return + + var $token = $('
') + .append('') + .append('×') + .data('attrs', attrs) + + // Insert token into HTML + if (this.$input.hasClass('tt-input')) { + // If the input has typeahead enabled, insert token before it's parent + this.$input.parent().before( $token ) + } else { + this.$input.before( $token ) + } + + // Temporarily set input width to minimum + this.$input.css('width', this.options.minWidth + 'px') + + var $tokenLabel = $token.find('.token-label') + , $closeButton = $token.find('.close') + + // Determine maximum possible token label width + if (!this.maxTokenWidth) { + this.maxTokenWidth = + this.$wrapper.width() - $closeButton.outerWidth() - + parseInt($closeButton.css('margin-left'), 10) - + parseInt($closeButton.css('margin-right'), 10) - + parseInt($token.css('border-left-width'), 10) - + parseInt($token.css('border-right-width'), 10) - + parseInt($token.css('padding-left'), 10) - + parseInt($token.css('padding-right'), 10) + parseInt($tokenLabel.css('border-left-width'), 10) - + parseInt($tokenLabel.css('border-right-width'), 10) - + parseInt($tokenLabel.css('padding-left'), 10) - + parseInt($tokenLabel.css('padding-right'), 10) + parseInt($tokenLabel.css('margin-left'), 10) - + parseInt($tokenLabel.css('margin-right'), 10) + } + + $tokenLabel.css('max-width', this.maxTokenWidth) + if (this.options.html) + $tokenLabel.html(attrs.label) + else + $tokenLabel.text(attrs.label) + + // Listen to events on token + $token + .on('mousedown', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = true + }) + .on('click', function (e) { + if (_self._disabled || _self._readonly) return false + _self.preventDeactivation = false + + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + return _self.toggle( $token ) + } + + _self.activate( $token, e.shiftKey, e.shiftKey ) + }) + .on('dblclick', function (e) { + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false + _self.edit( $token ) + }) + + $closeButton + .on('click', $.proxy(this.remove, this)) + + // Trigger createdtoken event on the original field + // indicating that the token is now in the DOM + this.$element.trigger($.Event('tokenfield:createdtoken', { + attrs: attrs, + relatedTarget: $token.get(0) + })) + + // Trigger change event on the original field + if (triggerChange) { + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) + } + + // Update tokenfield dimensions + var _self = this + setTimeout(function () { + _self.update() + }, 0) + + // Return original element + return this.$element.get(0) + } + + , setTokens: function (tokens, add, triggerChange) { + if (!add) this.$wrapper.find('.token').remove() + + if (!tokens) return + + if (typeof triggerChange === 'undefined') { + triggerChange = true + } + + if (typeof tokens === 'string') { + if (this._delimiters.length) { + // Split based on delimiters + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) + } else { + tokens = [tokens]; + } + } + + var _self = this + $.each(tokens, function (i, attrs) { + _self.createToken(attrs, triggerChange) + }) + + return this.$element.get(0) + } + + , getTokenData: function($token) { + var data = $token.map(function() { + var $token = $(this); + return $token.data('attrs') + }).get(); + + if (data.length == 1) { + data = data[0]; + } + + return data; + } + + , getTokens: function(active) { + var self = this + , tokens = [] + , activeClass = active ? '.active' : '' // get active tokens only + this.$wrapper.find( '.token' + activeClass ).each( function() { + tokens.push( self.getTokenData( $(this) ) ) + }) + return tokens + } + + , getTokensList: function(delimiter, beautify, active) { + delimiter = delimiter || this._firstDelimiter + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify + + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') + return $.map( this.getTokens(active), function (token) { + return token.value + }).join(separator) + } + + , getInput: function() { + return this.$input.val() + } + + , setInput: function (val) { + if (this.$input.hasClass('tt-input')) { + // Typeahead acts weird when simply setting input value to empty, + // so we set the query to empty instead + this.$input.typeahead('val', val) + } else { + this.$input.val(val) + } + } + + , listen: function () { + var _self = this + + this.$element + .on('change', $.proxy(this.change, this)) + + this.$wrapper + .on('mousedown',$.proxy(this.focusInput, this)) + + this.$input + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('paste', $.proxy(this.paste, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + this.$copyHelper + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keydown', $.proxy(this.keydown, this)) + .on('keyup', $.proxy(this.keyup, this)) + + // Secondary listeners for input width calculation + this.$input + .on('keypress', $.proxy(this.update, this)) + .on('keyup', $.proxy(this.update, this)) + + this.$input + .on('autocompletecreate', function() { + // Set minimum autocomplete menu width + var $_menuElement = $(this).data('ui-autocomplete').menu.element + + var minWidth = _self.$wrapper.outerWidth() - + parseInt( $_menuElement.css('border-left-width'), 10 ) - + parseInt( $_menuElement.css('border-right-width'), 10 ) + + $_menuElement.css( 'min-width', minWidth + 'px' ) + }) + .on('autocompleteselect', function (e, ui) { + if (_self.createToken( ui.item )) { + _self.$input.val('') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + return false + }) + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { + // Create token + if (_self.createToken( datum )) { + _self.$input.typeahead('val', '') + if (_self.$input.data( 'edit' )) { + _self.unedit(true) + } + } + }) + + // Listen to window resize + $(window).on('resize', $.proxy(this.update, this )) + + } + + , keydown: function (e) { + + if (!this.focused) return + + var _self = this + + switch(e.keyCode) { + case 8: // backspace + if (!this.$input.is(document.activeElement)) break + this.lastInputValue = this.$input.val() + break + + case 37: // left arrow + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) + break + + case 38: // up arrow + upDown('prev') + break + + case 39: // right arrow + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) + break + + case 40: // down arrow + upDown('next') + break + + case 65: // a (to handle ctrl + a) + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break + this.activateAll() + e.preventDefault() + break + + case 9: // tab + case 13: // enter + + // We will handle creating tokens from autocomplete in autocomplete events + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break + + // We will handle creating tokens from typeahead in typeahead events + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break + + // Create token + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { + return this.createTokensFromInput(e, this.$input.data('edit')); + } + + // Edit token + if (e.keyCode === 13) { + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break + if (!_self.options.allowEditing) break + this.edit( this.$wrapper.find('.token.active') ) + } + } + + function leftRight(direction) { + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + direction += 'All' + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') + if (!$token.length) return + + _self.preventInputFocus = true + _self.preventDeactivation = true + + _self.activate( $token ) + e.preventDefault() + + } else { + _self[direction]( e.shiftKey ) + e.preventDefault() + } + } + + function upDown(direction) { + if (!e.shiftKey) return + + if (_self.$input.is(document.activeElement)) { + if (_self.$input.val().length > 0) return + + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') + if (!$token.length) return + + _self.activate( $token ) + } + + var opposite = direction === 'prev' ? 'next' : 'prev' + , position = direction === 'prev' ? 'first' : 'last' + + _self.$firstActiveToken[opposite + 'All']('.token').each(function() { + _self.deactivate( $(this) ) + }) + + _self.activate( _self.$wrapper.find('.token:' + position), true, true ) + e.preventDefault() + } + + this.lastKeyDown = e.keyCode + } + + , keypress: function(e) { + + // Comma + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { + if (this.$input.val()) { + this.createTokensFromInput(e) + } + return false; + } + } + + , keyup: function (e) { + this.preventInputFocus = false + + if (!this.focused) return + + switch(e.keyCode) { + case 8: // backspace + if (this.$input.is(document.activeElement)) { + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break + + this.preventDeactivation = true + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') + + if (!$prevToken.length) break + + this.activate( $prevToken ) + } else { + this.remove(e) + } + break + + case 46: // delete + this.remove(e, 'next') + break + } + this.lastKeyUp = e.keyCode + } + + , focus: function (e) { + this.focused = true + this.$wrapper.addClass('focus') + + if (this.$input.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + + if (this.options.showAutocompleteOnFocus) { + this.search() + } + } + } + + , blur: function (e) { + + this.focused = false + this.$wrapper.removeClass('focus') + + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { + this.$wrapper.find('.active').removeClass('active') + this.$firstActiveToken = null + } + + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { + this.createTokensFromInput(e) + } + + this.preventDeactivation = false + this.preventCreateTokens = false + } + + , paste: function (e) { + var _self = this + + // Add tokens to existing ones + if (_self.options.allowPasting) { + setTimeout(function () { + _self.createTokensFromInput(e) + }, 1) + } + } + + , change: function (e) { + if ( e.initiator === 'tokenfield' ) return // Prevent loops + + this.setTokens( this.$element.val() ) + } + + , createTokensFromInput: function (e, focus) { + if (this.$input.val().length < this.options.minLength) + return // No input, simply return + + var tokensBefore = this.getTokensList() + this.setTokens( this.$input.val(), true ) + + if (tokensBefore == this.getTokensList() && this.$input.val().length) + return false // No tokens were added, do nothing (prevent form submit) + + this.setInput('') + + if (this.$input.data( 'edit' )) { + this.unedit(focus) + } + + return false // Prevent form being submitted + } + + , next: function (add) { + if (add) { + var $firstActiveToken = this.$wrapper.find('.active:first') + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $firstActiveToken ) + } + + var $lastActiveToken = this.$wrapper.find('.active:last') + , $nextToken = $lastActiveToken.nextAll('.token:first') + + if (!$nextToken.length) { + this.$input.focus() + return + } + + this.activate($nextToken, add) + } + + , prev: function (add) { + + if (add) { + var $lastActiveToken = this.$wrapper.find('.active:last') + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false + + if (deactivate) return this.deactivate( $lastActiveToken ) + } + + var $firstActiveToken = this.$wrapper.find('.active:first') + , $prevToken = $firstActiveToken.prevAll('.token:first') + + if (!$prevToken.length) { + $prevToken = this.$wrapper.find('.token:first') + } + + if (!$prevToken.length && !add) { + this.$input.focus() + return + } + + this.activate( $prevToken, add ) + } + + , activate: function ($token, add, multi, remember) { + + if (!$token) return + + if (typeof remember === 'undefined') var remember = true + + if (multi) var add = true + + this.$copyHelper.focus() + + if (!add) { + this.$wrapper.find('.active').removeClass('active') + if (remember) { + this.$firstActiveToken = $token + } else { + delete this.$firstActiveToken + } + } + + if (multi && this.$firstActiveToken) { + // Determine first active token and the current tokens indicies + // Account for the 1 hidden textarea by subtracting 1 from both + var i = this.$firstActiveToken.index() - 2 + , a = $token.index() - 2 + , _self = this + + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { + _self.activate( $(this), true ) + }) + } + + $token.addClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , activateAll: function() { + var _self = this + + this.$wrapper.find('.token').each( function (i) { + _self.activate($(this), i !== 0, false, false) + }) + } + + , deactivate: function($token) { + if (!$token) return + + $token.removeClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , toggle: function($token) { + if (!$token) return + + $token.toggleClass('active') + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() + } + + , edit: function ($token) { + if (!$token) return + + var attrs = $token.data('attrs') + + // Allow changing input value before editing + var options = { attrs: attrs, relatedTarget: $token.get(0) } + var editEvent = $.Event('tokenfield:edittoken', options) + this.$element.trigger( editEvent ) + + // Edit event can be cancelled if default is prevented + if (editEvent.isDefaultPrevented()) return + + $token.find('.token-label').text(attrs.value) + var tokenWidth = $token.outerWidth() + + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + + $token.replaceWith( $_input ) + + this.preventCreateTokens = true + + this.$input.val( attrs.value ) + .select() + .data( 'edit', true ) + .width( tokenWidth ) + + this.update(); + + // Indicate that token is now being edited, and is replaced with an input field in the DOM + this.$element.trigger($.Event('tokenfield:editedtoken', options )) + } + + , unedit: function (focus) { + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input + $_input.appendTo( this.$wrapper ) + + this.$input.data('edit', false) + this.$mirror.text('') + + this.update() + + // Because moving the input element around in DOM + // will cause it to lose focus, we provide an option + // to re-focus the input after appending it to the wrapper + if (focus) { + var _self = this + setTimeout(function () { + _self.$input.focus() + }, 1) + } + } + + , remove: function (e, direction) { + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return + + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') + + if (e.type !== 'click') { + if (!direction) var direction = 'prev' + this[direction]() + + // Was it the first token? + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 + } + + // Prepare events and their options + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } + , removeEvent = $.Event('tokenfield:removetoken', options) + + this.$element.trigger(removeEvent); + + // Remove event can be intercepted and cancelled + if (removeEvent.isDefaultPrevented()) return + + var removedEvent = $.Event('tokenfield:removedtoken', options) + , changeEvent = $.Event('change', { initiator: 'tokenfield' }) + + // Remove token from DOM + $token.remove() + + // Trigger events + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) + + // Focus, when necessary: + // When there are no more tokens, or if this was the first token + // and it was removed with backspace or it was clicked on + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() + + // Adjust input width + this.$input.css('width', this.options.minWidth + 'px') + this.update() + + // Cancel original event handlers + e.preventDefault() + e.stopPropagation() + } + + /** + * Update tokenfield dimensions + */ + , update: function (e) { + var value = this.$input.val() + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) + , inputPadding = inputPaddingLeft + inputPaddingRight + + if (this.$input.data('edit')) { + + if (!value) { + value = this.$input.prop("placeholder") + } + if (value === this.$mirror.text()) return + + this.$mirror.text(value) + + var mirrorWidth = this.$mirror.width() + 10; + if ( mirrorWidth > this.$wrapper.width() ) { + return this.$input.width( this.$wrapper.width() ) + } + + this.$input.width( mirrorWidth ) + } + else { + //temporary reset width to minimal value to get proper results + this.$input.width(this.options.minWidth); + + var w = (this.textDirection === 'rtl') + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; + // + // some usecases pre-render widget before attaching to DOM, + // dimensions returned by jquery will be NaN -> we default to 100% + // so placeholder won't be cut off. + isNaN(w) ? this.$input.width('100%') : this.$input.width(w); + } + } + + , focusInput: function (e) { + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return + // Focus only after the current call stack has cleared, + // otherwise has no effect. + // Reason: mousedown is too early - input will lose focus + // after mousedown. However, since the input may be moved + // in DOM, there may be no click or mouseup event triggered. + var _self = this + setTimeout(function() { + _self.$input.focus() + }, 0) + } + + , search: function () { + if ( this.$input.data('ui-autocomplete') ) { + this.$input.autocomplete('search') + } + } + + , disable: function () { + this.setProperty('disabled', true); + } + + , enable: function () { + this.setProperty('disabled', false); + } + + , readonly: function () { + this.setProperty('readonly', true); + } + + , writeable: function () { + this.setProperty('readonly', false); + } + + , setProperty: function(property, value) { + this['_' + property] = value; + this.$input.prop(property, value); + this.$element.prop(property, value); + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); + } + + , destroy: function() { + // Set field value + this.$element.val( this.getTokensList() ); + // Restore styles and properties + this.$element.css( this.$element.data('original-styles') ); + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); + + // Re-route tokenfield label to original input + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) + if ( $label.length ) { + $label.prop( 'for', this.$element.prop('id') ) + } + + // Move original element outside of tokenfield wrapper + this.$element.insertBefore( this.$wrapper ); + + // Remove tokenfield-related data + this.$element.removeData('original-styles') + .removeData('original-tabindex') + .removeData('bs.tokenfield'); + + // Remove tokenfield from DOM + this.$wrapper.remove(); + this.$mirror.remove(); + + var $_element = this.$element; + + return $_element; + } + + } + + + /* TOKENFIELD PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.tokenfield + + $.fn.tokenfield = function (option, param) { + var value + , args = [] + + Array.prototype.push.apply( args, arguments ); + + var elements = this.each(function () { + var $this = $(this) + , data = $this.data('bs.tokenfield') + , options = typeof option == 'object' && option + + if (typeof option === 'string' && data && data[option]) { + args.shift() + value = data[option].apply(data, args) + } else { + if (!data && typeof option !== 'string' && !param) { + $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) + $this.trigger('tokenfield:initialize') + } + } + }) + + return typeof value !== 'undefined' ? value : elements; + } + + $.fn.tokenfield.defaults = { + minWidth: 60, + minLength: 0, + html: true, + allowEditing: true, + allowPasting: true, + limit: 0, + autocomplete: {}, + typeahead: {}, + showAutocompleteOnFocus: false, + createTokensOnBlur: false, + delimiter: ',', + beautify: true, + inputType: 'text' + } + + $.fn.tokenfield.Constructor = Tokenfield + + + /* TOKENFIELD NO CONFLICT + * ================== */ + + $.fn.tokenfield.noConflict = function () { + $.fn.tokenfield = old + return this + } + + return Tokenfield; + +})); diff --git a/templates/base.html b/templates/base.html index 42505b23..dfaca5eb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -33,12 +33,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Load CSS and JavaScript #} {% bootstrap_css %} + + {% comment %}{% endcomment %} {% bootstrap_javascript %} + + {% comment %}{% endcomment %} {{ site_name }} : {% block title %}Accueil{% endblock %} diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index cb84e846..fe224678 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'un switch{% endblock %} @@ -47,16 +47,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if topoform %} - {% bootstrap_form_typeahead topoform 'switch_interface' %} + {% massive_bootstrap_form topoform 'switch_interface' %} {% endif %} {% if machineform %} - {% bootstrap_form_typeahead machineform 'user' %} + {% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %} {% if i_bft_param %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_bft_param %} {% else %} - {% bootstrap_form_typeahead interfaceform 'ipv4,machine' %} + {% massive_bootstrap_form interfaceform 'ipv4,machine' %} {% endif %} {% endif %} {% if domainform %} diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index bd07c2db..e14b72a7 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modificationd 'utilisateur{% endblock %} @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} - {% bootstrap_form_typeahead topoform 'room,related,machine_interface' %} + {% massive_bootstrap_form topoform 'room,related,machine_interface' %} {%bootstrap_button "Créer ou modifier" button_type="submit" icon="ok" %}
diff --git a/topologie/views.py b/topologie/views.py index 12732422..fb7550db 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -50,9 +50,8 @@ from topologie.forms import EditPortForm, NewSwitchForm, EditSwitchForm from topologie.forms import AddPortForm, EditRoomForm, StackForm from users.views import form -from machines.forms import AliasForm, NewMachineForm, EditMachineForm -from machines.forms import EditInterfaceForm, AddInterfaceForm -from machines.views import generate_ipv4_bft_param +from machines.forms import AliasForm, NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm +from machines.views import generate_ipv4_mbf_param from preferences.models import AssoOption, GeneralOption @@ -382,6 +381,10 @@ def new_switch(request): reversion.set_comment("Création") messages.success(request, "Le switch a été créé") return redirect("/topologie/") +<<<<<<< HEAD + i_bft_param = generate_ipv4_mbf_param( interface, False ) + return form({'topoform':switch, 'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) +======= i_bft_param = generate_ipv4_bft_param(interface, False) return form({ 'topoform': switch, @@ -391,6 +394,7 @@ def new_switch(request): 'i_bft_param': i_bft_param }, 'topologie/switch.html', request) +>>>>>>> master @login_required @permission_required('infra') @@ -450,6 +454,10 @@ def edit_switch(request, switch_id): ) messages.success(request, "Le switch a bien été modifié") return redirect("/topologie/") +<<<<<<< HEAD + i_bft_param = generate_ipv4_mbf_param( interface_form, False ) + return form({'topoform':switch_form, 'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'topologie/switch.html', request) +======= i_bft_param = generate_ipv4_bft_param(interface_form, False) return form({ 'topoform': switch_form, @@ -459,6 +467,7 @@ def edit_switch(request, switch_id): 'i_bft_param': i_bft_param }, 'topologie/switch.html', request) +>>>>>>> master @login_required @permission_required('infra') diff --git a/users/templates/users/user.html b/users/templates/users/user.html index 62d05146..756b4153 100644 --- a/users/templates/users/user.html +++ b/users/templates/users/user.html @@ -24,7 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load bootstrap_form_typeahead %} +{% load massive_bootstrap_form %} {% block title %}Création et modification d'utilisateur{% endblock %} @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% bootstrap_form_typeahead userform 'room' %} + {% massive_bootstrap_form userform 'room' %} {% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}