mirror of https://gitlab.federez.net/re2o/re2o
Browse Source
Autocomplete light See merge request re2o/re2o!582fix_api_permissions_queryset_property_access
50 changed files with 1143 additions and 4813 deletions
@ -0,0 +1,50 @@ |
|||||
|
# -*- mode: python; coding: utf-8 -*- |
||||
|
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il |
||||
|
# se veut agnostique au réseau considéré, de manière à être installable en |
||||
|
# quelques clics. |
||||
|
# |
||||
|
# Copyright © 2017-2020 Gabriel Détraz |
||||
|
# Copyright © 2017-2020 Jean-Romain Garnier |
||||
|
# |
||||
|
# 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. |
||||
|
|
||||
|
# App de gestion des users pour re2o |
||||
|
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin |
||||
|
# Gplv2 |
||||
|
""" |
||||
|
Django views autocomplete view |
||||
|
|
||||
|
Here are defined the autocomplete class based view. |
||||
|
|
||||
|
""" |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db.models import Q, Value, CharField |
||||
|
|
||||
|
from .models import ( |
||||
|
Banque |
||||
|
) |
||||
|
|
||||
|
from re2o.views import AutocompleteViewMixin |
||||
|
|
||||
|
from re2o.acl import ( |
||||
|
can_view_all, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class BanqueAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Banque |
||||
|
|
||||
|
|
||||
@ -0,0 +1,105 @@ |
|||||
|
# -*- mode: python; coding: utf-8 -*- |
||||
|
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il |
||||
|
# se veut agnostique au réseau considéré, de manière à être installable en |
||||
|
# quelques clics. |
||||
|
# |
||||
|
# Copyright © 2017-2020 Gabriel Détraz |
||||
|
# Copyright © 2017-2020 Jean-Romain Garnier |
||||
|
# |
||||
|
# 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. |
||||
|
|
||||
|
# App de gestion des users pour re2o |
||||
|
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin |
||||
|
# Gplv2 |
||||
|
""" |
||||
|
Django views autocomplete view |
||||
|
|
||||
|
Here are defined the autocomplete class based view. |
||||
|
|
||||
|
""" |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db.models import Q, Value, CharField |
||||
|
from django.db.models.functions import Concat |
||||
|
|
||||
|
from .models import ( |
||||
|
Interface, |
||||
|
Machine, |
||||
|
Vlan, |
||||
|
MachineType, |
||||
|
IpType, |
||||
|
Extension, |
||||
|
Domain, |
||||
|
OuverturePortList, |
||||
|
IpList, |
||||
|
) |
||||
|
|
||||
|
from re2o.views import AutocompleteViewMixin |
||||
|
|
||||
|
|
||||
|
class VlanAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Vlan |
||||
|
|
||||
|
|
||||
|
class MachineAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Machine |
||||
|
|
||||
|
|
||||
|
class MachineTypeAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = MachineType |
||||
|
|
||||
|
|
||||
|
class IpTypeAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = IpType |
||||
|
|
||||
|
|
||||
|
class ExtensionAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Extension |
||||
|
|
||||
|
|
||||
|
class DomainAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Domain |
||||
|
|
||||
|
|
||||
|
class OuverturePortListAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = OuverturePortList |
||||
|
|
||||
|
|
||||
|
class InterfaceAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Interface |
||||
|
|
||||
|
# Precision on search to add annotations so search behaves more like users expect it to |
||||
|
def filter_results(self): |
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(domain__name__icontains=self.q) | Q(machine__name__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class IpListAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = IpList |
||||
|
|
||||
|
# Precision on search to add annotations so search behaves more like users expect it to |
||||
|
def filter_results(self): |
||||
|
machine_type = self.forwarded.get("machine_type", None) |
||||
|
self.query_set = self.query_set.filter(interface__isnull=True) |
||||
|
|
||||
|
if machine_type: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
ip_type__machinetype__id=machine_type |
||||
|
) |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter(Q(ipv4__startswith=self.q)) |
||||
@ -1,2 +1,3 @@ |
|||||
django-bootstrap3==11.1.0 |
django-bootstrap3==11.1.0 |
||||
django-macaddress==1.6.0 |
django-macaddress==1.6.0 |
||||
|
django-autocomplete-light==3.8.1 |
||||
|
|||||
@ -1,752 +0,0 @@ |
|||||
# -*- mode: python; coding: utf-8 -*- |
|
||||
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il |
|
||||
# se veut agnostique au réseau considéré, de manière à être installable en |
|
||||
# quelques clics. |
|
||||
# |
|
||||
# Copyright © 2017 Maël Kervella |
|
||||
# |
|
||||
# This program is free software; you can redistribute it and/or modify |
|
||||
# it under the terms of the GNU General Public License as published by |
|
||||
# the Free Software Foundation; either version 2 of the License, or |
|
||||
# (at your option) any later version. |
|
||||
# |
|
||||
# This program is distributed in the hope that it will be useful, |
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
# GNU General Public License for more details. |
|
||||
# |
|
||||
# You should have received a copy of the GNU General Public License along |
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., |
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
|
||||
|
|
||||
""" Templatetag used to render massive django form selects into bootstrap |
|
||||
forms that can still be manipulating even if there is multiple tens of |
|
||||
thousands of elements in the select. It's made possible using JS libaries |
|
||||
Twitter Typeahead and Splitree's Tokenfield. |
|
||||
See docstring of massive_bootstrap_form for a detailed explaantion on how |
|
||||
to use this templatetag. |
|
||||
""" |
|
||||
|
|
||||
from django import template |
|
||||
from django.utils.safestring import mark_safe |
|
||||
from django.forms import TextInput |
|
||||
from django.forms.widgets import Select |
|
||||
from django.utils.translation import ugettext_lazy as _ |
|
||||
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_<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 (optional) |
|
||||
A dict of list of ids that the values depends on. The engine |
|
||||
and the typeahead properties are recalculated and reapplied. |
|
||||
Example : |
|
||||
'update_on' : { |
|
||||
'field_A' : [ 'id0', 'id1', ... ] , |
|
||||
'field_B' : ... , |
|
||||
... |
|
||||
} |
|
||||
|
|
||||
gen_select (optional) |
|
||||
A dict of boolean telling if the form should either generate |
|
||||
the normal select (set to true) and then use it to generate |
|
||||
the possible choices and then remove it or either (set to |
|
||||
false) generate the choices variable in this tag and do not |
|
||||
send any select. |
|
||||
Sending the select before can be usefull to permit the use |
|
||||
without any JS enabled but it will execute more code locally |
|
||||
for the client so the loading might be slower. |
|
||||
If not specified, this variable is set to true for each field |
|
||||
Example : |
|
||||
'gen_select' : { |
|
||||
'field_A': True , |
|
||||
'field_B': ... , |
|
||||
... |
|
||||
} |
|
||||
|
|
||||
See boostrap_form_ for other arguments |
|
||||
|
|
||||
**Usage**:: |
|
||||
|
|
||||
{% massive_bootstrap_form |
|
||||
form |
|
||||
[ '<field1>[,<field2>[,...]]' ] |
|
||||
[ mbf_param = { |
|
||||
[ '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>' |
|
||||
[, ... ] ] ] |
|
||||
} ], |
|
||||
[, 'gen_select': { |
|
||||
[ '<field1>': '<gen_select1>' |
|
||||
[, '<field2>': '<gen_select2>' |
|
||||
[, ... ] ] ] |
|
||||
} ] |
|
||||
} ] |
|
||||
[ <standard boostrap_form parameters> ] |
|
||||
%} |
|
||||
|
|
||||
**Example**: |
|
||||
|
|
||||
{% massive_bootstrap_form form 'ipv4' choices='[...]' %} |
|
||||
""" |
|
||||
|
|
||||
mbf_form = MBFForm(form, mbf_fields.split(","), *args, **kwargs) |
|
||||
return mbf_form.render() |
|
||||
|
|
||||
|
|
||||
class MBFForm: |
|
||||
""" An object to hold all the information and useful methods needed to |
|
||||
create and render a massive django form into an actual HTML and JS |
|
||||
code able to handle it correctly. |
|
||||
Every field that is not listed is rendered as a normal bootstrap_field. |
|
||||
""" |
|
||||
|
|
||||
def __init__(self, form, mbf_fields, *args, **kwargs): |
|
||||
# The django form object |
|
||||
self.form = form |
|
||||
# The fields on which to use JS |
|
||||
self.fields = mbf_fields |
|
||||
|
|
||||
# Other bootstrap_form arguments to render the fields |
|
||||
self.args = args |
|
||||
self.kwargs = kwargs |
|
||||
|
|
||||
# Fields to exclude form the form rendering |
|
||||
self.exclude = self.kwargs.get("exclude", "").split(",") |
|
||||
|
|
||||
# All the mbf parameters specified byt the user |
|
||||
param = kwargs.pop("mbf_param", {}) |
|
||||
self.choices = param.get("choices", {}) |
|
||||
self.engine = param.get("engine", {}) |
|
||||
self.match_func = param.get("match_func", {}) |
|
||||
self.update_on = param.get("update_on", {}) |
|
||||
self.gen_select = param.get("gen_select", {}) |
|
||||
self.hidden_fields = [h.name for h in self.form.hidden_fields()] |
|
||||
|
|
||||
# HTML code to insert inside a template |
|
||||
self.html = "" |
|
||||
|
|
||||
def render(self): |
|
||||
""" HTML code for the fully rendered form with all the necessary form |
|
||||
""" |
|
||||
for name, field in self.form.fields.items(): |
|
||||
if name not in self.exclude: |
|
||||
|
|
||||
if name in self.fields and name not in self.hidden_fields: |
|
||||
mbf_field = MBFField( |
|
||||
name, |
|
||||
field, |
|
||||
field.get_bound_field(self.form, name), |
|
||||
self.choices.get(name, None), |
|
||||
self.engine.get(name, None), |
|
||||
self.match_func.get(name, None), |
|
||||
self.update_on.get(name, None), |
|
||||
self.gen_select.get(name, True), |
|
||||
*self.args, |
|
||||
**self.kwargs |
|
||||
) |
|
||||
self.html += mbf_field.render() |
|
||||
|
|
||||
else: |
|
||||
f = field.get_bound_field(self.form, name), self.args, self.kwargs |
|
||||
self.html += render_field( |
|
||||
field.get_bound_field(self.form, name), |
|
||||
*self.args, |
|
||||
**self.kwargs |
|
||||
) |
|
||||
|
|
||||
return mark_safe(self.html) |
|
||||
|
|
||||
|
|
||||
class MBFField: |
|
||||
""" An object to hold all the information and useful methods needed to |
|
||||
create and render a massive django form field into an actual HTML and JS |
|
||||
code able to handle it correctly. |
|
||||
Twitter Typeahead is used for the display and the matching of queries and |
|
||||
in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage |
|
||||
multiple values. |
|
||||
A div with only non visible elements is created after the div containing |
|
||||
the displayed input. It's used to store the actual data that will be sent |
|
||||
to the server """ |
|
||||
|
|
||||
def __init__( |
|
||||
self, |
|
||||
name_, |
|
||||
field_, |
|
||||
bound_, |
|
||||
choices_, |
|
||||
engine_, |
|
||||
match_func_, |
|
||||
update_on_, |
|
||||
gen_select_, |
|
||||
*args_, |
|
||||
**kwargs_ |
|
||||
): |
|
||||
|
|
||||
# Verify this field is a Select (or MultipleSelect) (only supported) |
|
||||
if not isinstance(field_.widget, Select): |
|
||||
raise ValueError( |
|
||||
( |
|
||||
"Field named {f_name} is not a Select and" |
|
||||
"can't be rendered with massive_bootstrap_form." |
|
||||
).format(f_name=name_) |
|
||||
) |
|
||||
|
|
||||
# Name of the field |
|
||||
self.name = name_ |
|
||||
# Django field object |
|
||||
self.field = field_ |
|
||||
# Bound Django field associated with field |
|
||||
self.bound = bound_ |
|
||||
|
|
||||
# Id for the main visible input |
|
||||
self.input_id = self.bound.auto_id |
|
||||
# Id for a hidden input used to store the value |
|
||||
self.hidden_id = self.input_id + "_hidden" |
|
||||
# Id for another div containing hidden inputs and script |
|
||||
self.div2_id = self.input_id + "_div" |
|
||||
|
|
||||
# Should the standard select should be generated |
|
||||
self.gen_select = gen_select_ |
|
||||
# Is it select with multiple values possible (use of tokenfield) |
|
||||
self.multiple = self.field.widget.allow_multiple_selected |
|
||||
# JS for the choices variable (user specified or default) |
|
||||
self.choices = choices_ or self.default_choices() |
|
||||
# JS for the engine variable (typeahead) (user specified or default) |
|
||||
self.engine = engine_ or self.default_engine() |
|
||||
# JS for the matching function (typeahead) (user specified or default) |
|
||||
self.match_func = match_func_ or self.default_match_func() |
|
||||
# JS for the datasets variable (typeahead) (user specified or default) |
|
||||
self.datasets = self.default_datasets() |
|
||||
# Ids of other fields to bind a reset/reload with when changed |
|
||||
self.update_on = update_on_ or [] |
|
||||
|
|
||||
# Whole HTML code to insert in the template |
|
||||
self.html = "" |
|
||||
# JS code in the script tag |
|
||||
self.js_script = "" |
|
||||
# Input tag to display instead of select |
|
||||
self.replace_input = None |
|
||||
|
|
||||
# Other bootstrap_form arguments to render the fields |
|
||||
self.args = args_ |
|
||||
self.kwargs = kwargs_ |
|
||||
|
|
||||
def default_choices(self): |
|
||||
""" JS code of the variable choices_<fieldname> """ |
|
||||
|
|
||||
if self.gen_select: |
|
||||
return ( |
|
||||
"function plop(o) {{" |
|
||||
"var c = [];" |
|
||||
"for( let i=0 ; i<o.length ; i++) {{" |
|
||||
" c.push( {{ key: o[i].value, value: o[i].text }} );" |
|
||||
"}}" |
|
||||
"return c;" |
|
||||
'}} ($("#{select_id}")[0].options)' |
|
||||
).format(select_id=self.input_id) |
|
||||
|
|
||||
else: |
|
||||
return "[{objects}]".format( |
|
||||
objects=",".join( |
|
||||
[ |
|
||||
'{{key:{k},value:"{v}"}}'.format( |
|
||||
k=choice[0] if choice[0] != "" else '""', v=choice[1] |
|
||||
) |
|
||||
for choice in self.field.choices |
|
||||
] |
|
||||
) |
|
||||
) |
|
||||
|
|
||||
def default_engine(self): |
|
||||
""" Default JS code of the variable engine_<field_name> """ |
|
||||
return ( |
|
||||
"new Bloodhound({{" |
|
||||
' datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),' |
|
||||
" queryTokenizer: Bloodhound.tokenizers.whitespace," |
|
||||
" local: choices_{name}," |
|
||||
" identify: function(obj) {{ return obj.key; }}" |
|
||||
"}})" |
|
||||
).format(name=self.name) |
|
||||
|
|
||||
def default_datasets(self): |
|
||||
""" Default JS script of the datasets to use with typeahead """ |
|
||||
return ( |
|
||||
"{{" |
|
||||
" hint: true," |
|
||||
" highlight: true," |
|
||||
" minLength: 0" |
|
||||
"}}," |
|
||||
"{{" |
|
||||
' display: "value",' |
|
||||
' name: "{name}",' |
|
||||
" source: {match_func}" |
|
||||
"}}" |
|
||||
).format(name=self.name, match_func=self.match_func) |
|
||||
|
|
||||
def default_match_func(self): |
|
||||
""" Default JS code of the matching function to use with typeahed """ |
|
||||
return ( |
|
||||
"function ( q, sync ) {{" |
|
||||
' if ( q === "" ) {{' |
|
||||
" var first = choices_{name}.slice( 0, 5 ).map(" |
|
||||
" function ( obj ) {{ return obj.key; }}" |
|
||||
" );" |
|
||||
" sync( engine_{name}.get( first ) );" |
|
||||
" }} else {{" |
|
||||
" engine_{name}.search( q, sync );" |
|
||||
" }}" |
|
||||
"}}" |
|
||||
).format(name=self.name) |
|
||||
|
|
||||
def render(self): |
|
||||
""" HTML code for the fully rendered field """ |
|
||||
self.gen_displayed_div() |
|
||||
self.gen_hidden_div() |
|
||||
return mark_safe(self.html) |
|
||||
|
|
||||
def gen_displayed_div(self): |
|
||||
""" Generate HTML code for the div that contains displayed tags """ |
|
||||
if self.gen_select: |
|
||||
self.html += render_field(self.bound, *self.args, **self.kwargs) |
|
||||
|
|
||||
self.field.widget = TextInput( |
|
||||
attrs={ |
|
||||
"name": "mbf_" + self.name, |
|
||||
"placeholder": getattr(self.field, "empty_label", _("Nothing")), |
|
||||
} |
|
||||
) |
|
||||
self.replace_input = render_field(self.bound, *self.args, **self.kwargs) |
|
||||
|
|
||||
if not self.gen_select: |
|
||||
self.html += self.replace_input |
|
||||
|
|
||||
def gen_hidden_div(self): |
|
||||
""" Generate HTML code for the div that contains hidden tags """ |
|
||||
self.gen_full_js() |
|
||||
|
|
||||
content = self.js_script |
|
||||
if not self.multiple and not self.gen_select: |
|
||||
content += self.hidden_input() |
|
||||
|
|
||||
self.html += render_tag("div", content=content, attrs={"id": self.div2_id}) |
|
||||
|
|
||||
def hidden_input(self): |
|
||||
""" HTML for the hidden input element """ |
|
||||
return render_tag( |
|
||||
"input", |
|
||||
attrs={ |
|
||||
"id": self.hidden_id, |
|
||||
"name": self.bound.html_name, |
|
||||
"type": "hidden", |
|
||||
"value": self.bound.value() or "", |
|
||||
}, |
|
||||
) |
|
||||
|
|
||||
def gen_full_js(self): |
|
||||
""" Generate the full script tag containing the JS code """ |
|
||||
self.create_js() |
|
||||
self.fill_js() |
|
||||
self.get_script() |
|
||||
|
|
||||
def create_js(self): |
|
||||
""" Generate a template for the whole script to use depending on |
|
||||
gen_select and multiple """ |
|
||||
if self.gen_select: |
|
||||
if self.multiple: |
|
||||
self.js_script = ( |
|
||||
'$( "#{input_id}" ).ready( function() {{' |
|
||||
" var choices_{f_name} = {choices};" |
|
||||
" {del_select}" |
|
||||
" 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", {tok_create} );' |
|
||||
' $( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );' |
|
||||
' $( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );' |
|
||||
" {tok_updates}" |
|
||||
" setup_{f_name}();" |
|
||||
" {tok_init_input}" |
|
||||
"}} );" |
|
||||
) |
|
||||
else: |
|
||||
self.js_script = ( |
|
||||
'$( "#{input_id}" ).ready( function() {{' |
|
||||
" var choices_{f_name} = {choices};" |
|
||||
" {del_select}" |
|
||||
" {gen_hidden}" |
|
||||
" 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", {typ_select} );' |
|
||||
' $( "#{input_id}" ).bind( "typeahead:change", {typ_change} );' |
|
||||
" {typ_updates}" |
|
||||
" setup_{f_name}();" |
|
||||
" {typ_init_input}" |
|
||||
"}} );" |
|
||||
) |
|
||||
else: |
|
||||
if self.multiple: |
|
||||
self.js_script = ( |
|
||||
"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", {tok_create} );' |
|
||||
'$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );' |
|
||||
'$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );' |
|
||||
"{tok_updates}" |
|
||||
'$( "#{input_id}" ).ready( function() {{' |
|
||||
" setup_{f_name}();" |
|
||||
" {tok_init_input}" |
|
||||
"}} );" |
|
||||
) |
|
||||
else: |
|
||||
self.js_script = ( |
|
||||
"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", {typ_select} );' |
|
||||
'$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );' |
|
||||
"{typ_updates}" |
|
||||
'$( "#{input_id}" ).ready( function() {{' |
|
||||
" setup_{f_name}();" |
|
||||
" {typ_init_input}" |
|
||||
"}} );" |
|
||||
) |
|
||||
|
|
||||
# Make sure the visible element doesn't have the same name as the hidden elements |
|
||||
# Otherwise, in the POST request, they collide and an incoherent value is sent |
|
||||
self.js_script += ( |
|
||||
'$( "#{input_id}" ).ready( function() {{' |
|
||||
' $( "#{input_id}" ).attr("name", "mbf_{f_name}");' |
|
||||
"}} );" |
|
||||
) |
|
||||
|
|
||||
def fill_js(self): |
|
||||
""" Fill the template with the correct values """ |
|
||||
self.js_script = self.js_script.format( |
|
||||
f_name=self.name, |
|
||||
choices=self.choices, |
|
||||
del_select=self.del_select(), |
|
||||
gen_hidden=self.gen_hidden(), |
|
||||
engine=self.engine, |
|
||||
input_id=self.input_id, |
|
||||
datasets=self.datasets, |
|
||||
typ_select=self.typeahead_select(), |
|
||||
typ_change=self.typeahead_change(), |
|
||||
tok_create=self.tokenfield_create(), |
|
||||
tok_edit=self.tokenfield_edit(), |
|
||||
tok_remove=self.tokenfield_remove(), |
|
||||
typ_updates=self.typeahead_updates(), |
|
||||
tok_updates=self.tokenfield_updates(), |
|
||||
tok_init_input=self.tokenfield_init_input(), |
|
||||
typ_init_input=self.typeahead_init_input(), |
|
||||
) |
|
||||
|
|
||||
def get_script(self): |
|
||||
""" Insert the JS code inside a script tag """ |
|
||||
self.js_script = render_tag("script", content=mark_safe(self.js_script)) |
|
||||
|
|
||||
def del_select(self): |
|
||||
""" JS code to delete the select if it has been generated and replace |
|
||||
it with an input. """ |
|
||||
return ( |
|
||||
'var p = $("#{select_id}").parent()[0];' |
|
||||
"var new_input = `{replace_input}`;" |
|
||||
"p.innerHTML = new_input;" |
|
||||
).format(select_id=self.input_id, replace_input=self.replace_input) |
|
||||
|
|
||||
def gen_hidden(self): |
|
||||
""" JS code to add a hidden tag to store the value. """ |
|
||||
return ( |
|
||||
'var d = $("#{div2_id}")[0];' |
|
||||
'var i = document.createElement("input");' |
|
||||
'i.id = "{hidden_id}";' |
|
||||
'i.name = "{html_name}";' |
|
||||
'i.value = "";' |
|
||||
'i.type = "hidden";' |
|
||||
"d.appendChild(i);" |
|
||||
).format( |
|
||||
div2_id=self.div2_id, |
|
||||
hidden_id=self.hidden_id, |
|
||||
html_name=self.bound.html_name, |
|
||||
) |
|
||||
|
|
||||
def typeahead_init_input(self): |
|
||||
""" JS code to init the fields values """ |
|
||||
init_key = self.bound.value() or '""' |
|
||||
return ( |
|
||||
'$( "#{input_id}" ).typeahead("val", {init_val});' |
|
||||
'$( "#{hidden_id}" ).val( {init_key} );' |
|
||||
).format( |
|
||||
input_id=self.input_id, |
|
||||
init_val='""' |
|
||||
if init_key == '""' |
|
||||
else "engine_{name}.get( {init_key} )[0].value".format( |
|
||||
name=self.name, init_key=init_key |
|
||||
), |
|
||||
init_key=init_key, |
|
||||
hidden_id=self.hidden_id, |
|
||||
) |
|
||||
|
|
||||
def typeahead_reset_input(self): |
|
||||
""" JS code to reset the fields values """ |
|
||||
return ( |
|
||||
'$( "#{input_id}" ).typeahead("val", "");' '$( "#{hidden_id}" ).val( "" );' |
|
||||
).format(input_id=self.input_id, hidden_id=self.hidden_id) |
|
||||
|
|
||||
def typeahead_select(self): |
|
||||
""" JS code to create 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=self.hidden_id) |
|
||||
|
|
||||
def typeahead_change(self): |
|
||||
""" JS code of 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=self.input_id, hidden_id=self.hidden_id) |
|
||||
|
|
||||
def typeahead_updates(self): |
|
||||
""" JS code for binding external fields changes with a reset """ |
|
||||
reset_input = self.typeahead_reset_input() |
|
||||
updates = [ |
|
||||
( |
|
||||
'$( "#{u_id}" ).change( function() {{' |
|
||||
" setup_{name}();" |
|
||||
" {reset_input}" |
|
||||
"}} );" |
|
||||
).format(u_id=u_id, name=self.name, reset_input=reset_input) |
|
||||
for u_id in self.update_on |
|
||||
] |
|
||||
return "".join(updates) |
|
||||
|
|
||||
def tokenfield_init_input(self): |
|
||||
""" JS code to init the fields values """ |
|
||||
init_key = self.bound.value() or '""' |
|
||||
return ('$( "#{input_id}" ).tokenfield("setTokens", {init_val});').format( |
|
||||
input_id=self.input_id, |
|
||||
init_val='""' |
|
||||
if init_key == '""' |
|
||||
else ( |
|
||||
"engine_{name}.get( {init_key} ).map(" |
|
||||
" function(o) {{ return o.value; }}" |
|
||||
")" |
|
||||
).format(name=self.name, init_key=init_key), |
|
||||
) |
|
||||
|
|
||||
def tokenfield_reset_input(self): |
|
||||
""" JS code to reset the fields values """ |
|
||||
return ('$( "#{input_id}" ).tokenfield("setTokens", "");').format( |
|
||||
input_id=self.input_id |
|
||||
) |
|
||||
|
|
||||
def tokenfield_create(self): |
|
||||
""" JS code 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<choices_{name}.length &&" |
|
||||
" choices_{name}[i].value !== data ) {{" |
|
||||
" i++;" |
|
||||
" }}" |
|
||||
" if ( i === choices_{name}.length ) {{ return false; }}" |
|
||||
" k = choices_{name}[i].key;" |
|
||||
" }}" |
|
||||
' var new_input = document.createElement("input");' |
|
||||
' new_input.type = "hidden";' |
|
||||
' new_input.id = "{hidden_id}_"+k.toString();' |
|
||||
" new_input.value = k.toString();" |
|
||||
' new_input.name = "{html_name}";' |
|
||||
' $( "#{div2_id}" ).append(new_input);' |
|
||||
"}}" |
|
||||
).format( |
|
||||
name=self.name, |
|
||||
hidden_id=self.hidden_id, |
|
||||
html_name=self.bound.html_name, |
|
||||
div2_id=self.div2_id, |
|
||||
) |
|
||||
|
|
||||
def tokenfield_edit(self): |
|
||||
""" JS code triggered when a token is edited in tokenfield. """ |
|
||||
return ( |
|
||||
"function(evt) {{" |
|
||||
" var k = evt.attrs.key;" |
|
||||
" if (!k) {{" |
|
||||
" var data = evt.attrs.value;" |
|
||||
" var i = 0;" |
|
||||
" while ( i<choices_{name}.length &&" |
|
||||
" choices_{name}[i].value !== data ) {{" |
|
||||
" i++;" |
|
||||
" }}" |
|
||||
" if ( i === choices_{name}.length ) {{ return true; }}" |
|
||||
" k = choices_{name}[i].key;" |
|
||||
" }}" |
|
||||
" var old_input = document.getElementById(" |
|
||||
' "{hidden_id}_"+k.toString()' |
|
||||
" );" |
|
||||
" old_input.parentNode.removeChild(old_input);" |
|
||||
"}}" |
|
||||
).format(name=self.name, hidden_id=self.hidden_id) |
|
||||
|
|
||||
def tokenfield_remove(self): |
|
||||
""" JS code trigggered when a token is removed from tokenfield. """ |
|
||||
return ( |
|
||||
"function(evt) {{" |
|
||||
" var k = evt.attrs.key;" |
|
||||
" if (!k) {{" |
|
||||
" var data = evt.attrs.value;" |
|
||||
" var i = 0;" |
|
||||
" while ( i<choices_{name}.length &&" |
|
||||
" choices_{name}[i].value !== data ) {{" |
|
||||
" i++;" |
|
||||
" }}" |
|
||||
" if ( i === choices_{name}.length ) {{ return true; }}" |
|
||||
" k = choices_{name}[i].key;" |
|
||||
" }}" |
|
||||
" var old_input = document.getElementById(" |
|
||||
' "{hidden_id}_"+k.toString()' |
|
||||
" );" |
|
||||
" old_input.parentNode.removeChild(old_input);" |
|
||||
"}}" |
|
||||
).format(name=self.name, hidden_id=self.hidden_id) |
|
||||
|
|
||||
def tokenfield_updates(self): |
|
||||
""" JS code for binding external fields changes with a reset """ |
|
||||
reset_input = self.tokenfield_reset_input() |
|
||||
updates = [ |
|
||||
( |
|
||||
'$( "#{u_id}" ).change( function() {{' |
|
||||
" setup_{name}();" |
|
||||
" {reset_input}" |
|
||||
"}} );" |
|
||||
).format(u_id=u_id, name=self.name, reset_input=reset_input) |
|
||||
for u_id in self.update_on |
|
||||
] |
|
||||
return "".join(updates) |
|
||||
@ -0,0 +1,77 @@ |
|||||
|
# -*- mode: python; coding: utf-8 -*- |
||||
|
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il |
||||
|
# se veut agnostique au réseau considéré, de manière à être installable en |
||||
|
# quelques clics. |
||||
|
# |
||||
|
# Copyright © 2021 Gabriel Détraz |
||||
|
# Copyright © 2021 Jean-Romain Garnier |
||||
|
# |
||||
|
# 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. |
||||
|
""" |
||||
|
Re2o Forms and ModelForms Widgets. |
||||
|
|
||||
|
Used in others forms for using autocomplete engine. |
||||
|
""" |
||||
|
|
||||
|
from django.utils.translation import ugettext as _ |
||||
|
from dal import autocomplete |
||||
|
|
||||
|
|
||||
|
class AutocompleteModelWidget(autocomplete.ModelSelect2): |
||||
|
""" A mixin subclassing django-autocomplete-light's Select2 model to pass default options |
||||
|
See https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#passing-options-to-select2 |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
select2_attrs = kwargs.get("attrs", {}) |
||||
|
kwargs["attrs"] = self.fill_default_select2_attrs(select2_attrs) |
||||
|
|
||||
|
super().__init__(*args, **kwargs) |
||||
|
|
||||
|
def fill_default_select2_attrs(self, attrs): |
||||
|
""" |
||||
|
See https://select2.org/configuration/options-api |
||||
|
""" |
||||
|
# Display the "x" button to clear the input by default |
||||
|
attrs["data-allow-clear"] = attrs.get("data-allow-clear", "true") |
||||
|
# If there are less than 10 results, just show all of them (no need to autocomplete) |
||||
|
attrs["data-minimum-results-for-search"] = attrs.get( |
||||
|
"data-minimum-results-for-search", 10 |
||||
|
) |
||||
|
return attrs |
||||
|
|
||||
|
|
||||
|
class AutocompleteMultipleModelWidget(autocomplete.ModelSelect2Multiple): |
||||
|
""" A mixin subclassing django-autocomplete-light's Select2 model to pass default options |
||||
|
See https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#passing-options-to-select2 |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
select2_attrs = kwargs.get("attrs", {}) |
||||
|
kwargs["attrs"] = self.fill_default_select2_attrs(select2_attrs) |
||||
|
|
||||
|
super().__init__(*args, **kwargs) |
||||
|
|
||||
|
def fill_default_select2_attrs(self, attrs): |
||||
|
""" |
||||
|
See https://select2.org/configuration/options-api |
||||
|
""" |
||||
|
# Display the "x" button to clear the input by default |
||||
|
attrs["data-allow-clear"] = attrs.get("data-allow-clear", "true") |
||||
|
# If there are less than 10 results, just show all of them (no need to autocomplete) |
||||
|
attrs["data-minimum-results-for-search"] = attrs.get( |
||||
|
"data-minimum-results-for-search", 10 |
||||
|
) |
||||
|
return attrs |
||||
@ -0,0 +1,49 @@ |
|||||
|
/* |
||||
|
Don't blame me for all the '!important's |
||||
|
See github.com/yourlabs/django-autocomplete-light/issues/1149 |
||||
|
*/ |
||||
|
|
||||
|
/* dal bootstrap css fix */ |
||||
|
.select2-container { |
||||
|
width: 100% !important; |
||||
|
min-width: 10em !important; |
||||
|
} |
||||
|
|
||||
|
/* django-addanother bootstrap css fix */ |
||||
|
.related-widget-wrapper{ |
||||
|
padding-right: 16px; |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
.related-widget-wrapper-link{ |
||||
|
position: absolute; |
||||
|
top: 3px; |
||||
|
right: 0px; |
||||
|
} |
||||
|
|
||||
|
.select2-container .select2-selection--single { |
||||
|
height: 34px !important; |
||||
|
padding-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.select2-container--default .select2-selection--single .select2-selection__rendered { |
||||
|
line-height: 100% !important; |
||||
|
display: inline !important; |
||||
|
overflow-x: hidden !important; |
||||
|
overflow-y: auto !important; |
||||
|
} |
||||
|
.select2-container .select2-selection--multiple { |
||||
|
min-height: 45px !important; |
||||
|
padding-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.select2-container--default .select2-selection--multiple .select2-selection__rendered { |
||||
|
height: 100% !important; |
||||
|
display: inline !important; |
||||
|
overflow-x: hidden !important; |
||||
|
overflow-y: auto !important; |
||||
|
} |
||||
|
|
||||
|
.select2-container .select2-selection--multiple .select2-selection__rendered { |
||||
|
overflow: auto !important; |
||||
|
} |
||||
@ -1,210 +0,0 @@ |
|||||
/*! |
|
||||
* bootstrap-tokenfield |
|
||||
* https://github.com/sliptree/bootstrap-tokenfield |
|
||||
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT |
|
||||
*/ |
|
||||
@-webkit-keyframes blink { |
|
||||
0% { |
|
||||
border-color: #ededed; |
|
||||
} |
|
||||
100% { |
|
||||
border-color: #b94a48; |
|
||||
} |
|
||||
} |
|
||||
@-moz-keyframes blink { |
|
||||
0% { |
|
||||
border-color: #ededed; |
|
||||
} |
|
||||
100% { |
|
||||
border-color: #b94a48; |
|
||||
} |
|
||||
} |
|
||||
@keyframes blink { |
|
||||
0% { |
|
||||
border-color: #ededed; |
|
||||
} |
|
||||
100% { |
|
||||
border-color: #b94a48; |
|
||||
} |
|
||||
} |
|
||||
.tokenfield { |
|
||||
height: auto; |
|
||||
min-height: 34px; |
|
||||
padding-bottom: 0px; |
|
||||
} |
|
||||
.tokenfield.focus { |
|
||||
border-color: #66afe9; |
|
||||
outline: 0; |
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); |
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); |
|
||||
} |
|
||||
.tokenfield .token { |
|
||||
-webkit-box-sizing: border-box; |
|
||||
-moz-box-sizing: border-box; |
|
||||
box-sizing: border-box; |
|
||||
-webkit-border-radius: 3px; |
|
||||
-moz-border-radius: 3px; |
|
||||
border-radius: 3px; |
|
||||
display: inline-block; |
|
||||
border: 1px solid #d9d9d9; |
|
||||
background-color: #ededed; |
|
||||
white-space: nowrap; |
|
||||
margin: -1px 5px 5px 0; |
|
||||
height: 22px; |
|
||||
vertical-align: top; |
|
||||
cursor: default; |
|
||||
} |
|
||||
.tokenfield .token:hover { |
|
||||
border-color: #b9b9b9; |
|
||||
} |
|
||||
.tokenfield .token.active { |
|
||||
border-color: #52a8ec; |
|
||||
border-color: rgba(82, 168, 236, 0.8); |
|
||||
} |
|
||||
.tokenfield .token.duplicate { |
|
||||
border-color: #ebccd1; |
|
||||
-webkit-animation-name: blink; |
|
||||
animation-name: blink; |
|
||||
-webkit-animation-duration: 0.1s; |
|
||||
animation-duration: 0.1s; |
|
||||
-webkit-animation-direction: normal; |
|
||||
animation-direction: normal; |
|
||||
-webkit-animation-timing-function: ease; |
|
||||
animation-timing-function: ease; |
|
||||
-webkit-animation-iteration-count: infinite; |
|
||||
animation-iteration-count: infinite; |
|
||||
} |
|
||||
.tokenfield .token.invalid { |
|
||||
background: none; |
|
||||
border: 1px solid transparent; |
|
||||
-webkit-border-radius: 0; |
|
||||
-moz-border-radius: 0; |
|
||||
border-radius: 0; |
|
||||
border-bottom: 1px dotted #d9534f; |
|
||||
} |
|
||||
.tokenfield .token.invalid.active { |
|
||||
background: #ededed; |
|
||||
border: 1px solid #ededed; |
|
||||
-webkit-border-radius: 3px; |
|
||||
-moz-border-radius: 3px; |
|
||||
border-radius: 3px; |
|
||||
} |
|
||||
.tokenfield .token .token-label { |
|
||||
display: inline-block; |
|
||||
overflow: hidden; |
|
||||
text-overflow: ellipsis; |
|
||||
padding-left: 4px; |
|
||||
vertical-align: top; |
|
||||
} |
|
||||
.tokenfield .token .close { |
|
||||
font-family: Arial; |
|
||||
display: inline-block; |
|
||||
line-height: 100%; |
|
||||
font-size: 1.1em; |
|
||||
line-height: 1.49em; |
|
||||
margin-left: 5px; |
|
||||
float: none; |
|
||||
height: 100%; |
|
||||
vertical-align: top; |
|
||||
padding-right: 4px; |
|
||||
} |
|
||||
.tokenfield .token-input { |
|
||||
background: none; |
|
||||
width: 60px; |
|
||||
min-width: 60px; |
|
||||
border: 0; |
|
||||
height: 20px; |
|
||||
padding: 0; |
|
||||
margin-bottom: 6px; |
|
||||
-webkit-box-shadow: none; |
|
||||
box-shadow: none; |
|
||||
} |
|
||||
.tokenfield .token-input:focus { |
|
||||
border-color: transparent; |
|
||||
outline: 0; |
|
||||
/* IE6-9 */ |
|
||||
-webkit-box-shadow: none; |
|
||||
box-shadow: none; |
|
||||
} |
|
||||
.tokenfield.disabled { |
|
||||
cursor: not-allowed; |
|
||||
background-color: #eeeeee; |
|
||||
} |
|
||||
.tokenfield.disabled .token-input { |
|
||||
cursor: not-allowed; |
|
||||
} |
|
||||
.tokenfield.disabled .token:hover { |
|
||||
cursor: not-allowed; |
|
||||
border-color: #d9d9d9; |
|
||||
} |
|
||||
.tokenfield.disabled .token:hover .close { |
|
||||
cursor: not-allowed; |
|
||||
opacity: 0.2; |
|
||||
filter: alpha(opacity=20); |
|
||||
} |
|
||||
.has-warning .tokenfield.focus { |
|
||||
border-color: #66512c; |
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; |
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; |
|
||||
} |
|
||||
.has-error .tokenfield.focus { |
|
||||
border-color: #843534; |
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; |
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; |
|
||||
} |
|
||||
.has-success .tokenfield.focus { |
|
||||
border-color: #2b542c; |
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; |
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; |
|
||||
} |
|
||||
.tokenfield.input-sm, |
|
||||
.input-group-sm .tokenfield { |
|
||||
min-height: 30px; |
|
||||
padding-bottom: 0px; |
|
||||
} |
|
||||
.input-group-sm .token, |
|
||||
.tokenfield.input-sm .token { |
|
||||
height: 20px; |
|
||||
margin-bottom: 4px; |
|
||||
} |
|
||||
.input-group-sm .token-input, |
|
||||
.tokenfield.input-sm .token-input { |
|
||||
height: 18px; |
|
||||
margin-bottom: 5px; |
|
||||
} |
|
||||
.tokenfield.input-lg, |
|
||||
.input-group-lg .tokenfield { |
|
||||
height: auto; |
|
||||
min-height: 45px; |
|
||||
padding-bottom: 4px; |
|
||||
} |
|
||||
.input-group-lg .token, |
|
||||
.tokenfield.input-lg .token { |
|
||||
height: 25px; |
|
||||
} |
|
||||
.input-group-lg .token-label, |
|
||||
.tokenfield.input-lg .token-label { |
|
||||
line-height: 23px; |
|
||||
} |
|
||||
.input-group-lg .token .close, |
|
||||
.tokenfield.input-lg .token .close { |
|
||||
line-height: 1.3em; |
|
||||
} |
|
||||
.input-group-lg .token-input, |
|
||||
.tokenfield.input-lg .token-input { |
|
||||
height: 23px; |
|
||||
line-height: 23px; |
|
||||
margin-bottom: 6px; |
|
||||
vertical-align: top; |
|
||||
} |
|
||||
.tokenfield.rtl { |
|
||||
direction: rtl; |
|
||||
text-align: right; |
|
||||
} |
|
||||
.tokenfield.rtl .token { |
|
||||
margin: -1px 0 5px 5px; |
|
||||
} |
|
||||
.tokenfield.rtl .token .token-label { |
|
||||
padding-left: 0px; |
|
||||
padding-right: 4px; |
|
||||
} |
|
||||
@ -1,93 +0,0 @@ |
|||||
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; |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
#### Sliptree |
|
||||
- by Illimar Tambek for [Sliptree](http://sliptree.com) |
|
||||
- Copyright (c) 2013 by Sliptree |
|
||||
|
|
||||
Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License) |
|
||||
|
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||
of this software and associated documentation files (the "Software"), to deal |
|
||||
in the Software without restriction, including without limitation the rights |
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||
copies of the Software, and to permit persons to whom the Software is |
|
||||
furnished to do so, subject to the following conditions: |
|
||||
|
|
||||
The above copyright notice and this permission notice shall be included in |
|
||||
all copies or substantial portions of the Software. |
|
||||
|
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||
THE SOFTWARE. |
|
||||
File diff suppressed because it is too large
@ -1,19 +0,0 @@ |
|||||
Copyright (c) 2013-2014 Twitter, Inc |
|
||||
|
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||
of this software and associated documentation files (the "Software"), to deal |
|
||||
in the Software without restriction, including without limitation the rights |
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||
copies of the Software, and to permit persons to whom the Software is |
|
||||
furnished to do so, subject to the following conditions: |
|
||||
|
|
||||
The above copyright notice and this permission notice shall be included in |
|
||||
all copies or substantial portions of the Software. |
|
||||
|
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||
THE SOFTWARE. |
|
||||
File diff suppressed because it is too large
@ -0,0 +1,169 @@ |
|||||
|
# -*- mode: python; coding: utf-8 -*- |
||||
|
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il |
||||
|
# se veut agnostique au réseau considéré, de manière à être installable en |
||||
|
# quelques clics. |
||||
|
# |
||||
|
# Copyright © 2017-2020 Gabriel Détraz |
||||
|
# Copyright © 2017-2020 Jean-Romain Garnier |
||||
|
# |
||||
|
# 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. |
||||
|
|
||||
|
# App de gestion des users pour re2o |
||||
|
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin |
||||
|
# Gplv2 |
||||
|
""" |
||||
|
Django views autocomplete view |
||||
|
|
||||
|
Here are defined the autocomplete class based view. |
||||
|
|
||||
|
""" |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from django.db.models import Q, Value, CharField |
||||
|
from django.db.models.functions import Concat |
||||
|
|
||||
|
from .models import Room, Dormitory, Building, Switch, PortProfile, Port, SwitchBay |
||||
|
|
||||
|
from re2o.views import AutocompleteViewMixin, AutocompleteLoggedOutViewMixin |
||||
|
|
||||
|
|
||||
|
class RoomAutocomplete(AutocompleteLoggedOutViewMixin): |
||||
|
obj_type = Room |
||||
|
|
||||
|
# Precision on search to add annotations so search behaves more like users expect it to |
||||
|
def filter_results(self): |
||||
|
# Suppose we have a dorm named Dorm, a building named B, and rooms from 001 - 999 |
||||
|
# Comments explain what we try to match |
||||
|
self.query_set = self.query_set.annotate( |
||||
|
full_name=Concat( |
||||
|
"building__name", Value(" "), "name" |
||||
|
), # Match when the user searches "B 001" |
||||
|
full_name_stuck=Concat("building__name", "name"), # Match "B001" |
||||
|
dorm_name=Concat( |
||||
|
"building__dormitory__name", Value(" "), "name" |
||||
|
), # Match "Dorm 001" |
||||
|
dorm_full_name=Concat( |
||||
|
"building__dormitory__name", |
||||
|
Value(" "), |
||||
|
"building__name", |
||||
|
Value(" "), |
||||
|
"name", |
||||
|
), # Match "Dorm B 001" |
||||
|
dorm_full_colon_name=Concat( |
||||
|
"building__dormitory__name", |
||||
|
Value(" : "), |
||||
|
"building__name", |
||||
|
Value(" "), |
||||
|
"name", |
||||
|
), # Match "Dorm : B 001" (see Room's full_name property) |
||||
|
).all() |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(full_name__icontains=self.q) |
||||
|
| Q(full_name_stuck__icontains=self.q) |
||||
|
| Q(dorm_name__icontains=self.q) |
||||
|
| Q(dorm_full_name__icontains=self.q) |
||||
|
| Q(dorm_full_colon_name__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class DormitoryAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Dormitory |
||||
|
|
||||
|
|
||||
|
class BuildingAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Building |
||||
|
|
||||
|
def filter_results(self): |
||||
|
# We want to be able to filter by dorm so it's easier |
||||
|
self.query_set = self.query_set.annotate( |
||||
|
full_name=Concat("dormitory__name", Value(" "), "name"), |
||||
|
full_name_colon=Concat("dormitory__name", Value(" : "), "name"), |
||||
|
).all() |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(full_name__icontains=self.q) | Q(full_name_colon__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class SwitchAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Switch |
||||
|
|
||||
|
|
||||
|
class PortAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Port |
||||
|
|
||||
|
def filter_results(self): |
||||
|
# We want to enter the switch name, not just the port number |
||||
|
# Because we're concatenating a CharField and an Integer, we have to specify the output_field |
||||
|
self.query_set = self.query_set.annotate( |
||||
|
full_name=Concat( |
||||
|
"switch__name", Value(" "), "port", output_field=CharField() |
||||
|
), |
||||
|
full_name_stuck=Concat("switch__name", "port", output_field=CharField()), |
||||
|
full_name_dash=Concat( |
||||
|
"switch__name", Value(" - "), "port", output_field=CharField() |
||||
|
), |
||||
|
).all() |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(full_name__icontains=self.q) |
||||
|
| Q(full_name_stuck__icontains=self.q) |
||||
|
| Q(full_name_dash__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class SwitchBayAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = SwitchBay |
||||
|
|
||||
|
def filter_results(self): |
||||
|
# See RoomAutocomplete.filter_results |
||||
|
self.query_set = self.query_set.annotate( |
||||
|
full_name=Concat( |
||||
|
"building__name", Value(" "), "name" |
||||
|
), |
||||
|
dorm_name=Concat( |
||||
|
"building__dormitory__name", Value(" "), "name" |
||||
|
), |
||||
|
dorm_full_name=Concat( |
||||
|
"building__dormitory__name", |
||||
|
Value(" "), |
||||
|
"building__name", |
||||
|
Value(" "), |
||||
|
"name", |
||||
|
), |
||||
|
dorm_full_colon_name=Concat( |
||||
|
"building__dormitory__name", |
||||
|
Value(" : "), |
||||
|
"building__name", |
||||
|
Value(" "), |
||||
|
"name", |
||||
|
), |
||||
|
).all() |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(full_name__icontains=self.q) |
||||
|
| Q(dorm_name__icontains=self.q) |
||||
|
| Q(dorm_full_name__icontains=self.q) |
||||
|
| Q(dorm_full_colon_name__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class PortProfileAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = PortProfile |
||||
@ -0,0 +1,98 @@ |
|||||
|
# -*- mode: python; coding: utf-8 -*- |
||||
|
# Re2o est un logiciel d'administration développé initiallement au Rézo Metz. Il |
||||
|
# se veut agnostique au réseau considéré, de manière à être installable en |
||||
|
# quelques clics. |
||||
|
# |
||||
|
# Copyright © 2017-2020 Gabriel Détraz |
||||
|
# Copyright © 2017-2020 Jean-Romain Garnier |
||||
|
# |
||||
|
# 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. |
||||
|
|
||||
|
# App de gestion des users pour re2o |
||||
|
# Lara Kermarec, Gabriel Détraz, Lemesle Augustin |
||||
|
# Gplv2 |
||||
|
""" |
||||
|
Django views autocomplete view |
||||
|
|
||||
|
Here are defined the autocomplete class based view. |
||||
|
|
||||
|
""" |
||||
|
from __future__ import unicode_literals |
||||
|
|
||||
|
from .models import User, School, Adherent, Club, ListShell |
||||
|
|
||||
|
from re2o.views import AutocompleteViewMixin, AutocompleteLoggedOutViewMixin |
||||
|
|
||||
|
from django.db.models import Q, Value, CharField |
||||
|
from django.db.models.functions import Concat |
||||
|
|
||||
|
|
||||
|
class SchoolAutocomplete(AutocompleteLoggedOutViewMixin): |
||||
|
obj_type = School |
||||
|
|
||||
|
|
||||
|
class UserAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = User |
||||
|
|
||||
|
# Precision on search to add annotations so search behaves more like users expect it to |
||||
|
def filter_results(self): |
||||
|
# Comments explain what we try to match |
||||
|
self.query_set = self.query_set.annotate( |
||||
|
full_name=Concat( |
||||
|
"adherent__name", Value(" "), "surname" |
||||
|
), # Match when the user searches "Toto Passoir" |
||||
|
full_name_reverse=Concat( |
||||
|
"surname", Value(" "), "adherent__name" |
||||
|
), # Match when the user searches "Passoir Toto" |
||||
|
).all() |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(pseudo__icontains=self.q) |
||||
|
| Q(full_name__icontains=self.q) |
||||
|
| Q(full_name_reverse__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class AdherentAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Adherent |
||||
|
|
||||
|
# Precision on search to add annotations so search behaves more like users expect it to |
||||
|
def filter_results(self): |
||||
|
# Comments explain what we try to match |
||||
|
self.query_set = self.query_set.annotate( |
||||
|
full_name=Concat( |
||||
|
"name", Value(" "), "surname" |
||||
|
), # Match when the user searches "Toto Passoir" |
||||
|
full_name_reverse=Concat( |
||||
|
"surname", Value(" "), "name" |
||||
|
), # Match when the user searches "Passoir Toto" |
||||
|
).all() |
||||
|
|
||||
|
if self.q: |
||||
|
self.query_set = self.query_set.filter( |
||||
|
Q(pseudo__icontains=self.q) |
||||
|
| Q(full_name__icontains=self.q) |
||||
|
| Q(full_name_reverse__icontains=self.q) |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class ClubAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = Club |
||||
|
|
||||
|
|
||||
|
class ShellAutocomplete(AutocompleteViewMixin): |
||||
|
obj_type = ListShell |
||||
|
query_filter = "shell__icontains" |
||||
Loading…
Reference in new issue