Commit 7d7a4500 authored by Chirac's avatar Chirac

Merge branch 'faster_ipform' into 'master'

Faster ipform

See merge request rezo/re2o!13
parents c9658205 25ddaa70
# -*- 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.
......@@ -5,6 +6,7 @@
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
# 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
......@@ -27,7 +29,7 @@ import re
from django.forms import ModelForm, Form, ValidationError
from django import forms
from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType, OuverturePortList, OuverturePort
from django.db.models import Q
from django.db.models import Q, F
from django.core.validators import validate_email
from users.models import User
......@@ -52,8 +54,7 @@ class BaseEditMachineForm(EditMachineForm):
class EditInterfaceForm(ModelForm):
class Meta:
model = Interface
# fields = '__all__'
exclude = ['port_lists']
fields = ['machine', 'type', 'ipv4', 'mac_address', 'details']
def __init__(self, *args, **kwargs):
super(EditInterfaceForm, self).__init__(*args, **kwargs)
......@@ -62,13 +63,15 @@ class EditInterfaceForm(ModelForm):
self.fields['type'].empty_label = "Séléctionner un type de machine"
if "ipv4" in self.fields:
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).annotate(mtype_id=F('ip_type__machinetype__id'))
# Add it's own address
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance).annotate(mtype_id=F('ip_type__machinetype__id'))
if "machine" in self.fields:
self.fields['machine'].queryset = Machine.objects.all().select_related('user')
class AddInterfaceForm(EditInterfaceForm):
class Meta(EditInterfaceForm.Meta):
fields = ['ipv4','mac_address','type','details']
fields = ['type','ipv4','mac_address','details']
def __init__(self, *args, **kwargs):
infra = kwargs.pop('infra')
......@@ -76,17 +79,17 @@ class AddInterfaceForm(EditInterfaceForm):
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
if not infra:
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False))
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False))
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)).annotate(mtype_id=F('ip_type__machinetype__id'))
else:
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).annotate(mtype_id=F('ip_type__machinetype__id'))
class NewInterfaceForm(EditInterfaceForm):
class Meta(EditInterfaceForm.Meta):
fields = ['mac_address','type','details']
fields = ['type','mac_address','details']
class BaseEditInterfaceForm(EditInterfaceForm):
class Meta(EditInterfaceForm.Meta):
fields = ['ipv4','mac_address','type','details']
fields = ['type','ipv4','mac_address','details']
def __init__(self, *args, **kwargs):
infra = kwargs.pop('infra')
......@@ -94,9 +97,12 @@ class BaseEditInterfaceForm(EditInterfaceForm):
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
if not infra:
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False))
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False))
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False)).annotate(mtype_id=F('ip_type__machinetype__id'))
# Add it's own address
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance).annotate(mtype_id=F('ip_type__machinetype__id'))
else:
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).annotate(mtype_id=F('ip_type__machinetype__id'))
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance).annotate(mtype_id=F('ip_type__machinetype__id'))
class AliasForm(ModelForm):
class Meta:
......
# -*- 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.
......
......@@ -7,6 +7,7 @@ quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
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
......@@ -24,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
{% load bootstrap_form_typeahead %}
{% block title %}Création et modification de machines{% endblock %}
......@@ -38,16 +40,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_form_errors domainform %}
{% endif %}
<form class="form" method="post">
{% csrf_token %}
{% if machineform %}
<h3>Machine</h3>
{% bootstrap_form machineform %}
{% endif %}
{% if interfaceform %}
{% bootstrap_form interfaceform %}
<h3>Interface</h3>
{% if i_bft_param %}
{% bootstrap_form_typeahead interfaceform 'ipv4' bft_param=i_bft_param %}
{% else %}
{% bootstrap_form_typeahead interfaceform 'ipv4' %}
{% endif %}
{% endif %}
{% if domainform %}
<h3>Domaine</h3>
{% bootstrap_form domainform %}
{% endif %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
......
# -*- 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.
# -*- 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_<fieldname>' and 'engine_<fieldname>' 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
[ '<field1>[,<field2>[,...]]' ]
[ {
[ 'choices': {
[ '<field1>': '<choices1>'
[, '<field2>': '<choices2>'
[, ... ] ] ]
} ]
[, 'engine': {
[ '<field1>': '<engine1>'
[, '<field2>': '<engine2>'
[, ... ] ] ]
} ]
[, 'match_func': {
[ '<field1>': '<match_func1>'
[, '<field2>': '<match_func2>'
[, ... ] ] ]
} ]
[, 'update_on': {
[ '<field1>': '<update_on1>'
[, '<field2>': '<update_on2>'
[, ... ] ] ]
} ]
} ]
[ <standard boostrap_form parameters> ]
%}
**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_name ) :
""" The id of the HTML input element """
return 'id_'+f_name
def hidden_id( f_name ):
""" The id of the HTML hidden input element """
return 'typeahead_hidden_'+f_name
def hidden_tag( f_bound, f_name ):
""" The HTML hidden input element """
return render_tag(
'input',
attrs={
'id': hidden_id(f_name),
'name': f_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 + ';\n' + \
'var setup_'+f_name+' = function() {\n' + \
'var engine_'+f_name+' = ' + engine + ';\n' + \
'$("#'+input_id(f_name) + '").typeahead("destroy");\n' + \
'$("#'+input_id(f_name) + '").typeahead(\n' + \
default_datasets( f_name, match_func ) + '\n' + \
');\n' + \
reset_input( f_name, f_bound ) + '\n' + \
'};\n' + \
'$("#'+input_id(f_name) + '").bind(\n' + \
'"typeahead:select", ' + \
typeahead_updater( f_name ) + '\n' + \
').bind(\n' + \
'"typeahead:change", ' + \
typeahead_change( f_name ) + '\n' + \
');\n'
for u_id in update_on :
js_content += '$("#'+u_id+'").change( setup_'+f_name+' );\n'
js_content += '$("#'+input_id(f_name)+'").ready( setup_'+f_name+' );\n'
return render_tag( 'script', content=mark_safe( js_content ) )
def reset_input( f_name, f_bound ) :
""" The JS script to reset the fields values """
return '$("#'+input_id(f_name)+'").typeahead(' \
'"val", ' \
'engine_'+f_name+'.get('+str(f_bound.value())+')[0].value' \
');\n' \
'$("#'+hidden_id(f_name)+'").val('+str(f_bound.value())+');'
def default_choices( f_value ) :
""" The JS script creating the variable choices_<fieldname> """
return '[' + \
', '.join([ \
'{key: ' + (str(choice[0]) if choice[0] != '' else '""') + \
', value: "' + str(choice[1]) + '"}' \
for choice in f_value.choices \
]) + \
']'
def default_engine ( f_name ) :
""" The JS script creating the variable engine_<field_name> """
return 'new Bloodhound({ ' \
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"), ' \
'queryTokenizer: Bloodhound.tokenizers.whitespace, ' \
'local: choices_'+f_name+', ' \
'identify: function(obj) { return obj.key; } ' \
'})'
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 + \
'}'
def default_match_func ( f_name ) :
""" The JS script creating the matching function to use with typeahed """
return 'function(q, sync) {' \
'if (q === "") {' \
'var nb = 10;' \
'var first = [] ;' \
'for ( var i=0 ; i<nb && i<choices_'+f_name+'.length; i++ ) {' \
'first.push(choices_'+f_name+'[i].key);' \
'}' \
'sync(engine_'+f_name+'.get(first));' \
'} else {' \
'engine_'+f_name+'.search(q, sync);' \
'}' \
'}'
def typeahead_updater( f_name ):
""" The JS script creating the function triggered when an item is
selected through typeahead """
return 'function(evt, item) { ' \
'$("#'+hidden_id(f_name)+'").val( item.key ); ' \
'$("#'+hidden_id(f_name)+'").change();' \
'return item; ' \
'}'
def typeahead_change( f_name ):
""" 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(f_name)+'").typeahead("val") === "") {' \
'$("#'+hidden_id(f_name)+'").val(""); ' \
'$("#'+hidden_id(f_name)+'").change();' \
'}' \
'}'
# -*- 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.
......
# -*- 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.
......
# -*- 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.
......@@ -5,6 +6,7 @@
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
# 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
......@@ -53,6 +55,7 @@ from .models import IpType, Machine, Interface, IpList, MachineType, Extension,
from users.models import User
from users.models import all_has_access
from preferences.models import GeneralOption, OptionalMachine
from .templatetags.bootstrap_form_typeahead import hidden_id, input_id
def all_active_interfaces():
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
......@@ -75,6 +78,81 @@ def form(ctx, template, request):
c.update(csrf(request))
return render(request, template, c)
def f_type_id( is_type_tt ):
""" The id that will be used in HTML to store the value of the field
type. Depends on the fact that type is generate using typeahead or not
"""
return hidden_id('type') if is_type_tt else input_id('type')
def generate_ipv4_choices( form ) :
""" Generate the parameter choices for the bootstrap_form_typeahead tag
"""
f_ipv4 = form.fields['ipv4']
used_mtype_id = []
choices = '{ "": [{key: "", value: "Choisissez d\'abord un type de machine"},'
mtype_id = -1
for ip in f_ipv4.queryset.order_by('mtype_id', 'id') :
if mtype_id != ip.mtype_id :
mtype_id = ip.mtype_id
used_mtype_id.append(mtype_id)
choices += '], "'+str(mtype_id)+'": ['
choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},'
choices += '{key: ' + str(ip.id) + ', value: "' + str(ip.ipv4) + '"},'
for t in form.fields['type'].queryset.exclude(id__in=used_mtype_id) :
choices += '], "'+str(t.id)+'": ['
choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},'
choices += ']}'
return choices
def generate_ipv4_engine( is_type_tt ) :
""" Generate the parameter engine for the bootstrap_form_typeahead tag
"""
return 'new Bloodhound({ ' \
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"), ' \
'queryTokenizer: Bloodhound.tokenizers.whitespace, ' \
'local: choices_ipv4[$("#'+f_type_id(is_type_tt)+'").val()], ' \
'identify: function(obj) { return obj.key; } ' \
'})'
def generate_ipv4_match_func( is_type_tt ) :
""" Generate the parameter match_func for the bootstrap_form_typeahead tag
"""
return 'function(q, sync) {' \
'if (q === "") {' \
'var nb = 10;' \
'var first = [] ;' \
'for(' \
'var i=0 ;' \
'i<nb && i<choices_ipv4[' \
'$("#'+f_type_id(is_type_tt)+'").val()' \
'].length ;' \
'i++' \
') { first.push(' \
'choices_ipv4[$("#'+f_type_id(is_type_tt)+'").val()][i].key' \
'); }' \
'sync(engine_ipv4.get(first));' \
'} else {' \
'engine_ipv4.search(q, sync);' \
'}' \
'}'
def generate_ipv4_bft_param( form, is_type_tt ):
""" Generate all the parameters to use with the bootstrap_form_typeahead
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 = {
'choices': i_choices,
'engine': i_engine,
'match_func': i_match_func,
'update_on': i_update_on
}
return i_bft_param
@login_required
def new_machine(request, userid):
try:
......@@ -118,7 +196,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))
return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'machines/machine.html', request)
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)
@login_required
def edit_interface(request, interfaceid):
......@@ -155,7 +234,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))
return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'machines/machine.html', request)
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)
@login_required
def del_machine(request, machineid):
......@@ -211,7 +291,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))
return form({'interfaceform': interface_form, 'domainform': domain_form}, 'machines/machine.html', request)
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)
@login_required
def del_interface(request, interfaceid):
......
span.twitter-typeahead .tt-menu,
span.twitter-typeahead .tt-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;