mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-30 08:32:26 +00:00
Merge branch 'faster_ipform' into 'master'
Faster ipform See merge request rezo/re2o!13
This commit is contained in:
commit
7fd3750a33
12 changed files with 7857 additions and 21 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
@ -5,6 +6,7 @@
|
||||||
# Copyright © 2017 Gabriel Détraz
|
# Copyright © 2017 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
|
# Copyright © 2017 Maël Kervella
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# 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.forms import ModelForm, Form, ValidationError
|
||||||
from django import forms
|
from django import forms
|
||||||
from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType, OuverturePortList, OuverturePort
|
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 django.core.validators import validate_email
|
||||||
|
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
@ -52,8 +54,7 @@ class BaseEditMachineForm(EditMachineForm):
|
||||||
class EditInterfaceForm(ModelForm):
|
class EditInterfaceForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
# fields = '__all__'
|
fields = ['machine', 'type', 'ipv4', 'mac_address', 'details']
|
||||||
exclude = ['port_lists']
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(EditInterfaceForm, self).__init__(*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"
|
self.fields['type'].empty_label = "Séléctionner un type de machine"
|
||||||
if "ipv4" in self.fields:
|
if "ipv4" in self.fields:
|
||||||
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
|
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:
|
if "machine" in self.fields:
|
||||||
self.fields['machine'].queryset = Machine.objects.all().select_related('user')
|
self.fields['machine'].queryset = Machine.objects.all().select_related('user')
|
||||||
|
|
||||||
class AddInterfaceForm(EditInterfaceForm):
|
class AddInterfaceForm(EditInterfaceForm):
|
||||||
class Meta(EditInterfaceForm.Meta):
|
class Meta(EditInterfaceForm.Meta):
|
||||||
fields = ['ipv4','mac_address','type','details']
|
fields = ['type','ipv4','mac_address','details']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
infra = kwargs.pop('infra')
|
infra = kwargs.pop('infra')
|
||||||
|
@ -76,17 +79,17 @@ class AddInterfaceForm(EditInterfaceForm):
|
||||||
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
|
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
|
||||||
if not infra:
|
if not infra:
|
||||||
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False))
|
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:
|
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 NewInterfaceForm(EditInterfaceForm):
|
||||||
class Meta(EditInterfaceForm.Meta):
|
class Meta(EditInterfaceForm.Meta):
|
||||||
fields = ['mac_address','type','details']
|
fields = ['type','mac_address','details']
|
||||||
|
|
||||||
class BaseEditInterfaceForm(EditInterfaceForm):
|
class BaseEditInterfaceForm(EditInterfaceForm):
|
||||||
class Meta(EditInterfaceForm.Meta):
|
class Meta(EditInterfaceForm.Meta):
|
||||||
fields = ['ipv4','mac_address','type','details']
|
fields = ['type','ipv4','mac_address','details']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
infra = kwargs.pop('infra')
|
infra = kwargs.pop('infra')
|
||||||
|
@ -94,9 +97,12 @@ class BaseEditInterfaceForm(EditInterfaceForm):
|
||||||
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
|
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
|
||||||
if not infra:
|
if not infra:
|
||||||
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False))
|
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:
|
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 AliasForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -7,6 +7,7 @@ quelques clics.
|
||||||
Copyright © 2017 Gabriel Détraz
|
Copyright © 2017 Gabriel Détraz
|
||||||
Copyright © 2017 Goulven Kermarec
|
Copyright © 2017 Goulven Kermarec
|
||||||
Copyright © 2017 Augustin Lemesle
|
Copyright © 2017 Augustin Lemesle
|
||||||
|
Copyright © 2017 Maël Kervella
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
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
|
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 %}
|
{% endcomment %}
|
||||||
|
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
|
{% load bootstrap_form_typeahead %}
|
||||||
|
|
||||||
{% block title %}Création et modification de machines{% endblock %}
|
{% 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 %}
|
{% bootstrap_form_errors domainform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if machineform %}
|
{% if machineform %}
|
||||||
|
<h3>Machine</h3>
|
||||||
{% bootstrap_form machineform %}
|
{% bootstrap_form machineform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if interfaceform %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% if domainform %}
|
{% if domainform %}
|
||||||
|
<h3>Domaine</h3>
|
||||||
{% bootstrap_form domainform %}
|
{% bootstrap_form domainform %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
|
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
|
||||||
|
|
21
machines/templatetags/__init__.py
Normal file
21
machines/templatetags/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
330
machines/templatetags/bootstrap_form_typeahead.py
Normal file
330
machines/templatetags/bootstrap_form_typeahead.py
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
# -*- 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();' \
|
||||||
|
'}' \
|
||||||
|
'}'
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# -*- mode: python; coding: utf-8 -*-
|
||||||
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
# 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
|
# se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
# quelques clics.
|
# quelques clics.
|
||||||
|
@ -5,6 +6,7 @@
|
||||||
# Copyright © 2017 Gabriel Détraz
|
# Copyright © 2017 Gabriel Détraz
|
||||||
# Copyright © 2017 Goulven Kermarec
|
# Copyright © 2017 Goulven Kermarec
|
||||||
# Copyright © 2017 Augustin Lemesle
|
# Copyright © 2017 Augustin Lemesle
|
||||||
|
# Copyright © 2017 Maël Kervella
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or modify
|
# 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
|
# 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 User
|
||||||
from users.models import all_has_access
|
from users.models import all_has_access
|
||||||
from preferences.models import GeneralOption, OptionalMachine
|
from preferences.models import GeneralOption, OptionalMachine
|
||||||
|
from .templatetags.bootstrap_form_typeahead import hidden_id, input_id
|
||||||
|
|
||||||
def all_active_interfaces():
|
def all_active_interfaces():
|
||||||
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
|
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
|
||||||
|
@ -75,6 +78,81 @@ def form(ctx, template, request):
|
||||||
c.update(csrf(request))
|
c.update(csrf(request))
|
||||||
return render(request, template, c)
|
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
|
@login_required
|
||||||
def new_machine(request, userid):
|
def new_machine(request, userid):
|
||||||
try:
|
try:
|
||||||
|
@ -92,7 +170,7 @@ def new_machine(request, userid):
|
||||||
messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces)
|
messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces)
|
||||||
return redirect("/users/profil/" + str(request.user.id))
|
return redirect("/users/profil/" + str(request.user.id))
|
||||||
machine = NewMachineForm(request.POST or None)
|
machine = NewMachineForm(request.POST or None)
|
||||||
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
|
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
|
||||||
nb_machine = Interface.objects.filter(machine__user=userid).count()
|
nb_machine = Interface.objects.filter(machine__user=userid).count()
|
||||||
domain = DomainForm(request.POST or None, user=user, nb_machine=nb_machine)
|
domain = DomainForm(request.POST or None, user=user, nb_machine=nb_machine)
|
||||||
if machine.is_valid() and interface.is_valid():
|
if machine.is_valid() and interface.is_valid():
|
||||||
|
@ -118,7 +196,8 @@ def new_machine(request, userid):
|
||||||
reversion.set_comment("Création")
|
reversion.set_comment("Création")
|
||||||
messages.success(request, "La machine a été créée")
|
messages.success(request, "La machine a été créée")
|
||||||
return redirect("/users/profil/" + str(user.id))
|
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
|
@login_required
|
||||||
def edit_interface(request, interfaceid):
|
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))
|
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")
|
messages.success(request, "La machine a été modifiée")
|
||||||
return redirect("/users/profil/" + str(interface.machine.user.id))
|
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
|
@login_required
|
||||||
def del_machine(request, machineid):
|
def del_machine(request, machineid):
|
||||||
|
@ -211,7 +291,8 @@ def new_interface(request, machineid):
|
||||||
reversion.set_comment("Création")
|
reversion.set_comment("Création")
|
||||||
messages.success(request, "L'interface a été ajoutée")
|
messages.success(request, "L'interface a été ajoutée")
|
||||||
return redirect("/users/profil/" + str(machine.user.id))
|
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
|
@login_required
|
||||||
def del_interface(request, interfaceid):
|
def del_interface(request, interfaceid):
|
||||||
|
@ -869,7 +950,7 @@ def history(request, object, id):
|
||||||
object_instance = Text.objects.get(pk=id)
|
object_instance = Text.objects.get(pk=id)
|
||||||
except Text.DoesNotExist:
|
except Text.DoesNotExist:
|
||||||
messages.error(request, "Text inexistant")
|
messages.error(request, "Text inexistant")
|
||||||
return redirect("/machines/")
|
return redirect("/machines/")
|
||||||
elif object == 'ns' and request.user.has_perms(('cableur',)):
|
elif object == 'ns' and request.user.has_perms(('cableur',)):
|
||||||
try:
|
try:
|
||||||
object_instance = Ns.objects.get(pk=id)
|
object_instance = Ns.objects.get(pk=id)
|
||||||
|
@ -916,7 +997,7 @@ def history(request, object, id):
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('cableur')
|
@permission_required('cableur')
|
||||||
def index_portlist(request):
|
def index_portlist(request):
|
||||||
port_list = OuverturePortList.objects.all().order_by('name')
|
port_list = OuverturePortList.objects.all().order_by('name')
|
||||||
return render(request, "machines/index_portlist.html", {'port_list':port_list})
|
return render(request, "machines/index_portlist.html", {'port_list':port_list})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -929,7 +1010,7 @@ def edit_portlist(request, pk):
|
||||||
return redirect("/machines/index_portlist/")
|
return redirect("/machines/index_portlist/")
|
||||||
port_list = EditOuverturePortListForm(request.POST or None, instance=port_list_instance)
|
port_list = EditOuverturePortListForm(request.POST or None, instance=port_list_instance)
|
||||||
port_formset = modelformset_factory(
|
port_formset = modelformset_factory(
|
||||||
OuverturePort,
|
OuverturePort,
|
||||||
fields=('begin','end','protocole','io'),
|
fields=('begin','end','protocole','io'),
|
||||||
extra=0,
|
extra=0,
|
||||||
can_delete=True,
|
can_delete=True,
|
||||||
|
@ -968,7 +1049,7 @@ def del_portlist(request, pk):
|
||||||
def add_portlist(request):
|
def add_portlist(request):
|
||||||
port_list = EditOuverturePortListForm(request.POST or None)
|
port_list = EditOuverturePortListForm(request.POST or None)
|
||||||
port_formset = modelformset_factory(
|
port_formset = modelformset_factory(
|
||||||
OuverturePort,
|
OuverturePort,
|
||||||
fields=('begin','end','protocole','io'),
|
fields=('begin','end','protocole','io'),
|
||||||
extra=0,
|
extra=0,
|
||||||
can_delete=True,
|
can_delete=True,
|
||||||
|
|
93
static/css/typeaheadjs.css
Normal file
93
static/css/typeaheadjs.css
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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;
|
||||||
|
padding: 5px 0;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
span.twitter-typeahead .tt-suggestion {
|
||||||
|
display: block;
|
||||||
|
padding: 3px 20px;
|
||||||
|
clear: both;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #333333;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
span.twitter-typeahead .tt-suggestion.tt-cursor,
|
||||||
|
span.twitter-typeahead .tt-suggestion:hover,
|
||||||
|
span.twitter-typeahead .tt-suggestion:focus {
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: 0;
|
||||||
|
background-color: #337ab7;
|
||||||
|
}
|
||||||
|
.input-group.input-group-lg span.twitter-typeahead .form-control {
|
||||||
|
height: 46px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.3333333;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.input-group.input-group-sm span.twitter-typeahead .form-control {
|
||||||
|
height: 30px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
span.twitter-typeahead {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.input-group span.twitter-typeahead {
|
||||||
|
display: block !important;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
.input-group span.twitter-typeahead .tt-menu,
|
||||||
|
.input-group span.twitter-typeahead .tt-dropdown-menu {
|
||||||
|
top: 32px !important;
|
||||||
|
}
|
||||||
|
.input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.input-group span.twitter-typeahead:first-child .form-control {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.input-group span.twitter-typeahead:last-child .form-control {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.input-group.input-group-sm span.twitter-typeahead {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
.input-group.input-group-sm span.twitter-typeahead .tt-menu,
|
||||||
|
.input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu {
|
||||||
|
top: 30px !important;
|
||||||
|
}
|
||||||
|
.input-group.input-group-lg span.twitter-typeahead {
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
.input-group.input-group-lg span.twitter-typeahead .tt-menu,
|
||||||
|
.input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu {
|
||||||
|
top: 46px !important;
|
||||||
|
}
|
4840
static/js/handlebars.js
Normal file
4840
static/js/handlebars.js
Normal file
File diff suppressed because one or more lines are too long
2451
static/js/typeahead.js
Normal file
2451
static/js/typeahead.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -32,8 +32,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<head>
|
<head>
|
||||||
{# Load CSS and JavaScript #}
|
{# Load CSS and JavaScript #}
|
||||||
{% bootstrap_css %}
|
{% bootstrap_css %}
|
||||||
|
<link href="/static/css/typeaheadjs.css" rel="stylesheet">
|
||||||
|
|
||||||
{% bootstrap_javascript %}
|
{% bootstrap_javascript %}
|
||||||
|
<script src="/static/js/typeahead.js"></script>
|
||||||
|
<script src="/static/js/handlebars.js"></script>
|
||||||
<link rel="stylesheet" href="{% static "/css/base.css" %}">
|
<link rel="stylesheet" href="{% static "/css/base.css" %}">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>
|
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>
|
||||||
|
|
Loading…
Reference in a new issue