From 3f0901bbb31dd585c7b1263c47bbc4b089155686 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Sun, 27 Dec 2020 15:47:41 +0100 Subject: [PATCH 01/55] Fix advanced search filters for empty query --- search/engine.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/search/engine.py b/search/engine.py index dc4eede8..ae772514 100644 --- a/search/engine.py +++ b/search/engine.py @@ -564,4 +564,9 @@ def create_queries(query): queries.append(current_query) + # Make sure there is at least one query, even if it's empty + # Otherwise, display filters (for advanced search) won't work + # when the search text field is empty + queries = queries or [Query()] + return queries From 10d6e3535265fbf35054721253e241da0567ed65 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Sun, 27 Dec 2020 15:50:16 +0100 Subject: [PATCH 02/55] Improve code formatting of search/engine.py --- search/engine.py | 110 ++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/search/engine.py b/search/engine.py index ae772514..a724cd8c 100644 --- a/search/engine.py +++ b/search/engine.py @@ -42,6 +42,20 @@ from preferences.models import GeneralOption from re2o.base import SortTable, re2o_paginator +# List of fields the search applies to +FILTER_FIELDS = [ + "users", + "clubs", + "machines", + "factures", + "bans", + "whitelists", + "rooms", + "ports", + "switches", +] + + class Query: """Class representing a query. It can contain the user-entered text, the operator for the query, @@ -53,6 +67,7 @@ class Query: subqueries: list of Query objects when the current query is split in several parts. """ + def __init__(self, text="", case_sensitive=False): """Initialise an instance of Query. @@ -98,27 +113,14 @@ class Query: return self.operator.join([q.plaintext for q in self.subqueries]) if self.case_sensitive: - return "\"{}\"".format(self.text) + return '"{}"'.format(self.text) return self.text -def filter_fields(): - """Return the list of fields the search applies to.""" - return ["users", - "clubs", - "machines", - "factures", - "bans", - "whitelists", - "rooms", - "ports", - "switches"] - - def empty_filters(): """Build empty filters used by Django.""" - return {f: Q() for f in filter_fields()} + return {f: Q() for f in FILTER_FIELDS} def is_int(variable): @@ -176,10 +178,9 @@ def finish_results(request, results, col, order): max_result = GeneralOption.get_cached_value("search_display_page") for name, val in results.items(): page_arg = name + "_page" - results[name] = re2o_paginator(request, - val.distinct(), - max_result, - page_arg=page_arg) + results[name] = re2o_paginator( + request, val.distinct(), max_result, page_arg=page_arg + ) results.update({"max_result": max_result}) @@ -206,9 +207,9 @@ def contains_filter(attribute, word, case_sensitive=False): return Q(**{attr: word}) -def search_single_word(word, filters, user, start, end, - user_state, email_state, aff, - case_sensitive=False): +def search_single_word( + word, filters, user, start, end, user_state, email_state, aff, case_sensitive=False +): """Construct the correct filters to match differents fields of some models with the given query according to the given filters. The match fields are either CharField or IntegerField that will be displayed @@ -230,10 +231,7 @@ def search_single_word(word, filters, user, start, end, ) # Users have a name whereas clubs only have a surname - filter_users = ( - filter_clubs - | contains_filter("name", word, case_sensitive) - ) + filter_users = filter_clubs | contains_filter("name", word, case_sensitive) if not User.can_view_all(user)[0]: filter_clubs &= Q(id=user.id) @@ -252,12 +250,15 @@ def search_single_word(word, filters, user, start, end, if "1" in aff: filter_machines = ( contains_filter("name", word, case_sensitive) - | (contains_filter("user__pseudo", word, case_sensitive) - & Q(user__state__in=user_state) - & Q(user__email_state__in=email_state)) + | ( + contains_filter("user__pseudo", word, case_sensitive) + & Q(user__state__in=user_state) + & Q(user__email_state__in=email_state) + ) | contains_filter("interface__domain__name", word, case_sensitive) - | contains_filter("interface__domain__related_domain__name", - word, case_sensitive) + | contains_filter( + "interface__domain__related_domain__name", word, case_sensitive + ) | contains_filter("interface__mac_address", word, case_sensitive) | contains_filter("interface__ipv4__ipv4", word, case_sensitive) ) @@ -339,13 +340,12 @@ def search_single_word(word, filters, user, start, end, # Switch ports if "6" in aff and User.can_view_all(user): filter_ports = ( - contains_filter("machine_interface__domain__name", - word, case_sensitive) - | contains_filter("related__switch__interface__domain__name", - word, case_sensitive) + contains_filter("machine_interface__domain__name", word, case_sensitive) + | contains_filter( + "related__switch__interface__domain__name", word, case_sensitive + ) | contains_filter("custom_profile__name", word, case_sensitive) - | contains_filter("custom_profile__profil_default", - word, case_sensitive) + | contains_filter("custom_profile__profil_default", word, case_sensitive) | contains_filter("details", word, case_sensitive) # Added through annotate | contains_filter("room_full_name", word, case_sensitive) @@ -360,8 +360,7 @@ def search_single_word(word, filters, user, start, end, filter_switches = ( contains_filter("interface__domain__name", word, case_sensitive) | contains_filter("interface__ipv4__ipv4", word, case_sensitive) - | contains_filter("switchbay__building__name", - word, case_sensitive) + | contains_filter("switchbay__building__name", word, case_sensitive) | contains_filter("stack__name", word, case_sensitive) | contains_filter("model__reference", word, case_sensitive) | contains_filter("model__constructor__name", word, case_sensitive) @@ -399,13 +398,11 @@ def apply_filters(filters, user, aff): # Users and clubs if "0" in aff: results["users"] = Adherent.objects.annotate( - room_full_name=Concat("room__building__name", - Value(" "), "room__name"), + room_full_name=Concat("room__building__name", Value(" "), "room__name"), room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["users"]) results["clubs"] = Club.objects.annotate( - room_full_name=Concat("room__building__name", - Value(" "), "room__name"), + room_full_name=Concat("room__building__name", Value(" "), "room__name"), room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["clubs"]) @@ -435,8 +432,7 @@ def apply_filters(filters, user, aff): # Switch ports if "6" in aff and User.can_view_all(user): results["ports"] = Port.objects.annotate( - room_full_name=Concat("room__building__name", - Value(" "), "room__name"), + room_full_name=Concat("room__building__name", Value(" "), "room__name"), room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["ports"]) @@ -455,24 +451,32 @@ def search_single_query(query, filters, user, start, end, user_state, email_stat newfilters = empty_filters() for q in query.subqueries: # Construct an independent filter for each subquery - subfilters = search_single_query(q, empty_filters(), user, - start, end, user_state, - email_state, aff) + subfilters = search_single_query( + q, empty_filters(), user, start, end, user_state, email_state, aff + ) # Apply the subfilter - for field in filter_fields(): + for field in FILTER_FIELDS: newfilters[field] &= subfilters[field] # Add these filters to the existing ones - for field in filter_fields(): + for field in FILTER_FIELDS: filters[field] |= newfilters[field] return filters # Handle standard queries - return search_single_word(query.text, filters, user, start, end, - user_state, email_state, aff, - query.case_sensitive) + return search_single_word( + query.text, + filters, + user, + start, + end, + user_state, + email_state, + aff, + query.case_sensitive, + ) def create_queries(query): From 7f36b642acd96e80daca03e1e8023713ec71447c Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Mon, 28 Dec 2020 16:37:43 +0100 Subject: [PATCH 03/55] Remove useless MinValueValidator(0) on PositiveIntegerField. --- .../migrations/0051_auto_20201228_1636.py | 45 +++++++++++++++++++ cotisations/models.py | 6 --- 2 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 cotisations/migrations/0051_auto_20201228_1636.py diff --git a/cotisations/migrations/0051_auto_20201228_1636.py b/cotisations/migrations/0051_auto_20201228_1636.py new file mode 100644 index 00000000..031ccf63 --- /dev/null +++ b/cotisations/migrations/0051_auto_20201228_1636.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-12-28 15:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0050_auto_20201102_2342'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='duration_connection', + field=models.PositiveIntegerField(verbose_name='duration of the connection (in months)'), + ), + migrations.AlterField( + model_name='article', + name='duration_days_connection', + field=models.PositiveIntegerField(verbose_name='duration of the connection (in days, will be added to duration in months)'), + ), + migrations.AlterField( + model_name='article', + name='duration_days_membership', + field=models.PositiveIntegerField(verbose_name='duration of the membership (in days, will be added to duration in months)'), + ), + migrations.AlterField( + model_name='article', + name='duration_membership', + field=models.PositiveIntegerField(verbose_name='duration of the membership (in months)'), + ), + migrations.AlterField( + model_name='vente', + name='duration_days_connection', + field=models.PositiveIntegerField(default=0, verbose_name='duration of the connection (in days, will be added to duration in months)'), + ), + migrations.AlterField( + model_name='vente', + name='duration_days_membership', + field=models.PositiveIntegerField(default=0, verbose_name='duration of the membership (in days, will be added to duration in months)'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index b3480561..328a2657 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -467,7 +467,6 @@ class Vente(RevMixin, AclMixin, models.Model): ) duration_days_connection = models.PositiveIntegerField( default=0, - validators=[MinValueValidator(0)], verbose_name=_("duration of the connection (in days, will be added to duration in months)"), ) duration_membership = models.PositiveIntegerField( @@ -475,7 +474,6 @@ class Vente(RevMixin, AclMixin, models.Model): ) duration_days_membership = models.PositiveIntegerField( default=0, - validators=[MinValueValidator(0)], verbose_name=_("duration of the membership (in days, will be added to duration in months)"), ) @@ -704,19 +702,15 @@ class Article(RevMixin, AclMixin, models.Model): ) duration_membership = models.PositiveIntegerField( - validators=[MinValueValidator(0)], verbose_name=_("duration of the membership (in months)") ) duration_days_membership = models.PositiveIntegerField( - validators=[MinValueValidator(0)], verbose_name=_("duration of the membership (in days, will be added to duration in months)"), ) duration_connection = models.PositiveIntegerField( - validators=[MinValueValidator(0)], verbose_name=_("duration of the connection (in months)") ) duration_days_connection = models.PositiveIntegerField( - validators=[MinValueValidator(0)], verbose_name=_("duration of the connection (in days, will be added to duration in months)"), ) From 4d82eda5b1e82e2d7d1e6f69dd99d79f893663b0 Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Mon, 28 Dec 2020 16:38:44 +0100 Subject: [PATCH 04/55] Code formatting. --- .../migrations/0051_auto_20201228_1636.py | 52 ++++++++++------ cotisations/models.py | 59 ++++++++++++------- 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/cotisations/migrations/0051_auto_20201228_1636.py b/cotisations/migrations/0051_auto_20201228_1636.py index 031ccf63..572c9634 100644 --- a/cotisations/migrations/0051_auto_20201228_1636.py +++ b/cotisations/migrations/0051_auto_20201228_1636.py @@ -8,38 +8,52 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('cotisations', '0050_auto_20201102_2342'), + ("cotisations", "0050_auto_20201102_2342"), ] operations = [ migrations.AlterField( - model_name='article', - name='duration_connection', - field=models.PositiveIntegerField(verbose_name='duration of the connection (in months)'), + model_name="article", + name="duration_connection", + field=models.PositiveIntegerField( + verbose_name="duration of the connection (in months)" + ), ), migrations.AlterField( - model_name='article', - name='duration_days_connection', - field=models.PositiveIntegerField(verbose_name='duration of the connection (in days, will be added to duration in months)'), + model_name="article", + name="duration_days_connection", + field=models.PositiveIntegerField( + verbose_name="duration of the connection (in days, will be added to duration in months)" + ), ), migrations.AlterField( - model_name='article', - name='duration_days_membership', - field=models.PositiveIntegerField(verbose_name='duration of the membership (in days, will be added to duration in months)'), + model_name="article", + name="duration_days_membership", + field=models.PositiveIntegerField( + verbose_name="duration of the membership (in days, will be added to duration in months)" + ), ), migrations.AlterField( - model_name='article', - name='duration_membership', - field=models.PositiveIntegerField(verbose_name='duration of the membership (in months)'), + model_name="article", + name="duration_membership", + field=models.PositiveIntegerField( + verbose_name="duration of the membership (in months)" + ), ), migrations.AlterField( - model_name='vente', - name='duration_days_connection', - field=models.PositiveIntegerField(default=0, verbose_name='duration of the connection (in days, will be added to duration in months)'), + model_name="vente", + name="duration_days_connection", + field=models.PositiveIntegerField( + default=0, + verbose_name="duration of the connection (in days, will be added to duration in months)", + ), ), migrations.AlterField( - model_name='vente', - name='duration_days_membership', - field=models.PositiveIntegerField(default=0, verbose_name='duration of the membership (in days, will be added to duration in months)'), + model_name="vente", + name="duration_days_membership", + field=models.PositiveIntegerField( + default=0, + verbose_name="duration of the membership (in days, will be added to duration in months)", + ), ), ] diff --git a/cotisations/models.py b/cotisations/models.py index 328a2657..5dc18e4f 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -98,7 +98,7 @@ class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): def name_detailed(self): """ - Return: + Return: - a list of strings with the name of all article in the invoice and their quantity. """ @@ -248,8 +248,8 @@ class Facture(BaseInvoice): @staticmethod def can_change_control(user_request, *_args, **_kwargs): - """ Returns True if the user can change the 'controlled' status of - this invoice """ + """Returns True if the user can change the 'controlled' status of + this invoice""" can = user_request.has_perm("cotisations.change_facture_control") return ( can, @@ -293,8 +293,7 @@ class Facture(BaseInvoice): """Returns every subscription associated with this invoice.""" return Cotisation.objects.filter( vente__in=self.vente_set.filter( - ~(Q(duration_membership=0)) |\ - ~(Q(duration_days_membership=0)) + ~(Q(duration_membership=0)) | ~(Q(duration_days_membership=0)) ) ) @@ -454,7 +453,7 @@ class Vente(RevMixin, AclMixin, models.Model): number = models.IntegerField( validators=[MinValueValidator(1)], verbose_name=_("amount") ) - # TODO : change this field for a ForeinKey to Article + # TODO : change this field for a ForeinKey to Article # Note: With a foreign key, modifing an Article modifis the Purchase, wich is bad. # To use a foreign key, you need to make Article read only name = models.CharField(max_length=255, verbose_name=_("article")) @@ -467,14 +466,18 @@ class Vente(RevMixin, AclMixin, models.Model): ) duration_days_connection = models.PositiveIntegerField( default=0, - verbose_name=_("duration of the connection (in days, will be added to duration in months)"), + verbose_name=_( + "duration of the connection (in days, will be added to duration in months)" + ), ) duration_membership = models.PositiveIntegerField( default=0, verbose_name=_("duration of the membership (in months)") ) duration_days_membership = models.PositiveIntegerField( default=0, - verbose_name=_("duration of the membership (in days, will be added to duration in months)"), + verbose_name=_( + "duration of the membership (in days, will be added to duration in months)" + ), ) class Meta: @@ -511,7 +514,7 @@ class Vente(RevMixin, AclMixin, models.Model): def create_cotis(self, date_start_con=False, date_start_memb=False): """ - Creates a cotisation without initializing the dates (start and end ar set to self.facture.facture.date) + Creates a cotisation without initializing the dates (start and end ar set to self.facture.facture.date) and without saving it. You should use Facture.reorder_purchases to set the right dates. """ try: @@ -629,12 +632,14 @@ class Vente(RevMixin, AclMixin, models.Model): return str(self.name) + " " + str(self.facture) def test_membership_or_connection(self): - """ Test if the purchase include membership or connecton - """ - return self.duration_membership or \ - self.duration_days_membership or \ - self.duration_connection or \ - self.duration_days_connection + """Test if the purchase include membership or connecton""" + return ( + self.duration_membership + or self.duration_days_membership + or self.duration_connection + or self.duration_days_connection + ) + # TODO : change vente to purchase @receiver(post_save, sender=Vente) @@ -705,13 +710,17 @@ class Article(RevMixin, AclMixin, models.Model): verbose_name=_("duration of the membership (in months)") ) duration_days_membership = models.PositiveIntegerField( - verbose_name=_("duration of the membership (in days, will be added to duration in months)"), + verbose_name=_( + "duration of the membership (in days, will be added to duration in months)" + ), ) duration_connection = models.PositiveIntegerField( verbose_name=_("duration of the connection (in months)") ) duration_days_connection = models.PositiveIntegerField( - verbose_name=_("duration of the connection (in days, will be added to duration in months)"), + verbose_name=_( + "duration of the connection (in days, will be added to duration in months)" + ), ) need_membership = models.BooleanField( @@ -787,8 +796,8 @@ class Article(RevMixin, AclMixin, models.Model): if target_user is not None and not target_user.is_adherent(): objects_pool = objects_pool.filter( Q(duration_membership__gt=0) - |Q(duration_days_membership__gt=0) - |Q(need_membership=False) + | Q(duration_days_membership__gt=0) + | Q(need_membership=False) ) if user.has_perm("cotisations.buy_every_article"): return objects_pool @@ -878,7 +887,9 @@ class Paiement(RevMixin, AclMixin, models.Model): # In case a cotisation was bought, inform the user, the # cotisation time has been extended too - if any(sell.test_membership_or_connection() for sell in invoice.vente_set.all()): + if any( + sell.test_membership_or_connection() for sell in invoice.vente_set.all() + ): messages.success( request, _( @@ -950,9 +961,13 @@ class Cotisation(RevMixin, AclMixin, models.Model): vente = models.OneToOneField( "Vente", on_delete=models.CASCADE, null=True, verbose_name=_("purchase") ) - date_start_con = models.DateTimeField(verbose_name=_("start date for the connection")) + date_start_con = models.DateTimeField( + verbose_name=_("start date for the connection") + ) date_end_con = models.DateTimeField(verbose_name=_("end date for the connection")) - date_start_memb = models.DateTimeField(verbose_name=_("start date for the membership")) + date_start_memb = models.DateTimeField( + verbose_name=_("start date for the membership") + ) date_end_memb = models.DateTimeField(verbose_name=_("end date for the membership")) class Meta: From 11c4f4ef212d31a8a99033d7642f243aa65ff480 Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Mon, 28 Dec 2020 20:50:00 +0100 Subject: [PATCH 05/55] Fix club edit and add some documentation on that error. --- re2o/acl.py | 10 ++++++++-- users/models.py | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index 600b839f..e73ffdfa 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -64,11 +64,17 @@ def acl_base_decorator(method_name, *targets, on_instance=True): """Base decorator for acl. It checks if the `request.user` has the permission by calling model.method_name. If the flag on_instance is True, tries to get an instance of the model by calling - `model.get_instance(*args, **kwargs)` and runs `instance.mehod_name` + `model.get_instance(obj_id, *args, **kwargs)` and runs `instance.mehod_name` rather than model.method_name. It is not intended to be used as is. It is a base for others ACL - decorators. + decorators. Beware, if you redefine the `get_instance` method for your + model, give it a signature such as + `def get_instance(cls, object_id, *_args, **_kwargs)`, because you will + likely have an url with a named parameter "userid" if *e.g.* your model + is an user. Otherwise, if the parameter name in `get_instance` was also + `userid`, then `get_instance` would end up having two identical parameter + passed on, and this would result in a `TypeError` exception. Args: method_name: The name of the method which is to to be used for ACL. diff --git a/users/models.py b/users/models.py index 67560472..14215fdf 100755 --- a/users/models.py +++ b/users/models.py @@ -2027,10 +2027,10 @@ class Adherent(User): self.gpg_fingerprint = gpg_fingerprint @classmethod - def get_instance(cls, adherentid, *_args, **_kwargs): + def get_instance(cls, object_id, *_args, **_kwargs): """Try to find an instance of `Adherent` with the given id. - :param adherentid: The id of the adherent we are looking for. + :param object_id: The id of the adherent we are looking for. :return: An adherent. """ @@ -2154,13 +2154,13 @@ class Club(User): ) @classmethod - def get_instance(cls, clubid, *_args, **_kwargs): + def get_instance(cls, object_id, *_args, **_kwargs): """Try to find an instance of `Club` with the given id. - :param clubid: The id of the adherent we are looking for. + :param object_id: The id of the adherent we are looking for. :return: A club. """ - return cls.objects.get(pk=clubid) + return cls.objects.get(pk=object_id) @receiver(post_save, sender=Adherent) From a96a2f50fae790b27c176f6a1b4987db24d27828 Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 29 Nov 2020 17:17:42 +0100 Subject: [PATCH 06/55] =?UTF-8?q?modification=20des=20acl=20pour=20g=C3=A9?= =?UTF-8?q?rer=20les=20vues=20de=20l'api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- re2o/acl.py | 82 ++++++++++++++++++++++++++++++++++++++------------ re2o/mixins.py | 24 ++++++++++++++- 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/re2o/acl.py b/re2o/acl.py index e73ffdfa..0a5dd1ee 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -35,6 +35,7 @@ from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import ugettext as _ +from rest_framework.response import Response from re2o.utils import get_group_having_permission @@ -43,11 +44,13 @@ def acl_error_message(msg, permissions): """Create an error message for msg and permissions.""" if permissions is None: return msg - groups = ", ".join([g.name for g in get_group_having_permission(*permissions)]) + groups = ", ".join( + [g.name for g in get_group_having_permission(*permissions)]) message = msg or _("You don't have the right to edit this option.") if groups: return ( - message + _("You need to be a member of one of these groups: %s.") % groups + message + + _("You need to be a member of one of these groups: %s.") % groups ) else: return message + _("No group has the %s permission(s)!") % " or ".join( @@ -60,7 +63,7 @@ def acl_error_message(msg, permissions): # This is the function of main interest of this file. Almost all the decorators # use it, and it is a fairly complicated piece of code. Let me guide you through # this ! 🌈😸 -def acl_base_decorator(method_name, *targets, on_instance=True): +def acl_base_decorator(method_name, *targets, on_instance=True, api=False): """Base decorator for acl. It checks if the `request.user` has the permission by calling model.method_name. If the flag on_instance is True, tries to get an instance of the model by calling @@ -121,6 +124,9 @@ on_instance=False) method `get_instance` of the model, with the arguments originally passed to the view. + api: when set to True, errors will no longer trigger redirection and + messages but will send a 403 with errors in JSON + Returns: The user is either redirected to their own page with an explanation message if at least one access is not granted, or to the view. In order @@ -192,7 +198,8 @@ ModelC) # and store it to pass it to the view. if on_instance: try: - target = target.get_instance(target_id, *args, **kwargs) + target = target.get_instance( + target_id, *args, **kwargs) instances.append(target) except target.DoesNotExist: # A non existing instance is a valid reason to deny @@ -238,28 +245,37 @@ ModelC) # Store the messages at the right place. for can, msg, permissions in process_target(target, fields, target_id): if not can: - error_messages.append(acl_error_message(msg, permissions)) + error_messages.append( + acl_error_message(msg, permissions)) elif msg: - warning_messages.append(acl_error_message(msg, permissions)) + warning_messages.append( + acl_error_message(msg, permissions)) # Display the warning messages - if warning_messages: - for msg in warning_messages: - messages.warning(request, msg) + if not api: + if warning_messages: + for msg in warning_messages: + messages.warning(request, msg) # If there is any error message, then the request must be denied. if error_messages: # We display the message - for msg in error_messages: - messages.error( - request, - msg or _("You don't have the right to access this menu."), - ) + if not api: + for msg in error_messages: + messages.error( + request, + msg or _( + "You don't have the right to access this menu."), + ) # And redirect the user to the right place. if request.user.id is not None: - return redirect( - reverse("users:profil", kwargs={"userid": str(request.user.id)}) - ) + if not api: + return redirect( + reverse("users:profil", kwargs={ + "userid": str(request.user.id)}) + ) + else: + return Response(data={"errors": error_messages, "warning": warning_messages}, status=403) else: return redirect(reverse("index")) return view(request, *chain(instances, args), **kwargs) @@ -328,7 +344,8 @@ def can_delete_set(model): request, _("You don't have the right to access this menu.") ) return redirect( - reverse("users:profil", kwargs={"userid": str(request.user.id)}) + reverse("users:profil", kwargs={ + "userid": str(request.user.id)}) ) return view(request, instances, *args, **kwargs) @@ -375,9 +392,36 @@ def can_edit_history(view): """ if request.user.has_perm("admin.change_logentry"): return view(request, *args, **kwargs) - messages.error(request, _("You don't have the right to edit the history.")) + messages.error(request, _( + "You don't have the right to edit the history.")) return redirect( reverse("users:profil", kwargs={"userid": str(request.user.id)}) ) return wrapper + + +def can_view_all_api(*models): + """Decorator to check if an user can see an api page + Only used on functionnal api views (class-based api views ACL are checked + in api/permissions.py) + """ + return acl_base_decorator("can_view_all", *models, on_instance=False, api=True) + + +def can_edit_all_api(*models): + """Decorator to check if an user can edit via the api + We do not always know which instances will be edited, so we may need to know + if the user can edit any instance. + Only used on functionnal api views (class-based api views ACL are checked + in api/permissions.py) + """ + return acl_base_decorator("can_edit_all", *models, on_instance=False, api=True) + + +def can_create_api(*models): + """Decorator to check if an user can create the given models. via the api + Only used on functionnal api views (class-based api views ACL are checked + in api/permissions.py) + """ + return acl_base_decorator("can_create", *models, on_instance=False, api=True) diff --git a/re2o/mixins.py b/re2o/mixins.py index 44cd517e..fbe715f0 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -61,7 +61,8 @@ class FormRevMixin(object): ) elif self.changed_data: reversion.set_comment( - "Field(s) edited: %s" % ", ".join(field for field in self.changed_data) + "Field(s) edited: %s" % ", ".join( + field for field in self.changed_data) ) return super(FormRevMixin, self).save(*args, **kwargs) @@ -187,6 +188,27 @@ class AclMixin(object): (permission,), ) + @classmethod + def can_edit_all(cls, user_request, *_args, **_kwargs): + """Check if a user can edit all instances of an object + + Parameters: + user_request: User calling for this action + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ + permission = cls.get_modulename() + ".change_" + cls.get_classname() + can = user_request.has_perm(permission) + return ( + can, + _("You don't have the right to edit every %s object.") % cls.get_classname() + if not can + else None, + (permission,), + ) + def can_view(self, user_request, *_args, **_kwargs): """Check if a user can view an instance of an object From ba9d5211b9e2f04cbdfb4bd7154f71f1492626a3 Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 29 Nov 2020 17:21:28 +0100 Subject: [PATCH 07/55] patch --- preferences/views.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/preferences/views.py b/preferences/views.py index 077fd105..5b5f6b03 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -96,7 +96,8 @@ def edit_options_template_function(request, section, forms, models): return redirect(reverse("preferences:display-options")) options_instance, _created = model.objects.get_or_create() - _is_allowed_to_edit, msg, permissions = options_instance.can_edit(request.user) + _is_allowed_to_edit, msg, permissions = options_instance.can_edit( + request.user) if not _is_allowed_to_edit: messages.error(request, acl_error_message(msg, permissions)) return redirect(reverse("index")) @@ -150,7 +151,7 @@ def display_options(request): optionnal_templates_list = [ app.preferences.views.aff_preferences(request) for app in optionnal_apps - if hasattr(app.preferences.views, "aff_preferences") + if hasattr(app, "preferences") and hasattr(app.preferences.views, "aff_preferences") ] return form( @@ -301,7 +302,8 @@ def add_radiuskey(request): @can_edit(RadiusKey) def edit_radiuskey(request, radiuskey_instance, **_kwargs): """View used to edit RADIUS keys.""" - radiuskey = RadiusKeyForm(request.POST or None, instance=radiuskey_instance) + radiuskey = RadiusKeyForm(request.POST or None, + instance=radiuskey_instance) if radiuskey.is_valid(): radiuskey.save() messages.success(request, _("The RADIUS key was edited.")) @@ -344,10 +346,11 @@ def add_switchmanagementcred(request): switchmanagementcred = SwitchManagementCredForm(request.POST or None) if switchmanagementcred.is_valid(): switchmanagementcred.save() - messages.success(request, _("The switch management credentials were added.")) + messages.success(request, _( + "The switch management credentials were added.")) return redirect(reverse("preferences:display-options")) return form( - {"preferenceform": switchmanagementcred, "action_name": _("Add"),}, + {"preferenceform": switchmanagementcred, "action_name": _("Add"), }, "preferences/preferences.html", request, ) @@ -361,7 +364,8 @@ def edit_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs) ) if switchmanagementcred.is_valid(): switchmanagementcred.save() - messages.success(request, _("The switch management credentials were edited.")) + messages.success(request, _( + "The switch management credentials were edited.")) return redirect(reverse("preferences:display-options")) return form( {"preferenceform": switchmanagementcred, "action_name": _("Edit")}, @@ -410,7 +414,7 @@ def add_mailcontact(request): messages.success(request, _("The contact email address was created.")) return redirect(reverse("preferences:display-options")) return form( - {"preferenceform": mailcontact, "action_name": _("Add"),}, + {"preferenceform": mailcontact, "action_name": _("Add"), }, "preferences/preferences.html", request, ) @@ -438,12 +442,14 @@ def edit_mailcontact(request, mailcontact_instance, **_kwargs): @can_delete_set(MailContact) def del_mailcontact(request, instances): """View used to delete one or several contact email addresses.""" - mailcontacts = DelMailContactForm(request.POST or None, instances=instances) + mailcontacts = DelMailContactForm( + request.POST or None, instances=instances) if mailcontacts.is_valid(): mailcontacts_dels = mailcontacts.cleaned_data["mailcontacts"] for mailcontacts_del in mailcontacts_dels: mailcontacts_del.delete() - messages.success(request, _("The contact email adress was deleted.")) + messages.success(request, _( + "The contact email adress was deleted.")) return redirect(reverse("preferences:display-options")) return form( {"preferenceform": mailcontacts, "action_name": _("Delete")}, From 5a79ffb0f5dfb8759790902c297751a486594a2e Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 29 Nov 2020 18:19:46 +0100 Subject: [PATCH 08/55] lets be sure that api permissions wont trigger on functional views --- api/permissions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api/permissions.py b/api/permissions.py index 1983bdc8..3ee61f33 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -239,6 +239,9 @@ class AutodetectACLPermission(permissions.BasePermission): if getattr(view, "_ignore_model_permissions", False): return True + if not getattr(view, "queryset", getattr(view, "get_queryset", None)): + return True + if not request.user or not request.user.is_authenticated: return False @@ -273,7 +276,8 @@ class AutodetectACLPermission(permissions.BasePermission): # they have read permissions to see 403, or not, and simply see # a 404 response. - SAFE_METHODS = ("GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE") + SAFE_METHODS = ("GET", "OPTIONS", "HEAD", + "POST", "PUT", "PATCH", "DELETE") if request.method in SAFE_METHODS: # Read permissions already checked and failed, no need From 70b492c31574a7f454d73a46c28c2de84575e15d Mon Sep 17 00:00:00 2001 From: chapeau Date: Sun, 29 Nov 2020 22:17:52 +0100 Subject: [PATCH 09/55] documentation --- api/permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/permissions.py b/api/permissions.py index 3ee61f33..ab7453f4 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -239,6 +239,8 @@ class AutodetectACLPermission(permissions.BasePermission): if getattr(view, "_ignore_model_permissions", False): return True + # Bypass permission verifications if it is a functional view + # (permissions are handled by ACL) if not getattr(view, "queryset", getattr(view, "get_queryset", None)): return True From a1c08c4e968bb596af21306988ed9b9d6cc9d782 Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Mon, 28 Dec 2020 20:59:35 +0100 Subject: [PATCH 10/55] Just blacked everything. --- api/permissions.py | 3 +-- preferences/views.py | 31 +++++++++++++------------- re2o/acl.py | 52 ++++++++++++++++++++------------------------ re2o/mixins.py | 17 +++++++-------- 4 files changed, 48 insertions(+), 55 deletions(-) diff --git a/api/permissions.py b/api/permissions.py index ab7453f4..646682c4 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -278,8 +278,7 @@ class AutodetectACLPermission(permissions.BasePermission): # they have read permissions to see 403, or not, and simply see # a 404 response. - SAFE_METHODS = ("GET", "OPTIONS", "HEAD", - "POST", "PUT", "PATCH", "DELETE") + SAFE_METHODS = ("GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE") if request.method in SAFE_METHODS: # Read permissions already checked and failed, no need diff --git a/preferences/views.py b/preferences/views.py index 5b5f6b03..5eab39f4 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -96,8 +96,7 @@ def edit_options_template_function(request, section, forms, models): return redirect(reverse("preferences:display-options")) options_instance, _created = model.objects.get_or_create() - _is_allowed_to_edit, msg, permissions = options_instance.can_edit( - request.user) + _is_allowed_to_edit, msg, permissions = options_instance.can_edit(request.user) if not _is_allowed_to_edit: messages.error(request, acl_error_message(msg, permissions)) return redirect(reverse("index")) @@ -151,7 +150,8 @@ def display_options(request): optionnal_templates_list = [ app.preferences.views.aff_preferences(request) for app in optionnal_apps - if hasattr(app, "preferences") and hasattr(app.preferences.views, "aff_preferences") + if hasattr(app, "preferences") + and hasattr(app.preferences.views, "aff_preferences") ] return form( @@ -302,8 +302,7 @@ def add_radiuskey(request): @can_edit(RadiusKey) def edit_radiuskey(request, radiuskey_instance, **_kwargs): """View used to edit RADIUS keys.""" - radiuskey = RadiusKeyForm(request.POST or None, - instance=radiuskey_instance) + radiuskey = RadiusKeyForm(request.POST or None, instance=radiuskey_instance) if radiuskey.is_valid(): radiuskey.save() messages.success(request, _("The RADIUS key was edited.")) @@ -346,11 +345,13 @@ def add_switchmanagementcred(request): switchmanagementcred = SwitchManagementCredForm(request.POST or None) if switchmanagementcred.is_valid(): switchmanagementcred.save() - messages.success(request, _( - "The switch management credentials were added.")) + messages.success(request, _("The switch management credentials were added.")) return redirect(reverse("preferences:display-options")) return form( - {"preferenceform": switchmanagementcred, "action_name": _("Add"), }, + { + "preferenceform": switchmanagementcred, + "action_name": _("Add"), + }, "preferences/preferences.html", request, ) @@ -364,8 +365,7 @@ def edit_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs) ) if switchmanagementcred.is_valid(): switchmanagementcred.save() - messages.success(request, _( - "The switch management credentials were edited.")) + messages.success(request, _("The switch management credentials were edited.")) return redirect(reverse("preferences:display-options")) return form( {"preferenceform": switchmanagementcred, "action_name": _("Edit")}, @@ -414,7 +414,10 @@ def add_mailcontact(request): messages.success(request, _("The contact email address was created.")) return redirect(reverse("preferences:display-options")) return form( - {"preferenceform": mailcontact, "action_name": _("Add"), }, + { + "preferenceform": mailcontact, + "action_name": _("Add"), + }, "preferences/preferences.html", request, ) @@ -442,14 +445,12 @@ def edit_mailcontact(request, mailcontact_instance, **_kwargs): @can_delete_set(MailContact) def del_mailcontact(request, instances): """View used to delete one or several contact email addresses.""" - mailcontacts = DelMailContactForm( - request.POST or None, instances=instances) + mailcontacts = DelMailContactForm(request.POST or None, instances=instances) if mailcontacts.is_valid(): mailcontacts_dels = mailcontacts.cleaned_data["mailcontacts"] for mailcontacts_del in mailcontacts_dels: mailcontacts_del.delete() - messages.success(request, _( - "The contact email adress was deleted.")) + messages.success(request, _("The contact email adress was deleted.")) return redirect(reverse("preferences:display-options")) return form( {"preferenceform": mailcontacts, "action_name": _("Delete")}, diff --git a/re2o/acl.py b/re2o/acl.py index 0a5dd1ee..f74427ea 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -44,13 +44,11 @@ def acl_error_message(msg, permissions): """Create an error message for msg and permissions.""" if permissions is None: return msg - groups = ", ".join( - [g.name for g in get_group_having_permission(*permissions)]) + groups = ", ".join([g.name for g in get_group_having_permission(*permissions)]) message = msg or _("You don't have the right to edit this option.") if groups: return ( - message + - _("You need to be a member of one of these groups: %s.") % groups + message + _("You need to be a member of one of these groups: %s.") % groups ) else: return message + _("No group has the %s permission(s)!") % " or ".join( @@ -181,8 +179,7 @@ ModelC) # `wrapper` inside the `decorator` function, you need to read some #  documentation on decorators ! def decorator(view): - """The decorator to use on a specific view - """ + """The decorator to use on a specific view""" def wrapper(request, *args, **kwargs): """The wrapper used for a specific request""" @@ -198,8 +195,7 @@ ModelC) # and store it to pass it to the view. if on_instance: try: - target = target.get_instance( - target_id, *args, **kwargs) + target = target.get_instance(target_id, *args, **kwargs) instances.append(target) except target.DoesNotExist: # A non existing instance is a valid reason to deny @@ -245,11 +241,9 @@ ModelC) # Store the messages at the right place. for can, msg, permissions in process_target(target, fields, target_id): if not can: - error_messages.append( - acl_error_message(msg, permissions)) + error_messages.append(acl_error_message(msg, permissions)) elif msg: - warning_messages.append( - acl_error_message(msg, permissions)) + warning_messages.append(acl_error_message(msg, permissions)) # Display the warning messages if not api: @@ -264,18 +258,24 @@ ModelC) for msg in error_messages: messages.error( request, - msg or _( - "You don't have the right to access this menu."), + msg or _("You don't have the right to access this menu."), ) # And redirect the user to the right place. if request.user.id is not None: if not api: return redirect( - reverse("users:profil", kwargs={ - "userid": str(request.user.id)}) + reverse( + "users:profil", kwargs={"userid": str(request.user.id)} + ) ) else: - return Response(data={"errors": error_messages, "warning": warning_messages}, status=403) + return Response( + data={ + "errors": error_messages, + "warning": warning_messages, + }, + status=403, + ) else: return redirect(reverse("index")) return view(request, *chain(instances, args), **kwargs) @@ -326,12 +326,10 @@ def can_delete_set(model): If none of them, return an error""" def decorator(view): - """The decorator to use on a specific view - """ + """The decorator to use on a specific view""" def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ + """The wrapper used for a specific request""" all_objects = model.objects.all() instances_id = [] for instance in all_objects: @@ -344,8 +342,7 @@ def can_delete_set(model): request, _("You don't have the right to access this menu.") ) return redirect( - reverse("users:profil", kwargs={ - "userid": str(request.user.id)}) + reverse("users:profil", kwargs={"userid": str(request.user.id)}) ) return view(request, instances, *args, **kwargs) @@ -373,8 +370,7 @@ def can_view_all(*targets): def can_view_app(*apps_name): - """Decorator to check if an user can view the applications. - """ + """Decorator to check if an user can view the applications.""" for app_name in apps_name: assert app_name in sys.modules.keys() return acl_base_decorator( @@ -388,12 +384,10 @@ def can_edit_history(view): """Decorator to check if an user can edit history.""" def wrapper(request, *args, **kwargs): - """The wrapper used for a specific request - """ + """The wrapper used for a specific request""" if request.user.has_perm("admin.change_logentry"): return view(request, *args, **kwargs) - messages.error(request, _( - "You don't have the right to edit the history.")) + messages.error(request, _("You don't have the right to edit the history.")) return redirect( reverse("users:profil", kwargs={"userid": str(request.user.id)}) ) diff --git a/re2o/mixins.py b/re2o/mixins.py index fbe715f0..26034e07 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -29,9 +29,9 @@ from django.utils.translation import ugettext as _ class RevMixin(object): - """ A mixin to subclass the save and delete function of a model + """A mixin to subclass the save and delete function of a model to enforce the versioning of the object before those actions - really happen """ + really happen""" def save(self, *args, **kwargs): """ Creates a version of this object and save it to database """ @@ -49,8 +49,8 @@ class RevMixin(object): class FormRevMixin(object): - """ A mixin to subclass the save function of a form - to enforce the versionning of the object before it is really edited """ + """A mixin to subclass the save function of a form + to enforce the versionning of the object before it is really edited""" def save(self, *args, **kwargs): """ Create a version of this object and save it to database """ @@ -61,8 +61,7 @@ class FormRevMixin(object): ) elif self.changed_data: reversion.set_comment( - "Field(s) edited: %s" % ", ".join( - field for field in self.changed_data) + "Field(s) edited: %s" % ", ".join(field for field in self.changed_data) ) return super(FormRevMixin, self).save(*args, **kwargs) @@ -130,7 +129,7 @@ class AclMixin(object): Parameters: user_request: User calling for this action - self: Instance to edit + self: Instance to edit Returns: Boolean: True if user_request has the right access to do it, else @@ -151,7 +150,7 @@ class AclMixin(object): Parameters: user_request: User calling for this action - self: Instance to delete + self: Instance to delete Returns: Boolean: True if user_request has the right access to do it, else @@ -214,7 +213,7 @@ class AclMixin(object): Parameters: user_request: User calling for this action - self: Instance to view + self: Instance to view Returns: Boolean: True if user_request has the right access to do it, else From b8890ffaa0ab09ed7d2355d6a636808e1dd28ab2 Mon Sep 17 00:00:00 2001 From: Yoann Pietri Date: Sun, 22 Nov 2020 09:51:25 +0100 Subject: [PATCH 11/55] Fix display theme on profile --- users/templates/users/profil.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index dbd5b6e1..3dfff7cf 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -351,7 +351,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Theme" %}
-
{{ request.user.theme_name }}
+
{{ users.theme_name }}
From 4ec4763d1c9ba78a74f610f7bc39ecc21afb793c Mon Sep 17 00:00:00 2001 From: Hugo Levy-Falk Date: Wed, 30 Dec 2020 18:23:55 +0100 Subject: [PATCH 12/55] Fix side effect when importing multi_op --- multi_op/forms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/multi_op/forms.py b/multi_op/forms.py index 59043287..f4c839de 100644 --- a/multi_op/forms.py +++ b/multi_op/forms.py @@ -36,15 +36,17 @@ from topologie.models import Dormitory from .preferences.models import MultiopOption + class DormitoryForm(FormRevMixin, Form): """Form used to select dormitories.""" dormitory = forms.ModelMultipleChoiceField( - queryset=MultiopOption.get_cached_value("enabled_dorm").all(), label=_("Dormitory"), widget=forms.CheckboxSelectMultiple, required=False, + queryset=Dormitory.objects.none(), ) def __init__(self, *args, **kwargs): super(DormitoryForm, self).__init__(*args, **kwargs) + self.fields["dormitory"].queryset = MultiopOption.get_cached_value("enabled_dorm").all() From 1d011435f52dbb50ce8ad569a569800466ede2ce Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 12:15:37 +0100 Subject: [PATCH 13/55] Add views and urls for autocomplete on user app --- users/urls.py | 7 ++++ users/views_autocomplete.py | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 users/views_autocomplete.py diff --git a/users/urls.py b/users/urls.py index 74cdd284..b7d52ddc 100644 --- a/users/urls.py +++ b/users/urls.py @@ -31,6 +31,7 @@ from __future__ import unicode_literals from django.conf.urls import url from . import views +from . import views_autocomplete urlpatterns = [ url(r"^new_user/$", views.new_user, name="new-user"), @@ -128,4 +129,10 @@ urlpatterns = [ url(r"^index_clubs/$", views.index_clubs, name="index-clubs"), url(r"^initial_register/$", views.initial_register, name="initial-register"), url(r"^edit_theme/(?P[0-9]+)$", views.edit_theme, name="edit-theme"), + ### Autocomplete Views + url(r'^user-autocomplete/$', views_autocomplete.UserAutocomplete.as_view(), name='user-autocomplete',), + url(r'^adherent-autocomplete/$', views_autocomplete.AdherentAutocomplete.as_view(), name='adherent-autocomplete',), + url(r'^club-autocomplete/$', views_autocomplete.ClubAutocomplete.as_view(), name='club-autocomplete',), + url(r'^school-autocomplete/$', views_autocomplete.SchoolAutocomplete.as_view(), name='school-autocomplete',), + url(r'^shell-autocomplete/$', views_autocomplete.ShellAutocomplete.as_view(), name='shell-autocomplete',), ] diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py new file mode 100644 index 00000000..a2ea0c06 --- /dev/null +++ b/users/views_autocomplete.py @@ -0,0 +1,72 @@ +# -*- 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, + Ban, + Whitelist, + School, + ListRight, + Request, + ServiceUser, + Adherent, + Club, + ListShell, + EMailAddress, +) + +from re2o.mixins import AutocompleteViewMixin + +from re2o.acl import ( + can_view_all, +) + +#@can_view_all(School) +class SchoolAutocomplete(AutocompleteViewMixin): + obj_type = School + +#@can_view_all(User) +class UserAutocomplete(AutocompleteViewMixin): + obj_type = User + +#@can_view_all(Adherent) +class AdherentAutocomplete(AutocompleteViewMixin): + obj_type = Adherent + +#@can_view_all(Club) +class ClubAutocomplete(AutocompleteViewMixin): + obj_type = Club + +class ShellAutocomplete(AutocompleteViewMixin): + obj_type = ListShell + query_filter = "shell__icontains" From ecd468201a91349ba3d17311d2d74125b954cc12 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 12:16:08 +0100 Subject: [PATCH 14/55] Remove and replace massive_bootstrap; forms change for autocomplete --- users/forms.py | 30 ++++++++++++++++++++++++++---- users/templates/users/user.html | 6 ++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/users/forms.py b/users/forms.py index 1fc00584..3bf19dcc 100644 --- a/users/forms.py +++ b/users/forms.py @@ -60,7 +60,7 @@ from topologie.models import Port from preferences.models import OptionalUser from re2o.utils import remove_user_room from re2o.base import get_input_formats_help_text -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin, AutocompleteModelMixin from re2o.field_permissions import FieldPermissionFormMixin from preferences.models import GeneralOption @@ -350,6 +350,17 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): "telephone", "room", ] + widgets = { + "school": AutocompleteModelMixin( + url="/users/school-autocomplete", + ), + "room": AutocompleteModelMixin( + url="/topologie/room-autocomplete", + ), + "shell": AutocompleteModelMixin( + url="/users/shell-autocomplete", + ) + } force = forms.BooleanField( label=_("Force the move?"), initial=False, required=False @@ -461,7 +472,7 @@ class AdherentCreationForm(AdherentForm): # Checkbox for GTU gtu_check = forms.BooleanField(required=True) - class Meta: + class Meta(AdherentForm.Meta): model = Adherent fields = [ "name", @@ -556,7 +567,7 @@ class AdherentEditForm(AdherentForm): if "shell" in self.fields: self.fields["shell"].empty_label = _("Default shell") - class Meta: + class Meta(AdherentForm.Meta): model = Adherent fields = [ "name", @@ -609,6 +620,17 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): "shell", "mailing", ] + widgets = { + "school": AutocompleteModelMixin( + url="/users/school-autocomplete", + ), + "room": AutocompleteModelMixin( + url="/topologie/room-autocomplete", + ), + "shell": AutocompleteModelMixin( + url="/users/shell-autocomplete", + ) + } def clean_telephone(self): """Clean telephone, check if telephone is made mandatory, and @@ -1056,4 +1078,4 @@ class ThemeForm(FormRevMixin, forms.Form): if not themes: themes = ["default.css"] super(ThemeForm, self).__init__(*args, **kwargs) - self.fields['theme'].choices = [(theme, theme) for theme in themes] \ No newline at end of file + self.fields['theme'].choices = [(theme, theme) for theme in themes] diff --git a/users/templates/users/user.html b/users/templates/users/user.html index d9302e00..8b143f5c 100644 --- a/users/templates/users/user.html +++ b/users/templates/users/user.html @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load static %} {% load i18n %} {% block title %}{% trans "Users" %}{% endblock %} @@ -34,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% massive_bootstrap_form userform 'room,school,administrators,members' %} + {% bootstrap_form userform %} {% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}
{% if load_js_file %} @@ -48,5 +47,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,


+ +{{ userform.media }} + {% endblock %} From ab7cb1de6c3e274d4924d6684b682ca3cb2246a2 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 13:20:46 +0100 Subject: [PATCH 15/55] Add views, forms and urls for autocomplete topologie --- topologie/forms.py | 46 ++++++++++- topologie/urls.py | 8 ++ topologie/views_autocomplete.py | 133 ++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 topologie/views_autocomplete.py diff --git a/topologie/forms.py b/topologie/forms.py index 4044ca3e..348e834e 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -37,7 +37,7 @@ from django.utils.translation import ugettext_lazy as _ from machines.models import Interface from machines.forms import EditMachineForm, NewMachineForm -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin from .models import ( Port, @@ -62,6 +62,23 @@ class PortForm(FormRevMixin, ModelForm): class Meta: model = Port fields = "__all__" + widgets = { + "switch": AutocompleteModelMixin( + url="/topologie/switch-autocomplete", + ), + "room": AutocompleteModelMixin( + url="/topologie/room-autocomplete", + ), + "machine_interface": AutocompleteModelMixin( + url="/machine/machine-autocomplete", + ), + "related": AutocompleteModelMixin( + url="/topologie/port-autocomplete", + ), + "custom_profile": AutocompleteModelMixin( + url="/topologie/portprofile-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -180,6 +197,11 @@ class EditRoomForm(FormRevMixin, ModelForm): class Meta: model = Room fields = "__all__" + widgets = { + "building": AutocompleteModelMixin( + url="/topologie/building-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -196,7 +218,11 @@ class CreatePortsForm(forms.Form): class EditModelSwitchForm(FormRevMixin, ModelForm): """Form used to edit switch models.""" - members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + required=False + ) class Meta: model = ModelSwitch @@ -230,11 +256,20 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm): class EditSwitchBayForm(FormRevMixin, ModelForm): """Form used to edit switch bays.""" - members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False, + widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + ) class Meta: model = SwitchBay fields = "__all__" + widgets = { + "building": AutocompleteModelMixin( + url="/topologie/building-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -279,6 +314,11 @@ class EditPortProfileForm(FormRevMixin, ModelForm): class Meta: model = PortProfile fields = "__all__" + widgets = { + "vlan_tagged": AutocompleteMultipleModelMixin( + url="/machine/vlan-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) diff --git a/topologie/urls.py b/topologie/urls.py index 9a0ff1e3..c5ac95eb 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -28,6 +28,7 @@ from __future__ import unicode_literals from django.conf.urls import url from . import views +from . import views_autocomplete urlpatterns = [ url(r"^$", views.index, name="index"), @@ -169,4 +170,11 @@ urlpatterns = [ views.del_module_on, name="del-module-on", ), + ### Autocomplete Views + url(r'^room-autocomplete/$', views_autocomplete.RoomAutocomplete.as_view(), name='room-autocomplete',), + url(r'^building-autocomplete/$', views_autocomplete.BuildingAutocomplete.as_view(), name='building-autocomplete',), + url(r'^dormitory-autocomplete/$', views_autocomplete.DormitoryAutocomplete.as_view(), name='dormitory-autocomplete',), + url(r'^switch-autocomplete/$', views_autocomplete.SwitchAutocomplete.as_view(), name='switch-autocomplete',), + url(r'^port-autocomplete/$', views_autocomplete.PortAutocomplete.as_view(), name='profile-autocomplete',), + url(r'^portprofile-autocomplete/$', views_autocomplete.PortProfileAutocomplete.as_view(), name='portprofile-autocomplete',), ] diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py new file mode 100644 index 00000000..36d03c3e --- /dev/null +++ b/topologie/views_autocomplete.py @@ -0,0 +1,133 @@ +# -*- 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, +) + +from re2o.mixins import AutocompleteViewMixin + +from re2o.acl import ( + can_view_all, +) + + +#@can_view_all(School) +class RoomAutocomplete(AutocompleteViewMixin): + obj_type = Room + + # Override get_queryset to add annotations so search behaves more like users expect it to + def get_queryset(self): + # Suppose we have a dorm named Dorm, a building name B, and rooms from 001 - 999 + # Comments explain what we try to match + qs = self.obj_type.objects.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: + qs = qs.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) + ) + + return qs + + +#@can_view_all(Dormitory) +class DormitoryAutocomplete(AutocompleteViewMixin): + obj_type = Dormitory + + +#@can_view_all(Building) +class BuildingAutocomplete(AutocompleteViewMixin): + obj_type = Building + + def get_queryset(self): + # We want to be able to filter by dorm so it's easier + qs = self.obj_type.objects.annotate( + full_name=Concat("dormitory__name", Value(" "), "name"), + full_name_colon=Concat("dormitory__name", Value(" : "), "name"), + ).all() + + if self.q: + qs = qs.filter( + Q(full_name__icontains=self.q) + | Q(full_name_colon__icontains=self.q) + ) + + return qs + +class SwitchAutocomplete(AutocompleteViewMixin): + obj_type = Switch + + +class PortAutocomplete(AutocompleteViewMixin): + obj_type = Port + + def get_queryset(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 sepcify the output_field + qs = self.obj_type.objects.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: + qs = qs.filter( + Q(full_name__icontains=self.q) + | Q(full_name_stuck__icontains=self.q) + | Q(full_name_dash__icontains=self.q) + ) + + return qs + + + +class PortProfileAutocomplete(AutocompleteViewMixin): + obj_type = PortProfile From c3000413e0252da09d68dc6ee6b9a162a4888538 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 13:22:23 +0100 Subject: [PATCH 16/55] Add autocomplete on topologie app --- topologie/templates/topologie/switch.html | 3 +-- topologie/templates/topologie/topo.html | 6 ++++-- topologie/templates/topologie/topo_more.html | 5 ++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/topologie/templates/topologie/switch.html b/topologie/templates/topologie/switch.html index 6af65bcd..6191b592 100644 --- a/topologie/templates/topologie/switch.html +++ b/topologie/templates/topologie/switch.html @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Topology" %}{% endblock %} @@ -41,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} {% if topoform %}

{% trans "Specific settings for the switch" %}

- {% massive_bootstrap_form topoform 'switch_interface' %} + {% bootstrap_form topoform %} {% endif %} {% trans "Confirm" as tr_confirm %} {% bootstrap_button tr_confirm button_type="submit" icon='ok' button_class='btn-success' %} diff --git a/topologie/templates/topologie/topo.html b/topologie/templates/topologie/topo.html index 900a2b59..640f40a0 100644 --- a/topologie/templates/topologie/topo.html +++ b/topologie/templates/topologie/topo.html @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Topology" %}{% endblock %} @@ -37,11 +36,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %}
{% csrf_token %} - {% massive_bootstrap_form topoform 'room,related,machine_interface,members,vlan_tagged,switch' %} + {% bootstrap_form topoform %} {% bootstrap_button action_name icon='ok' button_class='btn-success' %}



+ +{{ topoform.media }} + {% endblock %} diff --git a/topologie/templates/topologie/topo_more.html b/topologie/templates/topologie/topo_more.html index 0180c72f..b0208568 100644 --- a/topologie/templates/topologie/topo_more.html +++ b/topologie/templates/topologie/topo_more.html @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Topology" %}{% endblock %} @@ -46,11 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} {% if topoform %}

{% blocktrans %}Specific settings for the {{ device }} object{% endblocktrans %}

- {% massive_bootstrap_form topoform 'ipv4,machine' mbf_param=i_mbf_param%} + {% bootstrap_form topoform %} {% endif %} {% if machineform %}

{% blocktrans %}General settings for the machine linked to the {{ device }} object{% endblocktrans %}

- {% massive_bootstrap_form machineform 'user' %} + {% bootstrap_form machineform %} {% endif %} {% if domainform %}

{% trans "DNS name" %}

From d976b0b3f809b22d776a69a8f1589a26d70a4b5f Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 16:18:11 +0100 Subject: [PATCH 17/55] Improved filter for user search --- users/views_autocomplete.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index a2ea0c06..94f5a93f 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -31,7 +31,7 @@ Here are defined the autocomplete class based view. """ from __future__ import unicode_literals -from .models import ( +from .models import ( User, Ban, Whitelist, @@ -51,6 +51,9 @@ from re2o.acl import ( can_view_all, ) +from django.db.models import Q, Value, CharField +from django.db.models.functions import Concat + #@can_view_all(School) class SchoolAutocomplete(AutocompleteViewMixin): obj_type = School @@ -58,6 +61,22 @@ class SchoolAutocomplete(AutocompleteViewMixin): #@can_view_all(User) class UserAutocomplete(AutocompleteViewMixin): obj_type = User + # Override get_queryset to add annotations so search behaves more like users expect it to + def get_queryset(self): + # Comments explain what we try to match + qs = self.obj_type.objects.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: + qs = qs.filter( + Q(pseudo__icontains=self.q) + | Q(full_name__icontains=self.q) + | Q(full_name_reverse__icontains=self.q) + ) + + return qs #@can_view_all(Adherent) class AdherentAutocomplete(AutocompleteViewMixin): From 2b3bb62a21d0747c1f0fe100f51a38147f637724 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 16:18:58 +0100 Subject: [PATCH 18/55] Vlan Autocomplete for portprofile --- topologie/forms.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/topologie/forms.py b/topologie/forms.py index 348e834e..2687d65a 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -316,7 +316,10 @@ class EditPortProfileForm(FormRevMixin, ModelForm): fields = "__all__" widgets = { "vlan_tagged": AutocompleteMultipleModelMixin( - url="/machine/vlan-autocomplete", + url="/machines/vlan-autocomplete", + ), + "vlan_untagged": AutocompleteModelMixin( + url="/machines/vlan-autocomplete", ), } From 7441ed9246a780974ae72fdca0c97b93cf44f6b2 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 16:19:29 +0100 Subject: [PATCH 19/55] Add autocomplete on machine form fields --- machines/forms.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++- machines/urls.py | 9 +++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/machines/forms.py b/machines/forms.py index 03f1ecc7..7141bcfc 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -40,7 +40,7 @@ from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin from .models import ( Domain, Machine, @@ -71,6 +71,11 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class Meta: model = Machine fields = "__all__" + widgets = { + "user": AutocompleteModelMixin( + url="/users/user-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -91,6 +96,17 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class Meta: model = Interface fields = ["machine", "machine_type", "ipv4", "mac_address", "details"] + widgets = { + "machine": AutocompleteModelMixin( + url="/machines/machine-autocomplete", + ), + "machine_type": AutocompleteModelMixin( + url="/machines/machinetype-autocomplete", + ), + "ipv4": AutocompleteModelMixin( + url="/machines/ipv4-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -139,6 +155,11 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class Meta: model = Domain fields = ["name", "extension", "ttl"] + widgets = { + "extension": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -222,6 +243,14 @@ class IpTypeForm(FormRevMixin, ModelForm): class Meta: model = IpType fields = "__all__" + widgets = { + "vlan": AutocompleteModelMixin( + url="/machines/vlan-autocomplete", + ), + "extension": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -351,6 +380,14 @@ class MxForm(FormRevMixin, ModelForm): class Meta: model = Mx fields = ["zone", "priority", "name", "ttl"] + widgets = { + "zone": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + "name": AutocompleteModelMixin( + url="/machines/domain-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -386,6 +423,14 @@ class NsForm(FormRevMixin, ModelForm): class Meta: model = Ns fields = ["zone", "ns", "ttl"] + widgets = { + "zone": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + "ns": AutocompleteModelMixin( + url="/machines/domain-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -419,6 +464,11 @@ class TxtForm(FormRevMixin, ModelForm): class Meta: model = Txt fields = "__all__" + widgets = { + "zone": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -449,6 +499,11 @@ class DNameForm(FormRevMixin, ModelForm): class Meta: model = DName fields = "__all__" + widgets = { + "zone": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -479,6 +534,14 @@ class SrvForm(FormRevMixin, ModelForm): class Meta: model = Srv fields = "__all__" + widgets = { + "extension": AutocompleteModelMixin( + url="/machines/extension-autocomplete", + ), + "target": AutocompleteModelMixin( + url="/machines/domain-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -509,6 +572,14 @@ class NasForm(FormRevMixin, ModelForm): class Meta: model = Nas fields = "__all__" + widgets = { + "nas_type": AutocompleteModelMixin( + url="/machines/machinetype-autocomplete", + ), + "machine_type": AutocompleteModelMixin( + url="/machines/machinetype-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -539,6 +610,11 @@ class RoleForm(FormRevMixin, ModelForm): class Meta: model = Role fields = "__all__" + widgets = { + "servers": AutocompleteMultipleModelMixin( + url="/machines/interface-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -572,6 +648,11 @@ class ServiceForm(FormRevMixin, ModelForm): class Meta: model = Service fields = "__all__" + widgets = { + "servers": AutocompleteMultipleModelMixin( + url="/machines/interface-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -656,6 +737,11 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm): class Meta: model = Interface fields = ["port_lists"] + widgets = { + "port_lists": AutocompleteMultipleModelMixin( + url="/machines/ouvertureportlist-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) diff --git a/machines/urls.py b/machines/urls.py index a8e1dbea..2967f7fd 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -29,6 +29,7 @@ from __future__ import unicode_literals from django.conf.urls import url from . import views +from . import views_autocomplete urlpatterns = [ url(r"^new_machine/(?P[0-9]+)$", views.new_machine, name="new-machine"), @@ -153,4 +154,12 @@ urlpatterns = [ views.configure_ports, name="port-config", ), + ### Autocomplete Views + url(r'^vlan-autocomplete/$', views_autocomplete.VlanAutocomplete.as_view(), name='vlan-autocomplete',), + url(r'^interface-autocomplete/$', views_autocomplete.InterfaceAutocomplete.as_view(), name='interface-autocomplete',), + url(r'^machine-autocomplete/$', views_autocomplete.MachineAutocomplete.as_view(), name='machine-autocomplete',), + url(r'^machinetype-autocomplete/$', views_autocomplete.MachineTypeAutocomplete.as_view(), name='machinetype-autocomplete',), + url(r'^extension-autocomplete/$', views_autocomplete.ExtensionAutocomplete.as_view(), name='extension-autocomplete',), + url(r'^domain-autocomplete/$', views_autocomplete.DomainAutocomplete.as_view(), name='domain-autocomplete',), + url(r'^ouvertureportlist-autocomplete/$', views_autocomplete.OuverturePortListAutocomplete.as_view(), name='ouvertureportlist-autocomplete',), ] From db5873960c2b61ce54fb33939a9c45ed783f5b48 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 16:20:33 +0100 Subject: [PATCH 20/55] Remove massive_boostrap form on machine forms --- machines/templates/machines/machine.html | 35 ++++++++++++++---------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index f74730b2..2cd97097 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Machines" %}{% endblock %} @@ -33,15 +32,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %} {% if machineform %} {% bootstrap_form_errors machineform %} + {{ machineform.media }} {% endif %} {% if interfaceform %} {% bootstrap_form_errors interfaceform %} + {{ interfaceform.media }} {% endif %} {% if domainform %} {% bootstrap_form_errors domainform %} {% endif %} {% if iptypeform %} {% bootstrap_form_errors iptypeform %} + {{ iptypeform.media }} {% endif %} {% if machinetypeform %} {% bootstrap_form_errors machinetypeform %} @@ -51,36 +53,45 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if mxform %} {% bootstrap_form_errors mxform %} + {{ mxform.media }} {% endif %} {% if nsform %} {% bootstrap_form_errors nsform %} + {{ nsform.media }} {% endif %} {% if txtform %} {% bootstrap_form_errors txtform %} + {{ txtform.media }} {% endif %} {% if dnameform %} {% bootstrap_form_errors dnameform %} + {{ dnameform.media }} {% endif %} {% if srvform %} {% bootstrap_form_errors srvform %} + {{ srvform.media }} {% endif %} {% if aliasform %} {% bootstrap_form_errors aliasform %} + {{ aliasform.media }} {% endif %} {% if serviceform %} {% bootstrap_form_errors serviceform %} + {{ serviceform.media }} {% endif %} {% if sshfpform %} {% bootstrap_form_errors sshfpform %} {% endif %} {% if roleform %} {% bootstrap_form_errors roleform %} + {{ roleform.media }} {% endif %} {% if vlanform %} {% bootstrap_form_errors vlanform %} {% endif %} {% if nasform %} {% bootstrap_form_errors nasform %} + {{ nasform.media }} {% endif %} {% if ipv6form %} {% bootstrap_form_errors ipv6form %} @@ -90,15 +101,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% csrf_token %} {% if machineform %}

{% trans "Machine" %}

- {% massive_bootstrap_form machineform 'user' %} + {% bootstrap_form machineform %} {% endif %} {% if interfaceform %}

{% trans "Interface" %}

- {% if i_mbf_param %} - {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %} - {% else %} - {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %} - {% endif %} + {% bootstrap_form interfaceform %} {% endif %} {% if domainform %}

{% trans "Domain" %}

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

{% trans "Extension" %}

- {% massive_bootstrap_form extensionform 'origin' %} + {% bootstrap_form extensionform %} {% endif %} {% if soaform %}

{% trans "SOA record" %}

@@ -122,11 +129,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if mxform %}

{% trans "MX record" %}

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

{% trans "NS record" %}

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

{% trans "TXT record" %}

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

{% trans "SRV record" %}

- {% massive_bootstrap_form srvform 'target' %} + {% bootstrap_form srvform %} {% endif %} {% if sshfpform %}

{% trans "SSHFP record" %}

@@ -146,15 +153,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if aliasform %}

{% trans "Alias" %}

- {% massive_bootstrap_form aliasform 'extension' %} + {% bootstrap_form aliasform %} {% endif %} {% if serviceform %}

{% trans "Service" %}

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

Role

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

{% trans "VLAN" %}

From 64ef9109ae9182c95e387c57ae7c87a09ba83a18 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 16:21:33 +0100 Subject: [PATCH 21/55] Add autocomplete view on machine --- machines/views_autocomplete.py | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 machines/views_autocomplete.py diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py new file mode 100644 index 00000000..2d4c8931 --- /dev/null +++ b/machines/views_autocomplete.py @@ -0,0 +1,90 @@ +# -*- 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, + Extension, + Domain, + OuverturePortList +) + +from re2o.mixins import AutocompleteViewMixin + +from re2o.acl import ( + can_view_all, +) + + +class VlanAutocomplete(AutocompleteViewMixin): + obj_type = Vlan + + +class MachineAutocomplete(AutocompleteViewMixin): + obj_type = Machine + + +class MachineTypeAutocomplete(AutocompleteViewMixin): + obj_type = MachineType + + +class ExtensionAutocomplete(AutocompleteViewMixin): + obj_type = Extension + + +class DomainAutocomplete(AutocompleteViewMixin): + obj_type = Domain + + +class OuverturePortListAutocomplete(AutocompleteViewMixin): + obj_type = OuverturePortList + + +class InterfaceAutocomplete(AutocompleteViewMixin): + obj_type = Interface + + def get_queryset(self): + qs = self.obj_type.objects.all() + + if self.q: + qs = qs.filter( + Q(domain__name__icontains=self.q) + | Q(machine__name__icontains=self.q) + ) + + return qs From b1fcfc306a2e8a2bbca2cde6ba045413762ad798 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 18:26:39 +0100 Subject: [PATCH 22/55] Add autocomplete on switchs/ap edit/creation forms --- topologie/forms.py | 12 +++++++++-- topologie/templates/topologie/topo_more.html | 2 ++ topologie/urls.py | 1 + topologie/views_autocomplete.py | 21 ++++++++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/topologie/forms.py b/topologie/forms.py index 2687d65a..fc7a4106 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -171,7 +171,7 @@ class AddAccessPointForm(NewMachineForm): class EditAccessPointForm(EditMachineForm): """Form used to edit access points.""" - class Meta: + class Meta(EditMachineForm.Meta): model = AccessPoint fields = "__all__" @@ -179,9 +179,17 @@ class EditAccessPointForm(EditMachineForm): class EditSwitchForm(EditMachineForm): """Form used to edit switches.""" - class Meta: + class Meta(EditMachineForm.Meta): model = Switch fields = "__all__" + widgets = { + "switchbay": AutocompleteModelMixin( + url="/topologie/switchbay-autocomplete", + ), + "user": AutocompleteModelMixin( + url="/users/user-autocomplete", + ), + } class NewSwitchForm(NewMachineForm): diff --git a/topologie/templates/topologie/topo_more.html b/topologie/templates/topologie/topo_more.html index b0208568..74a5265a 100644 --- a/topologie/templates/topologie/topo_more.html +++ b/topologie/templates/topologie/topo_more.html @@ -31,9 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %} {% if topoform %} {% bootstrap_form_errors topoform %} + {{ topoform.media }} {% endif %} {% if machineform %} {% bootstrap_form_errors machineform %} +{{ machineform.media }} {% endif %} {% if domainform %} {% bootstrap_form_errors domainform %} diff --git a/topologie/urls.py b/topologie/urls.py index c5ac95eb..05688fd3 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -177,4 +177,5 @@ urlpatterns = [ url(r'^switch-autocomplete/$', views_autocomplete.SwitchAutocomplete.as_view(), name='switch-autocomplete',), url(r'^port-autocomplete/$', views_autocomplete.PortAutocomplete.as_view(), name='profile-autocomplete',), url(r'^portprofile-autocomplete/$', views_autocomplete.PortProfileAutocomplete.as_view(), name='portprofile-autocomplete',), + url(r'^switchbay-autocomplete/$', views_autocomplete.SwitchBayAutocomplete.as_view(), name='switchbay-autocomplete',), ] diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index 36d03c3e..bef31fdb 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -41,6 +41,7 @@ from .models import ( Switch, PortProfile, Port, + SwitchBay, ) from re2o.mixins import AutocompleteViewMixin @@ -128,6 +129,26 @@ class PortAutocomplete(AutocompleteViewMixin): return qs +class SwitchBayAutocomplete(AutocompleteViewMixin): + obj_type = SwitchBay + + def get_queryset(self): + # Comments explain what we try to match + qs = self.obj_type.objects.annotate( + full_name=Concat("building__name", Value(" "), "name"), # Match when the user searches "" + dorm_name=Concat("building__dormitory__name", Value(" "), "name"), # Match "Dorm Local Sud" + dorm_full_name=Concat("building__dormitory__name", Value(" "), "building__name", Value(" "), "name"), # Match "Dorm J Local Sud" + ).all() + + if self.q: + qs = qs.filter( + Q(full_name__icontains=self.q) + | Q(dorm_name__icontains=self.q) + | Q(dorm_full_name__icontains=self.q) + ) + + return qs + class PortProfileAutocomplete(AutocompleteViewMixin): obj_type = PortProfile From 7dfaf6ff651fdfc9b00c3beb4451b93777b9c207 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 18:27:15 +0100 Subject: [PATCH 23/55] Add autocomplete on machine, interface edit forms --- machines/forms.py | 5 ++++- machines/urls.py | 1 + machines/views_autocomplete.py | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/machines/forms.py b/machines/forms.py index 7141bcfc..0c78be3e 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -104,7 +104,10 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): url="/machines/machinetype-autocomplete", ), "ipv4": AutocompleteModelMixin( - url="/machines/ipv4-autocomplete", + url="/machines/iplist-autocomplete", forward=['machine_type'], + attrs={ + 'data-placeholder': 'Automatic assigment. Type to choose specific ip.', + } ), } diff --git a/machines/urls.py b/machines/urls.py index 2967f7fd..387bdae1 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -162,4 +162,5 @@ urlpatterns = [ url(r'^extension-autocomplete/$', views_autocomplete.ExtensionAutocomplete.as_view(), name='extension-autocomplete',), url(r'^domain-autocomplete/$', views_autocomplete.DomainAutocomplete.as_view(), name='domain-autocomplete',), url(r'^ouvertureportlist-autocomplete/$', views_autocomplete.OuverturePortListAutocomplete.as_view(), name='ouvertureportlist-autocomplete',), + url(r'^iplist-autocomplete/$', views_autocomplete.IpListAutocomplete.as_view(), name='iplist-autocomplete',), ] diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py index 2d4c8931..a79fd276 100644 --- a/machines/views_autocomplete.py +++ b/machines/views_autocomplete.py @@ -41,7 +41,8 @@ from .models import ( MachineType, Extension, Domain, - OuverturePortList + OuverturePortList, + IpList ) from re2o.mixins import AutocompleteViewMixin @@ -88,3 +89,19 @@ class InterfaceAutocomplete(AutocompleteViewMixin): ) return qs + + +class IpListAutocomplete(AutocompleteViewMixin): + obj_type = IpList + + def get_queryset(self): + machine_type = self.forwarded.get('machine_type', None) + qs = self.obj_type.objects.filter(interface__isnull=True) + if machine_type: + qs = qs.filter(ip_type__machinetype__id=machine_type) + if self.q: + qs = qs.filter( + Q(ipv4__startswith=self.q) + ) + + return qs From 3f93c48c3d29cc86ea7e3ba15220b9804feedb09 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 18:46:37 +0100 Subject: [PATCH 24/55] Add ouverture ports on autocomplete --- machines/forms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/machines/forms.py b/machines/forms.py index 0c78be3e..f663f5a9 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -253,6 +253,9 @@ class IpTypeForm(FormRevMixin, ModelForm): "extension": AutocompleteModelMixin( url="/machines/extension-autocomplete", ), + "ouverture_ports": AutocompleteModelMixin( + url="/machines/ouvertureportlist-autocomplete", + ), } def __init__(self, *args, **kwargs): From 709c9a839eadca35fbfcbc787a652b1a83842812 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 18:47:20 +0100 Subject: [PATCH 25/55] Remove mbf on views --- machines/views.py | 91 ---------------------------------------------- topologie/views.py | 9 ----- 2 files changed, 100 deletions(-) diff --git a/machines/views.py b/machines/views.py index 8ec85583..04b8a3a8 100644 --- a/machines/views.py +++ b/machines/views.py @@ -124,91 +124,6 @@ from .models import ( ) - -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 ( - "id_Interface-machine_type_hidden" - if is_type_tt - else "id_Interface-machine_type" - ) - - -def generate_ipv4_choices(form_obj): - """ Generate the parameter choices for the massive_bootstrap_form tag - """ - f_ipv4 = form_obj.fields["ipv4"] - used_mtype_id = [] - choices = '{"":[{key:"",value:"' + _("Select a machine type first.") + '"}' - mtype_id = -1 - - for ip in f_ipv4.queryset.annotate(mtype_id=F("ip_type__machinetype__id")).order_by( - "mtype_id", "id" - ): - if mtype_id != ip.mtype_id: - mtype_id = ip.mtype_id - used_mtype_id.append(mtype_id) - choices += '],"{t}":[{{key:"",value:"{v}"}},'.format( - t=mtype_id, v=f_ipv4.empty_label or '""' - ) - choices += '{{key:{k},value:"{v}"}},'.format(k=ip.id, v=ip.ipv4) - - for t in form_obj.fields["machine_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 massive_bootstrap_form tag - """ - return ( - "new Bloodhound( {{" - 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),' - "queryTokenizer: Bloodhound.tokenizers.whitespace," - 'local: choices_ipv4[ $( "#{machine_type_id}" ).val() ],' - "identify: function( obj ) {{ return obj.key; }}" - "}} )" - ).format(machine_type_id=f_type_id(is_type_tt)) - - -def generate_ipv4_match_func(is_type_tt): - """ Generate the parameter match_func for the massive_bootstrap_form tag - """ - return ( - "function(q, sync) {{" - 'if (q === "") {{' - 'var first = choices_ipv4[$("#{machine_type_id}").val()].slice(0, 5);' - "first = first.map( function (obj) {{ return obj.key; }} );" - "sync(engine_ipv4.get(first));" - "}} else {{" - "engine_ipv4.search(q, sync);" - "}}" - "}}" - ).format(machine_type_id=f_type_id(is_type_tt)) - - -def generate_ipv4_mbf_param(form_obj, is_type_tt): - """ Generate all the parameters to use with the massive_bootstrap_form - tag """ - i_choices = {"ipv4": generate_ipv4_choices(form_obj)} - i_engine = {"ipv4": generate_ipv4_engine(is_type_tt)} - i_match_func = {"ipv4": generate_ipv4_match_func(is_type_tt)} - i_update_on = {"ipv4": [f_type_id(is_type_tt)]} - i_gen_select = {"ipv4": False} - i_mbf_param = { - "choices": i_choices, - "engine": i_engine, - "match_func": i_match_func, - "update_on": i_update_on, - "gen_select": i_gen_select, - } - return i_mbf_param - - @login_required @can_create(Machine) @can_edit(User) @@ -235,13 +150,11 @@ def new_machine(request, user, **_kwargs): new_domain.save() messages.success(request, _("The machine was created.")) return redirect(reverse("users:profil", kwargs={"userid": str(user.id)})) - i_mbf_param = generate_ipv4_mbf_param(interface, False) return form( { "machineform": machine, "interfaceform": interface, "domainform": domain, - "i_mbf_param": i_mbf_param, "action_name": _("Add"), }, "machines/machine.html", @@ -281,13 +194,11 @@ def edit_interface(request, interface_instance, **_kwargs): kwargs={"userid": str(interface_instance.machine.user.id)}, ) ) - i_mbf_param = generate_ipv4_mbf_param(interface_form, False) return form( { "machineform": machine_form, "interfaceform": interface_form, "domainform": domain_form, - "i_mbf_param": i_mbf_param, "action_name": _("Edit"), }, "machines/machine.html", @@ -332,12 +243,10 @@ def new_interface(request, machine, **_kwargs): return redirect( reverse("users:profil", kwargs={"userid": str(machine.user.id)}) ) - i_mbf_param = generate_ipv4_mbf_param(interface_form, False) return form( { "interfaceform": interface_form, "domainform": domain_form, - "i_mbf_param": i_mbf_param, "action_name": _("Add"), }, "machines/machine.html", diff --git a/topologie/views.py b/topologie/views.py index ff4db520..93c21d0e 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -56,7 +56,6 @@ from machines.forms import ( AddInterfaceForm, EditOptionVlanForm, ) -from machines.views import generate_ipv4_mbf_param from machines.models import Interface, Service_link, Vlan from preferences.models import AssoOption, GeneralOption @@ -560,13 +559,11 @@ def new_switch(request): new_domain_obj.save() messages.success(request, _("The switch was created.")) return redirect(reverse("topologie:index")) - i_mbf_param = generate_ipv4_mbf_param(interface, False) return form( { "topoform": interface, "machineform": switch, "domainform": domain, - "i_mbf_param": i_mbf_param, "device": _("switch"), }, "topologie/topo_more.html", @@ -634,14 +631,12 @@ def edit_switch(request, switch, switchid): new_domain_obj.save() messages.success(request, _("The switch was edited.")) return redirect(reverse("topologie:index")) - i_mbf_param = generate_ipv4_mbf_param(interface_form, False) return form( { "id_switch": switchid, "topoform": interface_form, "machineform": switch_form, "domainform": domain_form, - "i_mbf_param": i_mbf_param, "device": _("switch"), }, "topologie/topo_more.html", @@ -686,13 +681,11 @@ def new_ap(request): new_domain_obj.save() messages.success(request, _("The access point was created.")) return redirect(reverse("topologie:index-ap")) - i_mbf_param = generate_ipv4_mbf_param(interface, False) return form( { "topoform": interface, "machineform": ap, "domainform": domain, - "i_mbf_param": i_mbf_param, "device": _("access point"), }, "topologie/topo_more.html", @@ -737,13 +730,11 @@ def edit_ap(request, ap, **_kwargs): new_domain_obj.save() messages.success(request, _("The access point was edited.")) return redirect(reverse("topologie:index-ap")) - i_mbf_param = generate_ipv4_mbf_param(interface_form, False) return form( { "topoform": interface_form, "machineform": ap_form, "domainform": domain_form, - "i_mbf_param": i_mbf_param, "device": _("access point"), }, "topologie/topo_more.html", From f4dbec01c8e1ea709211e899758496c2ec998961 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 18:57:08 +0100 Subject: [PATCH 26/55] Add autocomplete on cotisations --- cotisations/forms.py | 10 +++- .../templates/cotisations/edit_facture.html | 4 +- cotisations/urls.py | 4 +- cotisations/views_autocomplete.py | 50 +++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 cotisations/views_autocomplete.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 0f135963..926db818 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -45,7 +45,7 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin from .models import ( Article, Paiement, @@ -79,6 +79,14 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): class Meta: model = Facture fields = "__all__" + widgets = { + "user": AutocompleteModelMixin( + url="/users/user-autocomplete", + ), + "banque": AutocompleteModelMixin( + url="/cotisations/banque-autocomplete", + ), + } def clean(self): cleaned_data = super(FactureForm, self).clean() diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index ca55cb66..b2f58df9 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -25,13 +25,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load staticfiles%} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Creation and editing of invoices" %}{% endblock %} {% block content %} {% bootstrap_form_errors factureform %} +{{ factureform.media }}
{% csrf_token %} @@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% else %}

{% trans "Edit invoice" %}

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

{% trans "Articles" %}

diff --git a/cotisations/urls.py b/cotisations/urls.py index 6baf74c7..c78effa6 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -27,7 +27,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from . import views +from . import views, views_autocomplete from . import payment_methods urlpatterns = [ @@ -104,4 +104,6 @@ urlpatterns = [ url(r"^index_paiement/$", views.index_paiement, name="index-paiement"), url(r"^control/$", views.control, name="control"), url(r"^$", views.index, name="index"), + ### Autocomplete Views + url(r'^banque-autocomplete/$', views_autocomplete.BanqueAutocomplete.as_view(), name='banque-autocomplete',), ] + payment_methods.urls.urlpatterns diff --git a/cotisations/views_autocomplete.py b/cotisations/views_autocomplete.py new file mode 100644 index 00000000..70afe855 --- /dev/null +++ b/cotisations/views_autocomplete.py @@ -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.mixins import AutocompleteViewMixin + +from re2o.acl import ( + can_view_all, +) + + +class BanqueAutocomplete(AutocompleteViewMixin): + obj_type = Banque + + From d8a53230c63d1f926c0c16ec2505aac06a2b3ced Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 19:45:25 +0100 Subject: [PATCH 27/55] Autocomplete on ticket --- tickets/forms.py | 7 ++++++- tickets/templates/tickets/edit.html | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tickets/forms.py b/tickets/forms.py index e28bfa85..ecde5492 100644 --- a/tickets/forms.py +++ b/tickets/forms.py @@ -28,7 +28,7 @@ from django import forms from django.template.loader import render_to_string from django.forms import ModelForm, Form from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin from django.utils.translation import ugettext_lazy as _ from .models import Ticket, CommentTicket @@ -58,6 +58,11 @@ class EditTicketForm(FormRevMixin, ModelForm): class Meta: model = Ticket fields = "__all__" + widgets = { + "user": AutocompleteModelMixin( + url="/users/user-autocomplete", + ), + } def __init__(self, *args, **kwargs): super(EditTicketForm, self).__init__(*args, **kwargs) diff --git a/tickets/templates/tickets/edit.html b/tickets/templates/tickets/edit.html index 3c5c1c3f..751ab8f5 100644 --- a/tickets/templates/tickets/edit.html +++ b/tickets/templates/tickets/edit.html @@ -25,7 +25,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Ticket" %}{% endblock %} @@ -34,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "Ticket opening" %}

{% bootstrap_form_errors ticketform %} +{{ ticketform.media }} {% csrf_token %} From 188bed9c180a3404fa6d8bdf77fab991d6524dc3 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 19:46:17 +0100 Subject: [PATCH 28/55] Remove templatetag massive bootstrap form --- re2o/templatetags/massive_bootstrap_form.py | 752 -------------------- 1 file changed, 752 deletions(-) delete mode 100644 re2o/templatetags/massive_bootstrap_form.py diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py deleted file mode 100644 index 449f7c24..00000000 --- a/re2o/templatetags/massive_bootstrap_form.py +++ /dev/null @@ -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_' and 'engine_' contains - respectively the array of all possible values and the engine - to match queries with possible values. - If not specified, the function used display up to the 10 first - elements if the query is empty and else the matching results. - Example : - 'match_func' : { - 'field_A': 'function(q, sync) { engine.search(q, sync); }', - 'field_B': ..., - ... - } - - update_on (optional) - A dict of list of ids that the values depends on. The engine - and the typeahead properties are recalculated and reapplied. - Example : - '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 - [ '[,[,...]]' ] - [ mbf_param = { - [ 'choices': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'engine': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'match_func': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - [, 'update_on': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ], - [, 'gen_select': { - [ '': '' - [, '': '' - [, ... ] ] ] - } ] - } ] - [ ] - %} - - **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_ """ - - if self.gen_select: - return ( - "function plop(o) {{" - "var c = [];" - "for( let i=0 ; i """ - 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 Date: Mon, 28 Dec 2020 19:46:50 +0100 Subject: [PATCH 29/55] Add autocomplete on preferences pannel --- preferences/forms.py | 22 ++++++++++++++++--- .../preferences/edit_preferences.html | 4 ++-- .../templates/preferences/preferences.html | 4 ++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/preferences/forms.py b/preferences/forms.py index a0880fd6..fdc752ab 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -29,7 +29,7 @@ from django.forms import ModelForm, Form from django.db.models import Q from django import forms from django.utils.translation import ugettext_lazy as _ -from re2o.mixins import FormRevMixin +from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin from .models import ( OptionalUser, OptionalMachine, @@ -168,6 +168,11 @@ class EditAssoOptionForm(ModelForm): class Meta: model = AssoOption fields = "__all__" + widgets = { + "utilisateur_asso": AutocompleteModelMixin( + url="/users/user-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -254,6 +259,11 @@ class MandateForm(ModelForm): class Meta: model = Mandate fields = "__all__" + widgets = { + "president": AutocompleteModelMixin( + url="/users/user-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -368,7 +378,9 @@ class RadiusKeyForm(FormRevMixin, ModelForm): """Form used to add and edit RADIUS keys.""" members = forms.ModelMultipleChoiceField( - queryset=Switch.objects.all(), required=False + queryset=Switch.objects.all(), + required=False, + widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), ) class Meta: @@ -391,7 +403,11 @@ class RadiusKeyForm(FormRevMixin, ModelForm): class SwitchManagementCredForm(FormRevMixin, ModelForm): """Form used to add and edit switch management credentials.""" - members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False, + widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + ) class Meta: model = SwitchManagementCred diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 2d9e1a62..c0fe2256 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -24,19 +24,19 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} {% bootstrap_form_errors options %} +{{ options.media }}

{% trans "Editing of preferences" %}

{% csrf_token %} - {% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %} + {% bootstrap_form options %} {% if formset %} {{ formset.management_form }} {% for f in formset %} diff --git a/preferences/templates/preferences/preferences.html b/preferences/templates/preferences/preferences.html index dda6ddfa..d1409cb8 100644 --- a/preferences/templates/preferences/preferences.html +++ b/preferences/templates/preferences/preferences.html @@ -25,20 +25,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load i18n %} -{% load massive_bootstrap_form %} {% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} {% if preferenceform %} {% bootstrap_form_errors preferenceform %} +{{ preferenceform.media }} {% endif %} {% csrf_token %} {% if preferenceform %} - {% massive_bootstrap_form preferenceform 'members,president' %} + {% bootstrap_form preferenceform %} {% endif %} {% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %} From 204cc48f58b49e03bb89d0547e78e4a643abdd39 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 20:01:02 +0100 Subject: [PATCH 30/55] Add iptype autocomplete search --- machines/forms.py | 5 +++++ machines/templates/machines/machine.html | 1 + machines/urls.py | 1 + machines/views_autocomplete.py | 5 +++++ 4 files changed, 12 insertions(+) diff --git a/machines/forms.py b/machines/forms.py index f663f5a9..3f6a3864 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -212,6 +212,11 @@ class MachineTypeForm(FormRevMixin, ModelForm): class Meta: model = MachineType fields = ["name", "ip_type"] + widgets = { + "ip_type": AutocompleteModelMixin( + url="/machines/iptype-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 2cd97097..bc1cdbeb 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -47,6 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if machinetypeform %} {% bootstrap_form_errors machinetypeform %} + {{ machinetypeform.media }} {% endif %} {% if extensionform %} {% bootstrap_form_errors extensionform %} diff --git a/machines/urls.py b/machines/urls.py index 387bdae1..98a6695f 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -159,6 +159,7 @@ urlpatterns = [ url(r'^interface-autocomplete/$', views_autocomplete.InterfaceAutocomplete.as_view(), name='interface-autocomplete',), url(r'^machine-autocomplete/$', views_autocomplete.MachineAutocomplete.as_view(), name='machine-autocomplete',), url(r'^machinetype-autocomplete/$', views_autocomplete.MachineTypeAutocomplete.as_view(), name='machinetype-autocomplete',), + url(r'^iptype-autocomplete/$', views_autocomplete.IpTypeAutocomplete.as_view(), name='iptype-autocomplete',), url(r'^extension-autocomplete/$', views_autocomplete.ExtensionAutocomplete.as_view(), name='extension-autocomplete',), url(r'^domain-autocomplete/$', views_autocomplete.DomainAutocomplete.as_view(), name='domain-autocomplete',), url(r'^ouvertureportlist-autocomplete/$', views_autocomplete.OuverturePortListAutocomplete.as_view(), name='ouvertureportlist-autocomplete',), diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py index a79fd276..e68a11c9 100644 --- a/machines/views_autocomplete.py +++ b/machines/views_autocomplete.py @@ -39,6 +39,7 @@ from .models import ( Machine, Vlan, MachineType, + IpType, Extension, Domain, OuverturePortList, @@ -64,6 +65,10 @@ class MachineTypeAutocomplete(AutocompleteViewMixin): obj_type = MachineType +class IpTypeAutocomplete(AutocompleteViewMixin): + obj_type = IpType + + class ExtensionAutocomplete(AutocompleteViewMixin): obj_type = Extension From 3883238f106bc73baa38854574323e12311887c4 Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 20:01:39 +0100 Subject: [PATCH 31/55] Fix switchs management autocomplete form --- preferences/forms.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/preferences/forms.py b/preferences/forms.py index fdc752ab..a7d03162 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -108,12 +108,19 @@ class EditOptionalTopologieForm(ModelForm): """Form used to edit the configuration of switches.""" automatic_provision_switchs = forms.ModelMultipleChoiceField( - Switch.objects.all(), required=False + Switch.objects.all(), + required=False, + widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), ) class Meta: model = OptionalTopologie fields = "__all__" + widgets = { + "switchs_ip_type": AutocompleteModelMixin( + url="/machines/iptype-autocomplete", + ), + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) From 0b9e49c25303cd50ab6a29e33d33946d4e629b9f Mon Sep 17 00:00:00 2001 From: chirac Date: Mon, 28 Dec 2020 21:06:24 +0100 Subject: [PATCH 32/55] Autocomplete for club edition --- users/forms.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/users/forms.py b/users/forms.py index 3bf19dcc..195807a3 100644 --- a/users/forms.py +++ b/users/forms.py @@ -60,7 +60,7 @@ from topologie.models import Port from preferences.models import OptionalUser from re2o.utils import remove_user_room from re2o.base import get_input_formats_help_text -from re2o.mixins import FormRevMixin, AutocompleteModelMixin +from re2o.mixins import FormRevMixin, AutocompleteMultipleModelMixin, AutocompleteModelMixin from re2o.field_permissions import FieldPermissionFormMixin from preferences.models import GeneralOption @@ -356,6 +356,9 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): ), "room": AutocompleteModelMixin( url="/topologie/room-autocomplete", + attrs = { + "data-minimum-input-length": 3 # Only trigger autocompletion after 3 characters have been typed + } ), "shell": AutocompleteModelMixin( url="/users/shell-autocomplete", @@ -659,6 +662,14 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm): class Meta: model = Club fields = ["administrators", "members"] + widgets = { + "administrators": AutocompleteMultipleModelMixin( + url="/users/adherent-autocomplete", + ), + "members": AutocompleteMultipleModelMixin( + url="/users/adherent-autocomplete", + ) + } def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) From d1e22bbbd1601e40378c911cdb41550e5246c85c Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Mon, 28 Dec 2020 22:24:35 +0100 Subject: [PATCH 33/55] Add django-autocomplete-light dependency --- pip_requirements.txt | 2 ++ static/css/autocomplete.css | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 static/css/autocomplete.css diff --git a/pip_requirements.txt b/pip_requirements.txt index 2d0aba41..148a3b5b 100644 --- a/pip_requirements.txt +++ b/pip_requirements.txt @@ -1,2 +1,4 @@ django-bootstrap3==11.1.0 django-macaddress==1.6.0 +#django-autocomplete-light==3.2.10 # Until Django 2.0+ +django-autocomplete-light diff --git a/static/css/autocomplete.css b/static/css/autocomplete.css new file mode 100644 index 00000000..dea68af3 --- /dev/null +++ b/static/css/autocomplete.css @@ -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 !imoortant; + overflow-x: hidden !important; + overflow-y: auto !important; +} + +.select2-container .select2-selection--multiple .select2-selection__rendered { + overflow: auto !important; +} From 9934ea448b1eb7456a5d6803bf4ad360b299da5e Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Mon, 28 Dec 2020 22:28:37 +0100 Subject: [PATCH 34/55] Remove legacy typeahead files --- static/css/bootstrap-tokenfield.css | 210 -- static/css/typeaheadjs.css | 93 - static/js/bootstrap-tokenfield/LICENSE.md | 23 - .../bootstrap-tokenfield.js | 1042 ------- static/js/typeahead/LICENSE | 19 - static/js/typeahead/typeahead.js | 2451 ----------------- templates/base.html | 7 +- 7 files changed, 2 insertions(+), 3843 deletions(-) delete mode 100644 static/css/bootstrap-tokenfield.css delete mode 100644 static/css/typeaheadjs.css delete mode 100644 static/js/bootstrap-tokenfield/LICENSE.md delete mode 100644 static/js/bootstrap-tokenfield/bootstrap-tokenfield.js delete mode 100644 static/js/typeahead/LICENSE delete mode 100644 static/js/typeahead/typeahead.js diff --git a/static/css/bootstrap-tokenfield.css b/static/css/bootstrap-tokenfield.css deleted file mode 100644 index ae12c1b7..00000000 --- a/static/css/bootstrap-tokenfield.css +++ /dev/null @@ -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; -} diff --git a/static/css/typeaheadjs.css b/static/css/typeaheadjs.css deleted file mode 100644 index 64c10736..00000000 --- a/static/css/typeaheadjs.css +++ /dev/null @@ -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; -} diff --git a/static/js/bootstrap-tokenfield/LICENSE.md b/static/js/bootstrap-tokenfield/LICENSE.md deleted file mode 100644 index 2449b356..00000000 --- a/static/js/bootstrap-tokenfield/LICENSE.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js b/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js deleted file mode 100644 index 5b2759d4..00000000 --- a/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js +++ /dev/null @@ -1,1042 +0,0 @@ -/*! - * bootstrap-tokenfield - * https://github.com/sliptree/bootstrap-tokenfield - * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT - */ - -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof exports === 'object') { - // For CommonJS and CommonJS-like environments where a window with jQuery - // is present, execute the factory with the jQuery instance from the window object - // For environments that do not inherently posses a window with a document - // (such as Node.js), expose a Tokenfield-making factory as module.exports - // This accentuates the need for the creation of a real window or passing in a jQuery instance - // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($); - module.exports = global.window && global.window.$ ? - factory( global.window.$ ) : - function( input ) { - if ( !input.$ && !input.fn ) { - throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" ); - } - return factory( input.$ || input ); - }; - } else { - // Browser globals - factory(jQuery, window); - } -}(function ($, window) { - - "use strict"; // jshint ;_; - - /* TOKENFIELD PUBLIC CLASS DEFINITION - * ============================== */ - - var Tokenfield = function (element, options) { - var _self = this - - this.$element = $(element) - this.textDirection = this.$element.css('direction'); - - // Extend options - this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options) - - // Setup delimiters and trigger keys - this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter - this._triggerKeys = $.map(this._delimiters, function (delimiter) { - return delimiter.charCodeAt(0); - }); - this._firstDelimiter = this._delimiters[0]; - - // Check for whitespace, dash and special characters - var whitespace = $.inArray(' ', this._delimiters) - , dash = $.inArray('-', this._delimiters) - - if (whitespace >= 0) - this._delimiters[whitespace] = '\\s' - - if (dash >= 0) { - delete this._delimiters[dash] - this._delimiters.unshift('-') - } - - var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] - $.each(this._delimiters, function (index, character) { - var pos = $.inArray(character, specialCharacters) - if (pos >= 0) _self._delimiters[index] = '\\' + character; - }); - - // Store original input width - var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null - , elStyleWidth = element.style.width - , elCSSWidth - , elWidth = this.$element.width() - - if (elRules) { - $.each( elRules, function (i, rule) { - if (rule.style.width) { - elCSSWidth = rule.style.width; - } - }); - } - - // Move original input out of the way - var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', - originalStyles = { position: this.$element.css('position') }; - originalStyles[hidingPosition] = this.$element.css(hidingPosition); - - this.$element - .data('original-styles', originalStyles) - .data('original-tabindex', this.$element.prop('tabindex')) - .css('position', 'absolute') - .css(hidingPosition, '-10000px') - .prop('tabindex', -1) - - // Create a wrapper - this.$wrapper = $('
') - if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') - if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') - if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') - - // Create a new input - var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) - this.$input = $('') - .appendTo( this.$wrapper ) - .prop( 'placeholder', this.$element.prop('placeholder') ) - .prop( 'id', id + '-tokenfield' ) - .prop( 'tabindex', this.$element.data('original-tabindex') ) - - // Re-route original input label to new input - var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) - if ( $label.length ) { - $label.prop( 'for', this.$input.prop('id') ) - } - - // Set up a copy helper to handle copy & paste - this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) - - // Set wrapper width - if (elStyleWidth) { - this.$wrapper.css('width', elStyleWidth); - } - else if (elCSSWidth) { - this.$wrapper.css('width', elCSSWidth); - } - // If input is inside inline-form with no width set, set fixed width - else if (this.$element.parents('.form-inline').length) { - this.$wrapper.width( elWidth ) - } - - // Set tokenfield disabled, if original or fieldset input is disabled - if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { - this.disable(); - } - - // Set tokenfield readonly, if original input is readonly - if (this.$element.prop('readonly')) { - this.readonly(); - } - - // Set up mirror for input auto-sizing - this.$mirror = $(''); - this.$input.css('min-width', this.options.minWidth + 'px') - $.each([ - 'fontFamily', - 'fontSize', - 'fontWeight', - 'fontStyle', - 'letterSpacing', - 'textTransform', - 'wordSpacing', - 'textIndent' - ], function (i, val) { - _self.$mirror[0].style[val] = _self.$input.css(val); - }); - this.$mirror.appendTo( 'body' ) - - // Insert tokenfield to HTML - this.$wrapper.insertBefore( this.$element ) - this.$element.prependTo( this.$wrapper ) - - // Calculate inner input width - this.update() - - // Create initial tokens, if any - this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) - - // Start listening to events - this.listen() - - // Initialize autocomplete, if necessary - if ( ! $.isEmptyObject( this.options.autocomplete ) ) { - var side = this.textDirection === 'rtl' ? 'right' : 'left' - , autocompleteOptions = $.extend({ - minLength: this.options.showAutocompleteOnFocus ? 0 : null, - position: { my: side + " top", at: side + " bottom", of: this.$wrapper } - }, this.options.autocomplete ) - - this.$input.autocomplete( autocompleteOptions ) - } - - // Initialize typeahead, if necessary - if ( ! $.isEmptyObject( this.options.typeahead ) ) { - - var typeaheadOptions = this.options.typeahead - , defaults = { - minLength: this.options.showAutocompleteOnFocus ? 0 : null - } - , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] - - args[0] = $.extend( {}, defaults, args[0] ) - - this.$input.typeahead.apply( this.$input, args ) - this.typeahead = true - } - } - - Tokenfield.prototype = { - - constructor: Tokenfield - - , createToken: function (attrs, triggerChange) { - var _self = this - - if (typeof attrs === 'string') { - attrs = { value: attrs, label: attrs } - } else { - // Copy objects to prevent contamination of data sources. - attrs = $.extend( {}, attrs ) - } - - if (typeof triggerChange === 'undefined') { - triggerChange = true - } - - // Normalize label and value - attrs.value = $.trim(attrs.value.toString()); - attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value - - // Bail out if has no value or label, or label is too short - if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return - - // Bail out if maximum number of tokens is reached - if (this.options.limit && this.getTokens().length >= this.options.limit) return - - // Allow changing token data before creating it - var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) - this.$element.trigger(createEvent) - - // Bail out if there if attributes are empty or event was defaultPrevented - if (!createEvent.attrs || createEvent.isDefaultPrevented()) return - - var $token = $('
') - .append('') - .append('×') - .data('attrs', attrs) - - // Insert token into HTML - if (this.$input.hasClass('tt-input')) { - // If the input has typeahead enabled, insert token before it's parent - this.$input.parent().before( $token ) - } else { - this.$input.before( $token ) - } - - // Temporarily set input width to minimum - this.$input.css('width', this.options.minWidth + 'px') - - var $tokenLabel = $token.find('.token-label') - , $closeButton = $token.find('.close') - - // Determine maximum possible token label width - if (!this.maxTokenWidth) { - this.maxTokenWidth = - this.$wrapper.width() - $closeButton.outerWidth() - - parseInt($closeButton.css('margin-left'), 10) - - parseInt($closeButton.css('margin-right'), 10) - - parseInt($token.css('border-left-width'), 10) - - parseInt($token.css('border-right-width'), 10) - - parseInt($token.css('padding-left'), 10) - - parseInt($token.css('padding-right'), 10) - parseInt($tokenLabel.css('border-left-width'), 10) - - parseInt($tokenLabel.css('border-right-width'), 10) - - parseInt($tokenLabel.css('padding-left'), 10) - - parseInt($tokenLabel.css('padding-right'), 10) - parseInt($tokenLabel.css('margin-left'), 10) - - parseInt($tokenLabel.css('margin-right'), 10) - } - - $tokenLabel.css('max-width', this.maxTokenWidth) - if (this.options.html) - $tokenLabel.html(attrs.label) - else - $tokenLabel.text(attrs.label) - - // Listen to events on token - $token - .on('mousedown', function (e) { - if (_self._disabled || _self._readonly) return false - _self.preventDeactivation = true - }) - .on('click', function (e) { - if (_self._disabled || _self._readonly) return false - _self.preventDeactivation = false - - if (e.ctrlKey || e.metaKey) { - e.preventDefault() - return _self.toggle( $token ) - } - - _self.activate( $token, e.shiftKey, e.shiftKey ) - }) - .on('dblclick', function (e) { - if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false - _self.edit( $token ) - }) - - $closeButton - .on('click', $.proxy(this.remove, this)) - - // Trigger createdtoken event on the original field - // indicating that the token is now in the DOM - this.$element.trigger($.Event('tokenfield:createdtoken', { - attrs: attrs, - relatedTarget: $token.get(0) - })) - - // Trigger change event on the original field - if (triggerChange) { - this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) - } - - // Update tokenfield dimensions - var _self = this - setTimeout(function () { - _self.update() - }, 0) - - // Return original element - return this.$element.get(0) - } - - , setTokens: function (tokens, add, triggerChange) { - if (!add) this.$wrapper.find('.token').remove() - - if (!tokens) return - - if (typeof triggerChange === 'undefined') { - triggerChange = true - } - - if (typeof tokens === 'string') { - if (this._delimiters.length) { - // Split based on delimiters - tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) - } else { - tokens = [tokens]; - } - } - - var _self = this - $.each(tokens, function (i, attrs) { - _self.createToken(attrs, triggerChange) - }) - - return this.$element.get(0) - } - - , getTokenData: function($token) { - var data = $token.map(function() { - var $token = $(this); - return $token.data('attrs') - }).get(); - - if (data.length == 1) { - data = data[0]; - } - - return data; - } - - , getTokens: function(active) { - var self = this - , tokens = [] - , activeClass = active ? '.active' : '' // get active tokens only - this.$wrapper.find( '.token' + activeClass ).each( function() { - tokens.push( self.getTokenData( $(this) ) ) - }) - return tokens - } - - , getTokensList: function(delimiter, beautify, active) { - delimiter = delimiter || this._firstDelimiter - beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify - - var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') - return $.map( this.getTokens(active), function (token) { - return token.value - }).join(separator) - } - - , getInput: function() { - return this.$input.val() - } - - , setInput: function (val) { - if (this.$input.hasClass('tt-input')) { - // Typeahead acts weird when simply setting input value to empty, - // so we set the query to empty instead - this.$input.typeahead('val', val) - } else { - this.$input.val(val) - } - } - - , listen: function () { - var _self = this - - this.$element - .on('change', $.proxy(this.change, this)) - - this.$wrapper - .on('mousedown',$.proxy(this.focusInput, this)) - - this.$input - .on('focus', $.proxy(this.focus, this)) - .on('blur', $.proxy(this.blur, this)) - .on('paste', $.proxy(this.paste, this)) - .on('keydown', $.proxy(this.keydown, this)) - .on('keypress', $.proxy(this.keypress, this)) - .on('keyup', $.proxy(this.keyup, this)) - - this.$copyHelper - .on('focus', $.proxy(this.focus, this)) - .on('blur', $.proxy(this.blur, this)) - .on('keydown', $.proxy(this.keydown, this)) - .on('keyup', $.proxy(this.keyup, this)) - - // Secondary listeners for input width calculation - this.$input - .on('keypress', $.proxy(this.update, this)) - .on('keyup', $.proxy(this.update, this)) - - this.$input - .on('autocompletecreate', function() { - // Set minimum autocomplete menu width - var $_menuElement = $(this).data('ui-autocomplete').menu.element - - var minWidth = _self.$wrapper.outerWidth() - - parseInt( $_menuElement.css('border-left-width'), 10 ) - - parseInt( $_menuElement.css('border-right-width'), 10 ) - - $_menuElement.css( 'min-width', minWidth + 'px' ) - }) - .on('autocompleteselect', function (e, ui) { - if (_self.createToken( ui.item )) { - _self.$input.val('') - if (_self.$input.data( 'edit' )) { - _self.unedit(true) - } - } - return false - }) - .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { - // Create token - if (_self.createToken( datum )) { - _self.$input.typeahead('val', '') - if (_self.$input.data( 'edit' )) { - _self.unedit(true) - } - } - }) - - // Listen to window resize - $(window).on('resize', $.proxy(this.update, this )) - - } - - , keydown: function (e) { - - if (!this.focused) return - - var _self = this - - switch(e.keyCode) { - case 8: // backspace - if (!this.$input.is(document.activeElement)) break - this.lastInputValue = this.$input.val() - break - - case 37: // left arrow - leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) - break - - case 38: // up arrow - upDown('prev') - break - - case 39: // right arrow - leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) - break - - case 40: // down arrow - upDown('next') - break - - case 65: // a (to handle ctrl + a) - if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break - this.activateAll() - e.preventDefault() - break - - case 9: // tab - case 13: // enter - - // We will handle creating tokens from autocomplete in autocomplete events - if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break - - // We will handle creating tokens from typeahead in typeahead events - if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break - if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break - - // Create token - if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { - return this.createTokensFromInput(e, this.$input.data('edit')); - } - - // Edit token - if (e.keyCode === 13) { - if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break - if (!_self.options.allowEditing) break - this.edit( this.$wrapper.find('.token.active') ) - } - } - - function leftRight(direction) { - if (_self.$input.is(document.activeElement)) { - if (_self.$input.val().length > 0) return - - direction += 'All' - var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') - if (!$token.length) return - - _self.preventInputFocus = true - _self.preventDeactivation = true - - _self.activate( $token ) - e.preventDefault() - - } else { - _self[direction]( e.shiftKey ) - e.preventDefault() - } - } - - function upDown(direction) { - if (!e.shiftKey) return - - if (_self.$input.is(document.activeElement)) { - if (_self.$input.val().length > 0) return - - var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') - if (!$token.length) return - - _self.activate( $token ) - } - - var opposite = direction === 'prev' ? 'next' : 'prev' - , position = direction === 'prev' ? 'first' : 'last' - - _self.$firstActiveToken[opposite + 'All']('.token').each(function() { - _self.deactivate( $(this) ) - }) - - _self.activate( _self.$wrapper.find('.token:' + position), true, true ) - e.preventDefault() - } - - this.lastKeyDown = e.keyCode - } - - , keypress: function(e) { - - // Comma - if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { - if (this.$input.val()) { - this.createTokensFromInput(e) - } - return false; - } - } - - , keyup: function (e) { - this.preventInputFocus = false - - if (!this.focused) return - - switch(e.keyCode) { - case 8: // backspace - if (this.$input.is(document.activeElement)) { - if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break - - this.preventDeactivation = true - var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') - - if (!$prevToken.length) break - - this.activate( $prevToken ) - } else { - this.remove(e) - } - break - - case 46: // delete - this.remove(e, 'next') - break - } - this.lastKeyUp = e.keyCode - } - - , focus: function (e) { - this.focused = true - this.$wrapper.addClass('focus') - - if (this.$input.is(document.activeElement)) { - this.$wrapper.find('.active').removeClass('active') - this.$firstActiveToken = null - - if (this.options.showAutocompleteOnFocus) { - this.search() - } - } - } - - , blur: function (e) { - - this.focused = false - this.$wrapper.removeClass('focus') - - if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { - this.$wrapper.find('.active').removeClass('active') - this.$firstActiveToken = null - } - - if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { - this.createTokensFromInput(e) - } - - this.preventDeactivation = false - this.preventCreateTokens = false - } - - , paste: function (e) { - var _self = this - - // Add tokens to existing ones - if (_self.options.allowPasting) { - setTimeout(function () { - _self.createTokensFromInput(e) - }, 1) - } - } - - , change: function (e) { - if ( e.initiator === 'tokenfield' ) return // Prevent loops - - this.setTokens( this.$element.val() ) - } - - , createTokensFromInput: function (e, focus) { - if (this.$input.val().length < this.options.minLength) - return // No input, simply return - - var tokensBefore = this.getTokensList() - this.setTokens( this.$input.val(), true ) - - if (tokensBefore == this.getTokensList() && this.$input.val().length) - return false // No tokens were added, do nothing (prevent form submit) - - this.setInput('') - - if (this.$input.data( 'edit' )) { - this.unedit(focus) - } - - return false // Prevent form being submitted - } - - , next: function (add) { - if (add) { - var $firstActiveToken = this.$wrapper.find('.active:first') - , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false - - if (deactivate) return this.deactivate( $firstActiveToken ) - } - - var $lastActiveToken = this.$wrapper.find('.active:last') - , $nextToken = $lastActiveToken.nextAll('.token:first') - - if (!$nextToken.length) { - this.$input.focus() - return - } - - this.activate($nextToken, add) - } - - , prev: function (add) { - - if (add) { - var $lastActiveToken = this.$wrapper.find('.active:last') - , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false - - if (deactivate) return this.deactivate( $lastActiveToken ) - } - - var $firstActiveToken = this.$wrapper.find('.active:first') - , $prevToken = $firstActiveToken.prevAll('.token:first') - - if (!$prevToken.length) { - $prevToken = this.$wrapper.find('.token:first') - } - - if (!$prevToken.length && !add) { - this.$input.focus() - return - } - - this.activate( $prevToken, add ) - } - - , activate: function ($token, add, multi, remember) { - - if (!$token) return - - if (typeof remember === 'undefined') var remember = true - - if (multi) var add = true - - this.$copyHelper.focus() - - if (!add) { - this.$wrapper.find('.active').removeClass('active') - if (remember) { - this.$firstActiveToken = $token - } else { - delete this.$firstActiveToken - } - } - - if (multi && this.$firstActiveToken) { - // Determine first active token and the current tokens indicies - // Account for the 1 hidden textarea by subtracting 1 from both - var i = this.$firstActiveToken.index() - 2 - , a = $token.index() - 2 - , _self = this - - this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { - _self.activate( $(this), true ) - }) - } - - $token.addClass('active') - this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() - } - - , activateAll: function() { - var _self = this - - this.$wrapper.find('.token').each( function (i) { - _self.activate($(this), i !== 0, false, false) - }) - } - - , deactivate: function($token) { - if (!$token) return - - $token.removeClass('active') - this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() - } - - , toggle: function($token) { - if (!$token) return - - $token.toggleClass('active') - this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() - } - - , edit: function ($token) { - if (!$token) return - - var attrs = $token.data('attrs') - - // Allow changing input value before editing - var options = { attrs: attrs, relatedTarget: $token.get(0) } - var editEvent = $.Event('tokenfield:edittoken', options) - this.$element.trigger( editEvent ) - - // Edit event can be cancelled if default is prevented - if (editEvent.isDefaultPrevented()) return - - $token.find('.token-label').text(attrs.value) - var tokenWidth = $token.outerWidth() - - var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input - - $token.replaceWith( $_input ) - - this.preventCreateTokens = true - - this.$input.val( attrs.value ) - .select() - .data( 'edit', true ) - .width( tokenWidth ) - - this.update(); - - // Indicate that token is now being edited, and is replaced with an input field in the DOM - this.$element.trigger($.Event('tokenfield:editedtoken', options )) - } - - , unedit: function (focus) { - var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input - $_input.appendTo( this.$wrapper ) - - this.$input.data('edit', false) - this.$mirror.text('') - - this.update() - - // Because moving the input element around in DOM - // will cause it to lose focus, we provide an option - // to re-focus the input after appending it to the wrapper - if (focus) { - var _self = this - setTimeout(function () { - _self.$input.focus() - }, 1) - } - } - - , remove: function (e, direction) { - if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return - - var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') - - if (e.type !== 'click') { - if (!direction) var direction = 'prev' - this[direction]() - - // Was it the first token? - if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 - } - - // Prepare events and their options - var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } - , removeEvent = $.Event('tokenfield:removetoken', options) - - this.$element.trigger(removeEvent); - - // Remove event can be intercepted and cancelled - if (removeEvent.isDefaultPrevented()) return - - var removedEvent = $.Event('tokenfield:removedtoken', options) - , changeEvent = $.Event('change', { initiator: 'tokenfield' }) - - // Remove token from DOM - $token.remove() - - // Trigger events - this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) - - // Focus, when necessary: - // When there are no more tokens, or if this was the first token - // and it was removed with backspace or it was clicked on - if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() - - // Adjust input width - this.$input.css('width', this.options.minWidth + 'px') - this.update() - - // Cancel original event handlers - e.preventDefault() - e.stopPropagation() - } - - /** - * Update tokenfield dimensions - */ - , update: function (e) { - var value = this.$input.val() - , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) - , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) - , inputPadding = inputPaddingLeft + inputPaddingRight - - if (this.$input.data('edit')) { - - if (!value) { - value = this.$input.prop("placeholder") - } - if (value === this.$mirror.text()) return - - this.$mirror.text(value) - - var mirrorWidth = this.$mirror.width() + 10; - if ( mirrorWidth > this.$wrapper.width() ) { - return this.$input.width( this.$wrapper.width() ) - } - - this.$input.width( mirrorWidth ) - } - else { - //temporary reset width to minimal value to get proper results - this.$input.width(this.options.minWidth); - - var w = (this.textDirection === 'rtl') - ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 - : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; - // - // some usecases pre-render widget before attaching to DOM, - // dimensions returned by jquery will be NaN -> we default to 100% - // so placeholder won't be cut off. - isNaN(w) ? this.$input.width('100%') : this.$input.width(w); - } - } - - , focusInput: function (e) { - if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return - // Focus only after the current call stack has cleared, - // otherwise has no effect. - // Reason: mousedown is too early - input will lose focus - // after mousedown. However, since the input may be moved - // in DOM, there may be no click or mouseup event triggered. - var _self = this - setTimeout(function() { - _self.$input.focus() - }, 0) - } - - , search: function () { - if ( this.$input.data('ui-autocomplete') ) { - this.$input.autocomplete('search') - } - } - - , disable: function () { - this.setProperty('disabled', true); - } - - , enable: function () { - this.setProperty('disabled', false); - } - - , readonly: function () { - this.setProperty('readonly', true); - } - - , writeable: function () { - this.setProperty('readonly', false); - } - - , setProperty: function(property, value) { - this['_' + property] = value; - this.$input.prop(property, value); - this.$element.prop(property, value); - this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); - } - - , destroy: function() { - // Set field value - this.$element.val( this.getTokensList() ); - // Restore styles and properties - this.$element.css( this.$element.data('original-styles') ); - this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); - - // Re-route tokenfield label to original input - var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) - if ( $label.length ) { - $label.prop( 'for', this.$element.prop('id') ) - } - - // Move original element outside of tokenfield wrapper - this.$element.insertBefore( this.$wrapper ); - - // Remove tokenfield-related data - this.$element.removeData('original-styles') - .removeData('original-tabindex') - .removeData('bs.tokenfield'); - - // Remove tokenfield from DOM - this.$wrapper.remove(); - this.$mirror.remove(); - - var $_element = this.$element; - - return $_element; - } - - } - - - /* TOKENFIELD PLUGIN DEFINITION - * ======================== */ - - var old = $.fn.tokenfield - - $.fn.tokenfield = function (option, param) { - var value - , args = [] - - Array.prototype.push.apply( args, arguments ); - - var elements = this.each(function () { - var $this = $(this) - , data = $this.data('bs.tokenfield') - , options = typeof option == 'object' && option - - if (typeof option === 'string' && data && data[option]) { - args.shift() - value = data[option].apply(data, args) - } else { - if (!data && typeof option !== 'string' && !param) { - $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) - $this.trigger('tokenfield:initialize') - } - } - }) - - return typeof value !== 'undefined' ? value : elements; - } - - $.fn.tokenfield.defaults = { - minWidth: 60, - minLength: 0, - html: true, - allowEditing: true, - allowPasting: true, - limit: 0, - autocomplete: {}, - typeahead: {}, - showAutocompleteOnFocus: false, - createTokensOnBlur: false, - delimiter: ',', - beautify: true, - inputType: 'text' - } - - $.fn.tokenfield.Constructor = Tokenfield - - - /* TOKENFIELD NO CONFLICT - * ================== */ - - $.fn.tokenfield.noConflict = function () { - $.fn.tokenfield = old - return this - } - - return Tokenfield; - -})); diff --git a/static/js/typeahead/LICENSE b/static/js/typeahead/LICENSE deleted file mode 100644 index 83817bac..00000000 --- a/static/js/typeahead/LICENSE +++ /dev/null @@ -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. diff --git a/static/js/typeahead/typeahead.js b/static/js/typeahead/typeahead.js deleted file mode 100644 index 14f97d68..00000000 --- a/static/js/typeahead/typeahead.js +++ /dev/null @@ -1,2451 +0,0 @@ -/*! - * typeahead.js 0.11.1 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT - */ - -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define("bloodhound", [ "jquery" ], function(a0) { - return root["Bloodhound"] = factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - root["Bloodhound"] = factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var VERSION = "0.11.1"; - var tokenizers = function() { - "use strict"; - return { - nonword: nonword, - whitespace: whitespace, - obj: { - nonword: getObjTokenizer(nonword), - whitespace: getObjTokenizer(whitespace) - } - }; - function whitespace(str) { - str = _.toStr(str); - return str ? str.split(/\s+/) : []; - } - function nonword(str) { - str = _.toStr(str); - return str ? str.split(/\W+/) : []; - } - function getObjTokenizer(tokenizer) { - return function setKey(keys) { - keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); - return function tokenize(o) { - var tokens = []; - _.each(keys, function(k) { - tokens = tokens.concat(tokenizer(_.toStr(o[k]))); - }); - return tokens; - }; - }; - } - }(); - var LruCache = function() { - "use strict"; - function LruCache(maxSize) { - this.maxSize = _.isNumber(maxSize) ? maxSize : 100; - this.reset(); - if (this.maxSize <= 0) { - this.set = this.get = $.noop; - } - } - _.mixin(LruCache.prototype, { - set: function set(key, val) { - var tailItem = this.list.tail, node; - if (this.size >= this.maxSize) { - this.list.remove(tailItem); - delete this.hash[tailItem.key]; - this.size--; - } - if (node = this.hash[key]) { - node.val = val; - this.list.moveToFront(node); - } else { - node = new Node(key, val); - this.list.add(node); - this.hash[key] = node; - this.size++; - } - }, - get: function get(key) { - var node = this.hash[key]; - if (node) { - this.list.moveToFront(node); - return node.val; - } - }, - reset: function reset() { - this.size = 0; - this.hash = {}; - this.list = new List(); - } - }); - function List() { - this.head = this.tail = null; - } - _.mixin(List.prototype, { - add: function add(node) { - if (this.head) { - node.next = this.head; - this.head.prev = node; - } - this.head = node; - this.tail = this.tail || node; - }, - remove: function remove(node) { - node.prev ? node.prev.next = node.next : this.head = node.next; - node.next ? node.next.prev = node.prev : this.tail = node.prev; - }, - moveToFront: function(node) { - this.remove(node); - this.add(node); - } - }); - function Node(key, val) { - this.key = key; - this.val = val; - this.prev = this.next = null; - } - return LruCache; - }(); - var PersistentStorage = function() { - "use strict"; - var LOCAL_STORAGE; - try { - LOCAL_STORAGE = window.localStorage; - LOCAL_STORAGE.setItem("~~~", "!"); - LOCAL_STORAGE.removeItem("~~~"); - } catch (err) { - LOCAL_STORAGE = null; - } - function PersistentStorage(namespace, override) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); - this.ls = override || LOCAL_STORAGE; - !this.ls && this._noop(); - } - _.mixin(PersistentStorage.prototype, { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - _noop: function() { - this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; - }, - _safeSet: function(key, val) { - try { - this.ls.setItem(key, val); - } catch (err) { - if (err.name === "QuotaExceededError") { - this.clear(); - this._noop(); - } - } - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(this.ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (_.isNumber(ttl)) { - this._safeSet(this._ttlKey(key), encode(now() + ttl)); - } else { - this.ls.removeItem(this._ttlKey(key)); - } - return this._safeSet(this._prefix(key), encode(val)); - }, - remove: function(key) { - this.ls.removeItem(this._ttlKey(key)); - this.ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, keys = gatherMatchingKeys(this.keyMatcher); - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(this.ls.getItem(this._ttlKey(key))); - return _.isNumber(ttl) && now() > ttl ? true : false; - } - }); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(_.isUndefined(val) ? null : val); - } - function decode(val) { - return $.parseJSON(val); - } - function gatherMatchingKeys(keyMatcher) { - var i, key, keys = [], len = LOCAL_STORAGE.length; - for (i = 0; i < len; i++) { - if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { - keys.push(key.replace(keyMatcher, "")); - } - } - return keys; - } - }(); - var Transport = function() { - "use strict"; - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); - function Transport(o) { - o = o || {}; - this.cancelled = false; - this.lastReq = null; - this._send = o.transport; - this._get = o.limiter ? o.limiter(this._get) : this._get; - this._cache = o.cache === false ? new LruCache(0) : sharedCache; - } - Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { - maxPendingRequests = num; - }; - Transport.resetCache = function resetCache() { - sharedCache.reset(); - }; - _.mixin(Transport.prototype, { - _fingerprint: function fingerprint(o) { - o = o || {}; - return o.url + o.type + $.param(o.data || {}); - }, - _get: function(o, cb) { - var that = this, fingerprint, jqXhr; - fingerprint = this._fingerprint(o); - if (this.cancelled || fingerprint !== this.lastReq) { - return; - } - if (jqXhr = pendingRequests[fingerprint]) { - jqXhr.done(done).fail(fail); - } else if (pendingRequestsCount < maxPendingRequests) { - pendingRequestsCount++; - pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - cb(null, resp); - that._cache.set(fingerprint, resp); - } - function fail() { - cb(true); - } - function always() { - pendingRequestsCount--; - delete pendingRequests[fingerprint]; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(o, cb) { - var resp, fingerprint; - cb = cb || $.noop; - o = _.isString(o) ? { - url: o - } : o || {}; - fingerprint = this._fingerprint(o); - this.cancelled = false; - this.lastReq = fingerprint; - if (resp = this._cache.get(fingerprint)) { - cb(null, resp); - } else { - this._get(o, cb); - } - }, - cancel: function() { - this.cancelled = true; - } - }); - return Transport; - }(); - var SearchIndex = window.SearchIndex = function() { - "use strict"; - var CHILDREN = "c", IDS = "i"; - function SearchIndex(o) { - o = o || {}; - if (!o.datumTokenizer || !o.queryTokenizer) { - $.error("datumTokenizer and queryTokenizer are both required"); - } - this.identify = o.identify || _.stringify; - this.datumTokenizer = o.datumTokenizer; - this.queryTokenizer = o.queryTokenizer; - this.reset(); - } - _.mixin(SearchIndex.prototype, { - bootstrap: function bootstrap(o) { - this.datums = o.datums; - this.trie = o.trie; - }, - add: function(data) { - var that = this; - data = _.isArray(data) ? data : [ data ]; - _.each(data, function(datum) { - var id, tokens; - that.datums[id = that.identify(datum)] = datum; - tokens = normalizeTokens(that.datumTokenizer(datum)); - _.each(tokens, function(token) { - var node, chars, ch; - node = that.trie; - chars = token.split(""); - while (ch = chars.shift()) { - node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); - node[IDS].push(id); - } - }); - }); - }, - get: function get(ids) { - var that = this; - return _.map(ids, function(id) { - return that.datums[id]; - }); - }, - search: function search(query) { - var that = this, tokens, matches; - tokens = normalizeTokens(this.queryTokenizer(query)); - _.each(tokens, function(token) { - var node, chars, ch, ids; - if (matches && matches.length === 0) { - return false; - } - node = that.trie; - chars = token.split(""); - while (node && (ch = chars.shift())) { - node = node[CHILDREN][ch]; - } - if (node && chars.length === 0) { - ids = node[IDS].slice(0); - matches = matches ? getIntersection(matches, ids) : ids; - } else { - matches = []; - return false; - } - }); - return matches ? _.map(unique(matches), function(id) { - return that.datums[id]; - }) : []; - }, - all: function all() { - var values = []; - for (var key in this.datums) { - values.push(this.datums[key]); - } - return values; - }, - reset: function reset() { - this.datums = {}; - this.trie = newNode(); - }, - serialize: function serialize() { - return { - datums: this.datums, - trie: this.trie - }; - } - }); - return SearchIndex; - function normalizeTokens(tokens) { - tokens = _.filter(tokens, function(token) { - return !!token; - }); - tokens = _.map(tokens, function(token) { - return token.toLowerCase(); - }); - return tokens; - } - function newNode() { - var node = {}; - node[IDS] = []; - node[CHILDREN] = {}; - return node; - } - function unique(array) { - var seen = {}, uniques = []; - for (var i = 0, len = array.length; i < len; i++) { - if (!seen[array[i]]) { - seen[array[i]] = true; - uniques.push(array[i]); - } - } - return uniques; - } - function getIntersection(arrayA, arrayB) { - var ai = 0, bi = 0, intersection = []; - arrayA = arrayA.sort(); - arrayB = arrayB.sort(); - var lenArrayA = arrayA.length, lenArrayB = arrayB.length; - while (ai < lenArrayA && bi < lenArrayB) { - if (arrayA[ai] < arrayB[bi]) { - ai++; - } else if (arrayA[ai] > arrayB[bi]) { - bi++; - } else { - intersection.push(arrayA[ai]); - ai++; - bi++; - } - } - return intersection; - } - }(); - var Prefetch = function() { - "use strict"; - var keys; - keys = { - data: "data", - protocol: "protocol", - thumbprint: "thumbprint" - }; - function Prefetch(o) { - this.url = o.url; - this.ttl = o.ttl; - this.cache = o.cache; - this.prepare = o.prepare; - this.transform = o.transform; - this.transport = o.transport; - this.thumbprint = o.thumbprint; - this.storage = new PersistentStorage(o.cacheKey); - } - _.mixin(Prefetch.prototype, { - _settings: function settings() { - return { - url: this.url, - type: "GET", - dataType: "json" - }; - }, - store: function store(data) { - if (!this.cache) { - return; - } - this.storage.set(keys.data, data, this.ttl); - this.storage.set(keys.protocol, location.protocol, this.ttl); - this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); - }, - fromCache: function fromCache() { - var stored = {}, isExpired; - if (!this.cache) { - return null; - } - stored.data = this.storage.get(keys.data); - stored.protocol = this.storage.get(keys.protocol); - stored.thumbprint = this.storage.get(keys.thumbprint); - isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; - return stored.data && !isExpired ? stored.data : null; - }, - fromNetwork: function(cb) { - var that = this, settings; - if (!cb) { - return; - } - settings = this.prepare(this._settings()); - this.transport(settings).fail(onError).done(onResponse); - function onError() { - cb(true); - } - function onResponse(resp) { - cb(null, that.transform(resp)); - } - }, - clear: function clear() { - this.storage.clear(); - return this; - } - }); - return Prefetch; - }(); - var Remote = function() { - "use strict"; - function Remote(o) { - this.url = o.url; - this.prepare = o.prepare; - this.transform = o.transform; - this.transport = new Transport({ - cache: o.cache, - limiter: o.limiter, - transport: o.transport - }); - } - _.mixin(Remote.prototype, { - _settings: function settings() { - return { - url: this.url, - type: "GET", - dataType: "json" - }; - }, - get: function get(query, cb) { - var that = this, settings; - if (!cb) { - return; - } - query = query || ""; - settings = this.prepare(query, this._settings()); - return this.transport.get(settings, onResponse); - function onResponse(err, resp) { - err ? cb([]) : cb(that.transform(resp)); - } - }, - cancelLastRequest: function cancelLastRequest() { - this.transport.cancel(); - } - }); - return Remote; - }(); - var oParser = function() { - "use strict"; - return function parse(o) { - var defaults, sorter; - defaults = { - initialize: true, - identify: _.stringify, - datumTokenizer: null, - queryTokenizer: null, - sufficient: 5, - sorter: null, - local: [], - prefetch: null, - remote: null - }; - o = _.mixin(defaults, o || {}); - !o.datumTokenizer && $.error("datumTokenizer is required"); - !o.queryTokenizer && $.error("queryTokenizer is required"); - sorter = o.sorter; - o.sorter = sorter ? function(x) { - return x.sort(sorter); - } : _.identity; - o.local = _.isFunction(o.local) ? o.local() : o.local; - o.prefetch = parsePrefetch(o.prefetch); - o.remote = parseRemote(o.remote); - return o; - }; - function parsePrefetch(o) { - var defaults; - if (!o) { - return null; - } - defaults = { - url: null, - ttl: 24 * 60 * 60 * 1e3, - cache: true, - cacheKey: null, - thumbprint: "", - prepare: _.identity, - transform: _.identity, - transport: null - }; - o = _.isString(o) ? { - url: o - } : o; - o = _.mixin(defaults, o); - !o.url && $.error("prefetch requires url to be set"); - o.transform = o.filter || o.transform; - o.cacheKey = o.cacheKey || o.url; - o.thumbprint = VERSION + o.thumbprint; - o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; - return o; - } - function parseRemote(o) { - var defaults; - if (!o) { - return; - } - defaults = { - url: null, - cache: true, - prepare: null, - replace: null, - wildcard: null, - limiter: null, - rateLimitBy: "debounce", - rateLimitWait: 300, - transform: _.identity, - transport: null - }; - o = _.isString(o) ? { - url: o - } : o; - o = _.mixin(defaults, o); - !o.url && $.error("remote requires url to be set"); - o.transform = o.filter || o.transform; - o.prepare = toRemotePrepare(o); - o.limiter = toLimiter(o); - o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; - delete o.replace; - delete o.wildcard; - delete o.rateLimitBy; - delete o.rateLimitWait; - return o; - } - function toRemotePrepare(o) { - var prepare, replace, wildcard; - prepare = o.prepare; - replace = o.replace; - wildcard = o.wildcard; - if (prepare) { - return prepare; - } - if (replace) { - prepare = prepareByReplace; - } else if (o.wildcard) { - prepare = prepareByWildcard; - } else { - prepare = idenityPrepare; - } - return prepare; - function prepareByReplace(query, settings) { - settings.url = replace(settings.url, query); - return settings; - } - function prepareByWildcard(query, settings) { - settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); - return settings; - } - function idenityPrepare(query, settings) { - return settings; - } - } - function toLimiter(o) { - var limiter, method, wait; - limiter = o.limiter; - method = o.rateLimitBy; - wait = o.rateLimitWait; - if (!limiter) { - limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); - } - return limiter; - function debounce(wait) { - return function debounce(fn) { - return _.debounce(fn, wait); - }; - } - function throttle(wait) { - return function throttle(fn) { - return _.throttle(fn, wait); - }; - } - } - function callbackToDeferred(fn) { - return function wrapper(o) { - var deferred = $.Deferred(); - fn(o, onSuccess, onError); - return deferred; - function onSuccess(resp) { - _.defer(function() { - deferred.resolve(resp); - }); - } - function onError(err) { - _.defer(function() { - deferred.reject(err); - }); - } - }; - } - }(); - var Bloodhound = function() { - "use strict"; - var old; - old = window && window.Bloodhound; - function Bloodhound(o) { - o = oParser(o); - this.sorter = o.sorter; - this.identify = o.identify; - this.sufficient = o.sufficient; - this.local = o.local; - this.remote = o.remote ? new Remote(o.remote) : null; - this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; - this.index = new SearchIndex({ - identify: this.identify, - datumTokenizer: o.datumTokenizer, - queryTokenizer: o.queryTokenizer - }); - o.initialize !== false && this.initialize(); - } - Bloodhound.noConflict = function noConflict() { - window && (window.Bloodhound = old); - return Bloodhound; - }; - Bloodhound.tokenizers = tokenizers; - _.mixin(Bloodhound.prototype, { - __ttAdapter: function ttAdapter() { - var that = this; - return this.remote ? withAsync : withoutAsync; - function withAsync(query, sync, async) { - return that.search(query, sync, async); - } - function withoutAsync(query, sync) { - return that.search(query, sync); - } - }, - _loadPrefetch: function loadPrefetch() { - var that = this, deferred, serialized; - deferred = $.Deferred(); - if (!this.prefetch) { - deferred.resolve(); - } else if (serialized = this.prefetch.fromCache()) { - this.index.bootstrap(serialized); - deferred.resolve(); - } else { - this.prefetch.fromNetwork(done); - } - return deferred.promise(); - function done(err, data) { - if (err) { - return deferred.reject(); - } - that.add(data); - that.prefetch.store(that.index.serialize()); - deferred.resolve(); - } - }, - _initialize: function initialize() { - var that = this, deferred; - this.clear(); - (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); - return this.initPromise; - function addLocalToIndex() { - that.add(that.local); - } - }, - initialize: function initialize(force) { - return !this.initPromise || force ? this._initialize() : this.initPromise; - }, - add: function add(data) { - this.index.add(data); - return this; - }, - get: function get(ids) { - ids = _.isArray(ids) ? ids : [].slice.call(arguments); - return this.index.get(ids); - }, - search: function search(query, sync, async) { - var that = this, local; - local = this.sorter(this.index.search(query)); - sync(this.remote ? local.slice() : local); - if (this.remote && local.length < this.sufficient) { - this.remote.get(query, processRemote); - } else if (this.remote) { - this.remote.cancelLastRequest(); - } - return this; - function processRemote(remote) { - var nonDuplicates = []; - _.each(remote, function(r) { - !_.some(local, function(l) { - return that.identify(r) === that.identify(l); - }) && nonDuplicates.push(r); - }); - async && async(nonDuplicates); - } - }, - all: function all() { - return this.index.all(); - }, - clear: function clear() { - this.index.reset(); - return this; - }, - clearPrefetchCache: function clearPrefetchCache() { - this.prefetch && this.prefetch.clear(); - return this; - }, - clearRemoteCache: function clearRemoteCache() { - Transport.resetCache(); - return this; - }, - ttAdapter: function ttAdapter() { - return this.__ttAdapter(); - } - }); - return Bloodhound; - }(); - return Bloodhound; -}); - -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define("typeahead.js", [ "jquery" ], function(a0) { - return factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var WWW = function() { - "use strict"; - var defaultClassNames = { - wrapper: "twitter-typeahead", - input: "tt-input", - hint: "tt-hint", - menu: "tt-menu", - dataset: "tt-dataset", - suggestion: "tt-suggestion", - selectable: "tt-selectable", - empty: "tt-empty", - open: "tt-open", - cursor: "tt-cursor", - highlight: "tt-highlight" - }; - return build; - function build(o) { - var www, classes; - classes = _.mixin({}, defaultClassNames, o); - www = { - css: buildCss(), - classes: classes, - html: buildHtml(classes), - selectors: buildSelectors(classes) - }; - return { - css: www.css, - html: www.html, - classes: www.classes, - selectors: www.selectors, - mixin: function(o) { - _.mixin(o, www); - } - }; - } - function buildHtml(c) { - return { - wrapper: '', - menu: '
' - }; - } - function buildSelectors(classes) { - var selectors = {}; - _.each(classes, function(v, k) { - selectors[k] = "." + v; - }); - return selectors; - } - function buildCss() { - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none", - opacity: "1" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - menu: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } - }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - return css; - } - }(); - var EventBus = function() { - "use strict"; - var namespace, deprecationMap; - namespace = "typeahead:"; - deprecationMap = { - render: "rendered", - cursorchange: "cursorchanged", - select: "selected", - autocomplete: "autocompleted" - }; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); - } - this.$el = $(o.el); - } - _.mixin(EventBus.prototype, { - _trigger: function(type, args) { - var $e; - $e = $.Event(namespace + type); - (args = args || []).unshift($e); - this.$el.trigger.apply(this.$el, args); - return $e; - }, - before: function(type) { - var args, $e; - args = [].slice.call(arguments, 1); - $e = this._trigger("before" + type, args); - return $e.isDefaultPrevented(); - }, - trigger: function(type) { - var deprecatedType; - this._trigger(type, [].slice.call(arguments, 1)); - if (deprecatedType = deprecationMap[type]) { - this._trigger(deprecatedType, [].slice.call(arguments, 1)); - } - } - }); - return EventBus; - }(); - var EventEmitter = function() { - "use strict"; - var splitter = /\s+/, nextTick = getNextTick(); - return { - onSync: onSync, - onAsync: onAsync, - off: off, - trigger: trigger - }; - function on(method, types, cb, context) { - var type; - if (!cb) { - return this; - } - types = types.split(splitter); - cb = context ? bindContext(cb, context) : cb; - this._callbacks = this._callbacks || {}; - while (type = types.shift()) { - this._callbacks[type] = this._callbacks[type] || { - sync: [], - async: [] - }; - this._callbacks[type][method].push(cb); - } - return this; - } - function onAsync(types, cb, context) { - return on.call(this, "async", types, cb, context); - } - function onSync(types, cb, context) { - return on.call(this, "sync", types, cb, context); - } - function off(types) { - var type; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - while (type = types.shift()) { - delete this._callbacks[type]; - } - return this; - } - function trigger(types) { - var type, callbacks, args, syncFlush, asyncFlush; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - args = [].slice.call(arguments, 1); - while ((type = types.shift()) && (callbacks = this._callbacks[type])) { - syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); - asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); - syncFlush() && nextTick(asyncFlush); - } - return this; - } - function getFlush(callbacks, context, args) { - return flush; - function flush() { - var cancelled; - for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { - cancelled = callbacks[i].apply(context, args) === false; - } - return !cancelled; - } - } - function getNextTick() { - var nextTickFn; - if (window.setImmediate) { - nextTickFn = function nextTickSetImmediate(fn) { - setImmediate(function() { - fn(); - }); - }; - } else { - nextTickFn = function nextTickSetTimeout(fn) { - setTimeout(function() { - fn(); - }, 0); - }; - } - return nextTickFn; - } - function bindContext(fn, context) { - return fn.bind ? fn.bind(context) : function() { - fn.apply(context, [].slice.call(arguments, 0)); - }; - } - }(); - var highlight = function(doc) { - "use strict"; - var defaults = { - node: null, - pattern: null, - tagName: "strong", - className: null, - wordsOnly: false, - caseSensitive: false - }; - return function hightlight(o) { - var regex; - o = _.mixin({}, defaults, o); - if (!o.node || !o.pattern) { - return; - } - o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); - traverse(o.node, hightlightTextNode); - function hightlightTextNode(textNode) { - var match, patternNode, wrapperNode; - if (match = regex.exec(textNode.data)) { - wrapperNode = doc.createElement(o.tagName); - o.className && (wrapperNode.className = o.className); - patternNode = textNode.splitText(match.index); - patternNode.splitText(match[0].length); - wrapperNode.appendChild(patternNode.cloneNode(true)); - textNode.parentNode.replaceChild(wrapperNode, patternNode); - } - return !!match; - } - function traverse(el, hightlightTextNode) { - var childNode, TEXT_NODE_TYPE = 3; - for (var i = 0; i < el.childNodes.length; i++) { - childNode = el.childNodes[i]; - if (childNode.nodeType === TEXT_NODE_TYPE) { - i += hightlightTextNode(childNode) ? 1 : 0; - } else { - traverse(childNode, hightlightTextNode); - } - } - } - }; - function getRegex(patterns, caseSensitive, wordsOnly) { - var escapedPatterns = [], regexStr; - for (var i = 0, len = patterns.length; i < len; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); - } - regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; - return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); - } - }(window.document); - var Input = function() { - "use strict"; - var specialKeyCodeMap; - specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - function Input(o, www) { - o = o || {}; - if (!o.input) { - $.error("input is missing"); - } - www.mixin(this); - this.$hint = $(o.hint); - this.$input = $(o.input); - this.query = this.$input.val(); - this.queryWhenFocused = this.hasFocus() ? this.query : null; - this.$overflowHelper = buildOverflowHelper(this.$input); - this._checkLanguageDirection(); - if (this.$hint.length === 0) { - this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; - } - } - Input.normalizeQuery = function(str) { - return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - }; - _.mixin(Input.prototype, EventEmitter, { - _onBlur: function onBlur() { - this.resetInputValue(); - this.trigger("blurred"); - }, - _onFocus: function onFocus() { - this.queryWhenFocused = this.query; - this.trigger("focused"); - }, - _onKeydown: function onKeydown($e) { - var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; - this._managePreventDefault(keyName, $e); - if (keyName && this._shouldTrigger(keyName, $e)) { - this.trigger(keyName + "Keyed", $e); - } - }, - _onInput: function onInput() { - this._setQuery(this.getInputValue()); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); - }, - _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault; - switch (keyName) { - case "up": - case "down": - preventDefault = !withModifier($e); - break; - - default: - preventDefault = false; - } - preventDefault && $e.preventDefault(); - }, - _shouldTrigger: function shouldTrigger(keyName, $e) { - var trigger; - switch (keyName) { - case "tab": - trigger = !withModifier($e); - break; - - default: - trigger = true; - } - return trigger; - }, - _checkLanguageDirection: function checkLanguageDirection() { - var dir = (this.$input.css("direction") || "ltr").toLowerCase(); - if (this.dir !== dir) { - this.dir = dir; - this.$hint.attr("dir", dir); - this.trigger("langDirChanged", dir); - } - }, - _setQuery: function setQuery(val, silent) { - var areEquivalent, hasDifferentWhitespace; - areEquivalent = areQueriesEquivalent(val, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; - this.query = val; - if (!silent && !areEquivalent) { - this.trigger("queryChanged", this.query); - } else if (!silent && hasDifferentWhitespace) { - this.trigger("whitespaceChanged", this.query); - } - }, - bind: function() { - var that = this, onBlur, onFocus, onKeydown, onInput; - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); - this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); - if (!_.isMsie() || _.isMsie() > 9) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); - }); - } - return this; - }, - focus: function focus() { - this.$input.focus(); - }, - blur: function blur() { - this.$input.blur(); - }, - getLangDir: function getLangDir() { - return this.dir; - }, - getQuery: function getQuery() { - return this.query || ""; - }, - setQuery: function setQuery(val, silent) { - this.setInputValue(val); - this._setQuery(val, silent); - }, - hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { - return this.query !== this.queryWhenFocused; - }, - getInputValue: function getInputValue() { - return this.$input.val(); - }, - setInputValue: function setInputValue(value) { - this.$input.val(value); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); - }, - resetInputValue: function resetInputValue() { - this.setInputValue(this.query); - }, - getHint: function getHint() { - return this.$hint.val(); - }, - setHint: function setHint(value) { - this.$hint.val(value); - }, - clearHint: function clearHint() { - this.setHint(""); - }, - clearHintIfInvalid: function clearHintIfInvalid() { - var val, hint, valIsPrefixOfHint, isValid; - val = this.getInputValue(); - hint = this.getHint(); - valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; - isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); - !isValid && this.clearHint(); - }, - hasFocus: function hasFocus() { - return this.$input.is(":focus"); - }, - hasOverflow: function hasOverflow() { - var constraint = this.$input.width() - 2; - this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() >= constraint; - }, - isCursorAtEnd: function() { - var valueLength, selectionStart, range; - valueLength = this.$input.val().length; - selectionStart = this.$input[0].selectionStart; - if (_.isNumber(selectionStart)) { - return selectionStart === valueLength; - } else if (document.selection) { - range = document.selection.createRange(); - range.moveStart("character", -valueLength); - return valueLength === range.text.length; - } - return true; - }, - destroy: function destroy() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$overflowHelper.remove(); - this.$hint = this.$input = this.$overflowHelper = $("
"); - } - }); - return Input; - function buildOverflowHelper($input) { - return $('').css({ - position: "absolute", - visibility: "hidden", - whiteSpace: "pre", - fontFamily: $input.css("font-family"), - fontSize: $input.css("font-size"), - fontStyle: $input.css("font-style"), - fontVariant: $input.css("font-variant"), - fontWeight: $input.css("font-weight"), - wordSpacing: $input.css("word-spacing"), - letterSpacing: $input.css("letter-spacing"), - textIndent: $input.css("text-indent"), - textRendering: $input.css("text-rendering"), - textTransform: $input.css("text-transform") - }).insertAfter($input); - } - function areQueriesEquivalent(a, b) { - return Input.normalizeQuery(a) === Input.normalizeQuery(b); - } - function withModifier($e) { - return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; - } - }(); - var Dataset = function() { - "use strict"; - var keys, nameGenerator; - keys = { - val: "tt-selectable-display", - obj: "tt-selectable-object" - }; - nameGenerator = _.getIdGenerator(); - function Dataset(o, www) { - o = o || {}; - o.templates = o.templates || {}; - o.templates.notFound = o.templates.notFound || o.templates.empty; - if (!o.source) { - $.error("missing source"); - } - if (!o.node) { - $.error("missing node"); - } - if (o.name && !isValidName(o.name)) { - $.error("invalid dataset name: " + o.name); - } - www.mixin(this); - this.highlight = !!o.highlight; - this.name = o.name || nameGenerator(); - this.limit = o.limit || 5; - this.displayFn = getDisplayFn(o.display || o.displayKey); - this.templates = getTemplates(o.templates, this.displayFn); - this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; - this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; - this._resetLastSuggestion(); - this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); - } - Dataset.extractData = function extractData(el) { - var $el = $(el); - if ($el.data(keys.obj)) { - return { - val: $el.data(keys.val) || "", - obj: $el.data(keys.obj) || null - }; - } - return null; - }; - _.mixin(Dataset.prototype, EventEmitter, { - _overwrite: function overwrite(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (this.async && this.templates.pending) { - this._renderPending(query); - } else if (!this.async && this.templates.notFound) { - this._renderNotFound(query); - } else { - this._empty(); - } - this.trigger("rendered", this.name, suggestions, false); - }, - _append: function append(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length && this.$lastSuggestion.length) { - this._appendSuggestions(query, suggestions); - } else if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (!this.$lastSuggestion.length && this.templates.notFound) { - this._renderNotFound(query); - } - this.trigger("rendered", this.name, suggestions, true); - }, - _renderSuggestions: function renderSuggestions(query, suggestions) { - var $fragment; - $fragment = this._getSuggestionsFragment(query, suggestions); - this.$lastSuggestion = $fragment.children().last(); - this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); - }, - _appendSuggestions: function appendSuggestions(query, suggestions) { - var $fragment, $lastSuggestion; - $fragment = this._getSuggestionsFragment(query, suggestions); - $lastSuggestion = $fragment.children().last(); - this.$lastSuggestion.after($fragment); - this.$lastSuggestion = $lastSuggestion; - }, - _renderPending: function renderPending(query) { - var template = this.templates.pending; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); - }, - _renderNotFound: function renderNotFound(query) { - var template = this.templates.notFound; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); - }, - _empty: function empty() { - this.$el.empty(); - this._resetLastSuggestion(); - }, - _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { - var that = this, fragment; - fragment = document.createDocumentFragment(); - _.each(suggestions, function getSuggestionNode(suggestion) { - var $el, context; - context = that._injectQuery(query, suggestion); - $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); - fragment.appendChild($el[0]); - }); - this.highlight && highlight({ - className: this.classes.highlight, - node: fragment, - pattern: query - }); - return $(fragment); - }, - _getFooter: function getFooter(query, suggestions) { - return this.templates.footer ? this.templates.footer({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _getHeader: function getHeader(query, suggestions) { - return this.templates.header ? this.templates.header({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _resetLastSuggestion: function resetLastSuggestion() { - this.$lastSuggestion = $(); - }, - _injectQuery: function injectQuery(query, obj) { - return _.isObject(obj) ? _.mixin({ - _query: query - }, obj) : obj; - }, - update: function update(query) { - var that = this, canceled = false, syncCalled = false, rendered = 0; - this.cancel(); - this.cancel = function cancel() { - canceled = true; - that.cancel = $.noop; - that.async && that.trigger("asyncCanceled", query); - }; - this.source(query, sync, async); - !syncCalled && sync([]); - function sync(suggestions) { - if (syncCalled) { - return; - } - syncCalled = true; - suggestions = (suggestions || []).slice(0, that.limit); - rendered = suggestions.length; - that._overwrite(query, suggestions); - if (rendered < that.limit && that.async) { - that.trigger("asyncRequested", query); - } - } - function async(suggestions) { - suggestions = suggestions || []; - if (!canceled && rendered < that.limit) { - that.cancel = $.noop; - rendered += suggestions.length; - that._append(query, suggestions.slice(0, that.limit - rendered)); - that.async && that.trigger("asyncReceived", query); - } - } - }, - cancel: $.noop, - clear: function clear() { - this._empty(); - this.cancel(); - this.trigger("cleared"); - }, - isEmpty: function isEmpty() { - return this.$el.is(":empty"); - }, - destroy: function destroy() { - this.$el = $("
"); - } - }); - return Dataset; - function getDisplayFn(display) { - display = display || _.stringify; - return _.isFunction(display) ? display : displayFn; - function displayFn(obj) { - return obj[display]; - } - } - function getTemplates(templates, displayFn) { - return { - notFound: templates.notFound && _.templatify(templates.notFound), - pending: templates.pending && _.templatify(templates.pending), - header: templates.header && _.templatify(templates.header), - footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate - }; - function suggestionTemplate(context) { - return $("
").text(displayFn(context)); - } - } - function isValidName(str) { - return /^[_a-zA-Z0-9-]+$/.test(str); - } - }(); - var Menu = function() { - "use strict"; - function Menu(o, www) { - var that = this; - o = o || {}; - if (!o.node) { - $.error("node is required"); - } - www.mixin(this); - this.$node = $(o.node); - this.query = null; - this.datasets = _.map(o.datasets, initializeDataset); - function initializeDataset(oDataset) { - var node = that.$node.find(oDataset.node).first(); - oDataset.node = node.length ? node : $("
").appendTo(that.$node); - return new Dataset(oDataset, www); - } - } - _.mixin(Menu.prototype, EventEmitter, { - _onSelectableClick: function onSelectableClick($e) { - this.trigger("selectableClicked", $($e.currentTarget)); - }, - _onRendered: function onRendered(type, dataset, suggestions, async) { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetRendered", dataset, suggestions, async); - }, - _onCleared: function onCleared() { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetCleared"); - }, - _propagate: function propagate() { - this.trigger.apply(this, arguments); - }, - _allDatasetsEmpty: function allDatasetsEmpty() { - return _.every(this.datasets, isDatasetEmpty); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } - }, - _getSelectables: function getSelectables() { - return this.$node.find(this.selectors.selectable); - }, - _removeCursor: function _removeCursor() { - var $selectable = this.getActiveSelectable(); - $selectable && $selectable.removeClass(this.classes.cursor); - }, - _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, nodeScrollTop, nodeHeight; - elTop = $el.position().top; - elBottom = elTop + $el.outerHeight(true); - nodeScrollTop = this.$node.scrollTop(); - nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); - if (elTop < 0) { - this.$node.scrollTop(nodeScrollTop + elTop); - } else if (nodeHeight < elBottom) { - this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); - } - }, - bind: function() { - var that = this, onSelectableClick; - onSelectableClick = _.bind(this._onSelectableClick, this); - this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); - _.each(this.datasets, function(dataset) { - dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); - }); - return this; - }, - isOpen: function isOpen() { - return this.$node.hasClass(this.classes.open); - }, - open: function open() { - this.$node.addClass(this.classes.open); - }, - close: function close() { - this.$node.removeClass(this.classes.open); - this._removeCursor(); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.attr("dir", dir); - }, - selectableRelativeToCursor: function selectableRelativeToCursor(delta) { - var $selectables, $oldCursor, oldIndex, newIndex; - $oldCursor = this.getActiveSelectable(); - $selectables = this._getSelectables(); - oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; - newIndex = oldIndex + delta; - newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; - newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; - return newIndex === -1 ? null : $selectables.eq(newIndex); - }, - setCursor: function setCursor($selectable) { - this._removeCursor(); - if ($selectable = $selectable && $selectable.first()) { - $selectable.addClass(this.classes.cursor); - this._ensureVisible($selectable); - } - }, - getSelectableData: function getSelectableData($el) { - return $el && $el.length ? Dataset.extractData($el) : null; - }, - getActiveSelectable: function getActiveSelectable() { - var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); - return $selectable.length ? $selectable : null; - }, - getTopSelectable: function getTopSelectable() { - var $selectable = this._getSelectables().first(); - return $selectable.length ? $selectable : null; - }, - update: function update(query) { - var isValidUpdate = query !== this.query; - if (isValidUpdate) { - this.query = query; - _.each(this.datasets, updateDataset); - } - return isValidUpdate; - function updateDataset(dataset) { - dataset.update(query); - } - }, - empty: function empty() { - _.each(this.datasets, clearDataset); - this.query = null; - this.$node.addClass(this.classes.empty); - function clearDataset(dataset) { - dataset.clear(); - } - }, - destroy: function destroy() { - this.$node.off(".tt"); - this.$node = $("
"); - _.each(this.datasets, destroyDataset); - function destroyDataset(dataset) { - dataset.destroy(); - } - } - }); - return Menu; - }(); - var DefaultMenu = function() { - "use strict"; - var s = Menu.prototype; - function DefaultMenu() { - Menu.apply(this, [].slice.call(arguments, 0)); - } - _.mixin(DefaultMenu.prototype, Menu.prototype, { - open: function open() { - !this._allDatasetsEmpty() && this._show(); - return s.open.apply(this, [].slice.call(arguments, 0)); - }, - close: function close() { - this._hide(); - return s.close.apply(this, [].slice.call(arguments, 0)); - }, - _onRendered: function onRendered() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onRendered.apply(this, [].slice.call(arguments, 0)); - }, - _onCleared: function onCleared() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onCleared.apply(this, [].slice.call(arguments, 0)); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); - return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); - }, - _hide: function hide() { - this.$node.hide(); - }, - _show: function show() { - this.$node.css("display", "block"); - } - }); - return DefaultMenu; - }(); - var Typeahead = function() { - "use strict"; - function Typeahead(o, www) { - var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; - o = o || {}; - if (!o.input) { - $.error("missing input"); - } - if (!o.menu) { - $.error("missing menu"); - } - if (!o.eventBus) { - $.error("missing event bus"); - } - www.mixin(this); - this.eventBus = o.eventBus; - this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.input = o.input; - this.menu = o.menu; - this.enabled = true; - this.active = false; - this.input.hasFocus() && this.activate(); - this.dir = this.input.getLangDir(); - this._hacks(); - this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); - onFocused = c(this, "activate", "open", "_onFocused"); - onBlurred = c(this, "deactivate", "_onBlurred"); - onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); - onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); - onEscKeyed = c(this, "isActive", "_onEscKeyed"); - onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); - onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); - onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); - onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); - onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); - onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); - this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); - } - _.mixin(Typeahead.prototype, { - _hacks: function hacks() { - var $input, $menu; - $input = this.input.$input || $("
"); - $menu = this.menu.$node || $("
"); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - }, - _onSelectableClicked: function onSelectableClicked(type, $el) { - this.select($el); - }, - _onDatasetCleared: function onDatasetCleared() { - this._updateHint(); - }, - _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { - this._updateHint(); - this.eventBus.trigger("render", suggestions, async, dataset); - }, - _onAsyncRequested: function onAsyncRequested(type, dataset, query) { - this.eventBus.trigger("asyncrequest", query, dataset); - }, - _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { - this.eventBus.trigger("asynccancel", query, dataset); - }, - _onAsyncReceived: function onAsyncReceived(type, dataset, query) { - this.eventBus.trigger("asyncreceive", query, dataset); - }, - _onFocused: function onFocused() { - this._minLengthMet() && this.menu.update(this.input.getQuery()); - }, - _onBlurred: function onBlurred() { - if (this.input.hasQueryChangedSinceLastFocus()) { - this.eventBus.trigger("change", this.input.getQuery()); - } - }, - _onEnterKeyed: function onEnterKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } - }, - _onTabKeyed: function onTabKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } else if ($selectable = this.menu.getTopSelectable()) { - this.autocomplete($selectable) && $e.preventDefault(); - } - }, - _onEscKeyed: function onEscKeyed() { - this.close(); - }, - _onUpKeyed: function onUpKeyed() { - this.moveCursor(-1); - }, - _onDownKeyed: function onDownKeyed() { - this.moveCursor(+1); - }, - _onLeftKeyed: function onLeftKeyed() { - if (this.dir === "rtl" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getTopSelectable()); - } - }, - _onRightKeyed: function onRightKeyed() { - if (this.dir === "ltr" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getTopSelectable()); - } - }, - _onQueryChanged: function onQueryChanged(e, query) { - this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); - }, - _onWhitespaceChanged: function onWhitespaceChanged() { - this._updateHint(); - }, - _onLangDirChanged: function onLangDirChanged(e, dir) { - if (this.dir !== dir) { - this.dir = dir; - this.menu.setLanguageDirection(dir); - } - }, - _openIfActive: function openIfActive() { - this.isActive() && this.open(); - }, - _minLengthMet: function minLengthMet(query) { - query = _.isString(query) ? query : this.input.getQuery() || ""; - return query.length >= this.minLength; - }, - _updateHint: function updateHint() { - var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; - $selectable = this.menu.getTopSelectable(); - data = this.menu.getSelectableData($selectable); - val = this.input.getInputValue(); - if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { - query = Input.normalizeQuery(val); - escapedQuery = _.escapeRegExChars(query); - frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(data.val); - match && this.input.setHint(val + match[1]); - } else { - this.input.clearHint(); - } - }, - isEnabled: function isEnabled() { - return this.enabled; - }, - enable: function enable() { - this.enabled = true; - }, - disable: function disable() { - this.enabled = false; - }, - isActive: function isActive() { - return this.active; - }, - activate: function activate() { - if (this.isActive()) { - return true; - } else if (!this.isEnabled() || this.eventBus.before("active")) { - return false; - } else { - this.active = true; - this.eventBus.trigger("active"); - return true; - } - }, - deactivate: function deactivate() { - if (!this.isActive()) { - return true; - } else if (this.eventBus.before("idle")) { - return false; - } else { - this.active = false; - this.close(); - this.eventBus.trigger("idle"); - return true; - } - }, - isOpen: function isOpen() { - return this.menu.isOpen(); - }, - open: function open() { - if (!this.isOpen() && !this.eventBus.before("open")) { - this.menu.open(); - this._updateHint(); - this.eventBus.trigger("open"); - } - return this.isOpen(); - }, - close: function close() { - if (this.isOpen() && !this.eventBus.before("close")) { - this.menu.close(); - this.input.clearHint(); - this.input.resetInputValue(); - this.eventBus.trigger("close"); - } - return !this.isOpen(); - }, - setVal: function setVal(val) { - this.input.setQuery(_.toStr(val)); - }, - getVal: function getVal() { - return this.input.getQuery(); - }, - select: function select($selectable) { - var data = this.menu.getSelectableData($selectable); - if (data && !this.eventBus.before("select", data.obj)) { - this.input.setQuery(data.val, true); - this.eventBus.trigger("select", data.obj); - this.close(); - return true; - } - return false; - }, - autocomplete: function autocomplete($selectable) { - var query, data, isValid; - query = this.input.getQuery(); - data = this.menu.getSelectableData($selectable); - isValid = data && query !== data.val; - if (isValid && !this.eventBus.before("autocomplete", data.obj)) { - this.input.setQuery(data.val); - this.eventBus.trigger("autocomplete", data.obj); - return true; - } - return false; - }, - moveCursor: function moveCursor(delta) { - var query, $candidate, data, payload, cancelMove; - query = this.input.getQuery(); - $candidate = this.menu.selectableRelativeToCursor(delta); - data = this.menu.getSelectableData($candidate); - payload = data ? data.obj : null; - cancelMove = this._minLengthMet() && this.menu.update(query); - if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { - this.menu.setCursor($candidate); - if (data) { - this.input.setInputValue(data.val); - } else { - this.input.resetInputValue(); - this._updateHint(); - } - this.eventBus.trigger("cursorchange", payload); - return true; - } - return false; - }, - destroy: function destroy() { - this.input.destroy(); - this.menu.destroy(); - } - }); - return Typeahead; - function c(ctx) { - var methods = [].slice.call(arguments, 1); - return function() { - var args = [].slice.call(arguments); - _.each(methods, function(method) { - return ctx[method].apply(ctx, args); - }); - }; - } - }(); - (function() { - "use strict"; - var old, keys, methods; - old = $.fn.typeahead; - keys = { - www: "tt-www", - attrs: "tt-attrs", - typeahead: "tt-typeahead" - }; - methods = { - initialize: function initialize(o, datasets) { - var www; - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - www = WWW(o.classNames); - return this.each(attach); - function attach() { - var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - $input = $(this); - $wrapper = $(www.html.wrapper); - $hint = $elOrNull(o.hint); - $menu = $elOrNull(o.menu); - defaultHint = o.hint !== false && !$hint; - defaultMenu = o.menu !== false && !$menu; - defaultHint && ($hint = buildHintFromInput($input, www)); - defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); - $hint && $hint.val(""); - $input = prepInput($input, www); - if (defaultHint || defaultMenu) { - $wrapper.css(www.css.wrapper); - $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); - $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); - } - MenuConstructor = defaultMenu ? DefaultMenu : Menu; - eventBus = new EventBus({ - el: $input - }); - input = new Input({ - hint: $hint, - input: $input - }, www); - menu = new MenuConstructor({ - node: $menu, - datasets: datasets - }, www); - typeahead = new Typeahead({ - input: input, - menu: menu, - eventBus: eventBus, - minLength: o.minLength - }, www); - $input.data(keys.www, www); - $input.data(keys.typeahead, typeahead); - } - }, - isEnabled: function isEnabled() { - var enabled; - ttEach(this.first(), function(t) { - enabled = t.isEnabled(); - }); - return enabled; - }, - enable: function enable() { - ttEach(this, function(t) { - t.enable(); - }); - return this; - }, - disable: function disable() { - ttEach(this, function(t) { - t.disable(); - }); - return this; - }, - isActive: function isActive() { - var active; - ttEach(this.first(), function(t) { - active = t.isActive(); - }); - return active; - }, - activate: function activate() { - ttEach(this, function(t) { - t.activate(); - }); - return this; - }, - deactivate: function deactivate() { - ttEach(this, function(t) { - t.deactivate(); - }); - return this; - }, - isOpen: function isOpen() { - var open; - ttEach(this.first(), function(t) { - open = t.isOpen(); - }); - return open; - }, - open: function open() { - ttEach(this, function(t) { - t.open(); - }); - return this; - }, - close: function close() { - ttEach(this, function(t) { - t.close(); - }); - return this; - }, - select: function select(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.select($el); - }); - return success; - }, - autocomplete: function autocomplete(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.autocomplete($el); - }); - return success; - }, - moveCursor: function moveCursoe(delta) { - var success = false; - ttEach(this.first(), function(t) { - success = t.moveCursor(delta); - }); - return success; - }, - val: function val(newVal) { - var query; - if (!arguments.length) { - ttEach(this.first(), function(t) { - query = t.getVal(); - }); - return query; - } else { - ttEach(this, function(t) { - t.setVal(newVal); - }); - return this; - } - }, - destroy: function destroy() { - ttEach(this, function(typeahead, $input) { - revert($input); - typeahead.destroy(); - }); - return this; - } - }; - $.fn.typeahead = function(method) { - if (methods[method]) { - return methods[method].apply(this, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; - function ttEach($els, fn) { - $els.each(function() { - var $input = $(this), typeahead; - (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); - }); - } - function buildHintFromInput($input, www) { - return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ - autocomplete: "off", - spellcheck: "false", - tabindex: -1 - }); - } - function prepInput($input, www) { - $input.data(keys.attrs, { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass(www.classes.input).attr({ - autocomplete: "off", - spellcheck: false - }); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input; - } - function getBackgroundStyles($el) { - return { - backgroundAttachment: $el.css("background-attachment"), - backgroundClip: $el.css("background-clip"), - backgroundColor: $el.css("background-color"), - backgroundImage: $el.css("background-image"), - backgroundOrigin: $el.css("background-origin"), - backgroundPosition: $el.css("background-position"), - backgroundRepeat: $el.css("background-repeat"), - backgroundSize: $el.css("background-size") - }; - } - function revert($input) { - var www, $wrapper; - www = $input.data(keys.www); - $wrapper = $input.parent().filter(www.selectors.wrapper); - _.each($input.data(keys.attrs), function(val, key) { - _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); - if ($wrapper.length) { - $input.detach().insertAfter($wrapper); - $wrapper.remove(); - } - } - function $elOrNull(obj) { - var isValid, $el; - isValid = _.isJQuery(obj) || _.isElement(obj); - $el = isValid ? $(obj).first() : []; - return $el.length ? $el : null; - } - })(); -}); diff --git a/templates/base.html b/templates/base.html index be46cccb..36fc45e4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -45,16 +45,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Preload JavaScript #} {% bootstrap_javascript %} - - {% block custom_js %}{% endblock %} {# Load CSS #} {% bootstrap_css %} - - + {# load theme #} {% if request.user.is_authenticated %} @@ -118,4 +115,4 @@ with this program; if not, write to the Free Software Foundation, Inc., - \ No newline at end of file + From 44e41bb4f52ebd1cc8f1601049c8d0bb486d221e Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Mon, 28 Dec 2020 22:29:59 +0100 Subject: [PATCH 35/55] Add dal to installed apps --- re2o/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/re2o/settings.py b/re2o/settings.py index f7975ae4..2e3d75a7 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -59,6 +59,8 @@ LOGIN_URL = "/login/" # The URL for login page LOGIN_REDIRECT_URL = "/" # The URL for redirecting after login # Application definition +# dal_legacy_static only needed for Django < 2.0 (https://django-autocomplete-light.readthedocs.io/en/master/install.html#django-versions-earlier-than-2-0) +EARLY_EXTERNAL_CONTRIB_APPS = ("dal", "dal_select2", "dal_legacy_static") # Need to be added before django.contrib.admin (https://django-autocomplete-light.readthedocs.io/en/master/install.html#configuration) DJANGO_CONTRIB_APPS = ( "django.contrib.admin", "django.contrib.auth", @@ -80,7 +82,7 @@ LOCAL_APPS = ( "logs", ) INSTALLED_APPS = ( - DJANGO_CONTRIB_APPS + EXTERNAL_CONTRIB_APPS + LOCAL_APPS + OPTIONNAL_APPS + EARLY_EXTERNAL_CONTRIB_APPS + DJANGO_CONTRIB_APPS + EXTERNAL_CONTRIB_APPS + LOCAL_APPS + OPTIONNAL_APPS ) MIDDLEWARE_CLASSES = ( "django.contrib.sessions.middleware.SessionMiddleware", From 33279c3eb7f719cd95f82ff96b20dd47b3f1f4a6 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Mon, 28 Dec 2020 22:30:17 +0100 Subject: [PATCH 36/55] Create dal mixins --- re2o/mixins.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/re2o/mixins.py b/re2o/mixins.py index 26034e07..a57a0628 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -26,6 +26,7 @@ A set of mixins used all over the project to avoid duplicating code from reversion import revisions as reversion from django.db import transaction from django.utils.translation import ugettext as _ +from dal import autocomplete class RevMixin(object): @@ -228,3 +229,56 @@ class AclMixin(object): else None, (permission,), ) + + +class AutocompleteModelMixin(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 + """ + # By default, only trigger autocompletion after 3 characters have been typed + #attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) + # 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 AutocompleteMultipleModelMixin(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 + """ + # By default, only trigger autocompletion after 3 characters have been typed + #attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) + # 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 AutocompleteViewMixin(autocomplete.Select2QuerySetView): + obj_type = None # This MUST be overridden by child class + query_filter = "name__icontains" # Override this if necessary + + def get_queryset(self): + query_set = self.obj_type.objects.all() + if self.q: + query_set = query_set.filter(**{ self.query_filter: self.q}) + return query_set From 118bb653a40a9264ce63d39738e090a5b6642980 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 30 Dec 2020 18:49:52 +0100 Subject: [PATCH 37/55] Rename vars --- logs/forms.py | 16 ++++++++-------- logs/models.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/logs/forms.py b/logs/forms.py index b8c8b010..c98bd282 100644 --- a/logs/forms.py +++ b/logs/forms.py @@ -114,28 +114,28 @@ def classes_for_action_type(action_type): class ActionsSearchForm(Form): """Form used to do an advanced search through the logs.""" - u = forms.ModelChoiceField( + user = forms.ModelChoiceField( label=_("Performed by"), queryset=users.models.User.objects.all(), required=False, ) - t = forms.MultipleChoiceField( + action_type = forms.MultipleChoiceField( label=_("Action type"), required=False, widget=forms.CheckboxSelectMultiple, choices=CHOICES_ACTION_TYPE, initial=[i[0] for i in CHOICES_ACTION_TYPE], ) - s = forms.DateField(required=False, label=_("Start date")) - e = forms.DateField(required=False, label=_("End date")) + start_date = forms.DateField(required=False, label=_("Start date")) + end_date = forms.DateField(required=False, label=_("End date")) def __init__(self, *args, **kwargs): super(ActionsSearchForm, self).__init__(*args, **kwargs) - self.fields["s"].help_text = get_input_formats_help_text( - self.fields["s"].input_formats + self.fields["start_date"].help_text = get_input_formats_help_text( + self.fields["start_date"].input_formats ) - self.fields["e"].help_text = get_input_formats_help_text( - self.fields["e"].input_formats + self.fields["end_date"].help_text = get_input_formats_help_text( + self.fields["end_date"].input_formats ) diff --git a/logs/models.py b/logs/models.py index 8790f46e..aa5d7822 100644 --- a/logs/models.py +++ b/logs/models.py @@ -600,10 +600,10 @@ class ActionsSearch: Returns: The QuerySet of Revision objects corresponding to the search. """ - user = params.get("u", None) - start = params.get("s", None) - end = params.get("e", None) - action_types = params.get("t", None) + user = params.get("user", None) + start = params.get("start_date", None) + end = params.get("end_date", None) + action_types = params.get("action_type", None) query = Q() From db75f797c56775dce3bb436c0cdf689c2b0d1c39 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 30 Dec 2020 18:54:50 +0100 Subject: [PATCH 38/55] Autocomplete on history search --- logs/forms.py | 2 ++ logs/templates/logs/search_stats_logs.html | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/logs/forms.py b/logs/forms.py index c98bd282..eb623827 100644 --- a/logs/forms.py +++ b/logs/forms.py @@ -25,6 +25,7 @@ from django import forms from django.forms import Form from django.utils.translation import ugettext_lazy as _ from re2o.base import get_input_formats_help_text +from re2o.mixins import AutocompleteModelMixin import inspect @@ -118,6 +119,7 @@ class ActionsSearchForm(Form): label=_("Performed by"), queryset=users.models.User.objects.all(), required=False, + widget=AutocompleteModelMixin(url="/users/user-autocomplete"), ) action_type = forms.MultipleChoiceField( label=_("Action type"), diff --git a/logs/templates/logs/search_stats_logs.html b/logs/templates/logs/search_stats_logs.html index b124139e..88c2babd 100644 --- a/logs/templates/logs/search_stats_logs.html +++ b/logs/templates/logs/search_stats_logs.html @@ -22,7 +22,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load i18n %} {% block title %}{% trans "Search events" %}{% endblock %} @@ -32,10 +31,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "Search events" %}

- {% massive_bootstrap_form actions_form 'u' %} + {% bootstrap_form actions_form %} {% trans "Search" as tr_search %} {% bootstrap_button tr_search button_type="submit" icon="search" %} +{{ actions_form.media }}


From 501169447994220d52ce05a7583b481c5a45006c Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 30 Dec 2020 19:06:09 +0100 Subject: [PATCH 39/55] Add can_list acl, move views autocomplete mixins to re2o/views.py --- cotisations/views_autocomplete.py | 2 +- re2o/acl.py | 9 ++++++++ re2o/mixins.py | 35 ++++++++++++++++++++++--------- re2o/templatetags/acl.py | 3 +++ re2o/views.py | 23 ++++++++++++++++++++ topologie/views_autocomplete.py | 2 +- users/views_autocomplete.py | 2 +- 7 files changed, 63 insertions(+), 13 deletions(-) diff --git a/cotisations/views_autocomplete.py b/cotisations/views_autocomplete.py index 70afe855..1e634813 100644 --- a/cotisations/views_autocomplete.py +++ b/cotisations/views_autocomplete.py @@ -37,7 +37,7 @@ from .models import ( Banque ) -from re2o.mixins import AutocompleteViewMixin +from re2o.views import AutocompleteViewMixin from re2o.acl import ( can_view_all, diff --git a/re2o/acl.py b/re2o/acl.py index f74427ea..692df905 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -369,6 +369,15 @@ def can_view_all(*targets): return acl_base_decorator("can_view_all", *targets, on_instance=False) +def can_list(*targets): + """Decorator to check if an user can list a class of model. + It runs `acl_base_decorator` with the flag `on_instance=False` and the + method 'can_list'. See `acl_base_decorator` documentation for further + details. + """ + return acl_base_decorator("can_list", *targets, on_instance=False) + + def can_view_app(*apps_name): """Decorator to check if an user can view the applications.""" for app_name in apps_name: diff --git a/re2o/mixins.py b/re2o/mixins.py index a57a0628..df5d7310 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -29,6 +29,7 @@ from django.utils.translation import ugettext as _ from dal import autocomplete + class RevMixin(object): """A mixin to subclass the save and delete function of a model to enforce the versioning of the object before those actions @@ -80,6 +81,8 @@ class AclMixin(object): :can_view: Applied on an instance, return if the user can view the instance :can_view_all: Applied on a class, return if the user can view all + instances + :can_list: Applied on a class, return if the user can list all instances""" @classmethod @@ -209,6 +212,28 @@ class AclMixin(object): (permission,), ) + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """Check if a user can list all instances of an object + + Parameters: + user_request: User calling for this action + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ + permission = cls.get_modulename() + ".view_" + cls.get_classname() + can = user_request.has_perm(permission) + return ( + can, + _("You don't have the right to list every %s object.") % cls.get_classname() + if not can + else None, + (permission,), + cls.objects.all() if can else None, + ) + def can_view(self, user_request, *_args, **_kwargs): """Check if a user can view an instance of an object @@ -272,13 +297,3 @@ class AutocompleteMultipleModelMixin(autocomplete.ModelSelect2Multiple): attrs["data-minimum-results-for-search"] = attrs.get("data-minimum-results-for-search", 10) return attrs - -class AutocompleteViewMixin(autocomplete.Select2QuerySetView): - obj_type = None # This MUST be overridden by child class - query_filter = "name__icontains" # Override this if necessary - - def get_queryset(self): - query_set = self.obj_type.objects.all() - if self.q: - query_set = query_set.filter(**{ self.query_filter: self.q}) - return query_set diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py index 132239be..4c88b207 100644 --- a/re2o/templatetags/acl.py +++ b/re2o/templatetags/acl.py @@ -141,6 +141,8 @@ def get_callback(tag_name, obj=None): return acl_fct(obj.can_view_all, False) if tag_name == "cannot_view_all": return acl_fct(obj.can_view_all, True) + if tag_name == "can_list": + return acl_fct(obj.can_list, False) if tag_name == "can_view_app": return acl_fct( lambda x: ( @@ -296,6 +298,7 @@ def acl_change_filter(parser, token): @register.tag("cannot_delete_all") @register.tag("can_view_all") @register.tag("cannot_view_all") +@register.tag("can_list") def acl_model_filter(parser, token): """Generic definition of an acl templatetag for acl based on model""" diff --git a/re2o/views.py b/re2o/views.py index 51f36b1a..359c3e81 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -33,6 +33,10 @@ from django.template.context_processors import csrf from django.conf import settings from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_page +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.decorators import method_decorator +from dal import autocomplete from preferences.models import ( Service, @@ -43,6 +47,7 @@ from preferences.models import ( Mandate, ) +from .acl import can_list from .contributors import CONTRIBUTORS from importlib import import_module from re2o.settings_local import OPTIONNAL_APPS_RE2O @@ -169,3 +174,21 @@ def handler500(request): def handler404(request): """The handler view for a 404 error""" return render(request, "errors/404.html", status=404) + + +class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView): + obj_type = None # This MUST be overridden by child class + query_set = None + query_filter = "name__icontains" # Override this if necessary + + def get_queryset(self): + + can, reason, _permission, query_set = self.obj_type.can_list(self.request.user) + + self.query_set = query_set + if hasattr(self, "filter_results"): + self.filter_results() + else: + if self.q: + self.query_set = self.query_set.filter(**{ self.query_filter: self.q}) + return self.query_set diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index bef31fdb..801b1572 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -44,7 +44,7 @@ from .models import ( SwitchBay, ) -from re2o.mixins import AutocompleteViewMixin +from re2o.views import AutocompleteViewMixin from re2o.acl import ( can_view_all, diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index 94f5a93f..b565a4ce 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -45,7 +45,7 @@ from .models import ( EMailAddress, ) -from re2o.mixins import AutocompleteViewMixin +from re2o.views import AutocompleteViewMixin from re2o.acl import ( can_view_all, From a5e3016119f5f22be9a665a3e3e077ddce933583 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 30 Dec 2020 19:22:43 +0100 Subject: [PATCH 40/55] Replace get_queryset with filter_results --- machines/views_autocomplete.py | 22 +++++++++------------ topologie/views_autocomplete.py | 34 ++++++++++++++++----------------- users/views_autocomplete.py | 13 ++++++------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py index e68a11c9..ef259da3 100644 --- a/machines/views_autocomplete.py +++ b/machines/views_autocomplete.py @@ -46,7 +46,7 @@ from .models import ( IpList ) -from re2o.mixins import AutocompleteViewMixin +from re2o.views import AutocompleteViewMixin from re2o.acl import ( can_view_all, @@ -84,29 +84,25 @@ class OuverturePortListAutocomplete(AutocompleteViewMixin): class InterfaceAutocomplete(AutocompleteViewMixin): obj_type = Interface - def get_queryset(self): - qs = self.obj_type.objects.all() - + # Precision on search to add annotations so search behaves more like users expect it to + def filter_results(self): if self.q: - qs = qs.filter( + self.query_set = self.query_set.filter( Q(domain__name__icontains=self.q) | Q(machine__name__icontains=self.q) ) - return qs - class IpListAutocomplete(AutocompleteViewMixin): obj_type = IpList - def get_queryset(self): + # 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) - qs = self.obj_type.objects.filter(interface__isnull=True) + self.query_set = self.query_set.filter(interface__isnull=True) if machine_type: - qs = qs.filter(ip_type__machinetype__id=machine_type) + self.query_set = self.query_set.filter(ip_type__machinetype__id=machine_type) if self.q: - qs = qs.filter( + self.query_set = self.query_set.filter( Q(ipv4__startswith=self.q) ) - - return qs diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index 801b1572..7f8553c1 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -55,11 +55,11 @@ from re2o.acl import ( class RoomAutocomplete(AutocompleteViewMixin): obj_type = Room - # Override get_queryset to add annotations so search behaves more like users expect it to - def get_queryset(self): + # 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 name B, and rooms from 001 - 999 # Comments explain what we try to match - qs = self.obj_type.objects.annotate( + 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" @@ -68,7 +68,7 @@ class RoomAutocomplete(AutocompleteViewMixin): ).all() if self.q: - qs = qs.filter( + 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) @@ -76,8 +76,6 @@ class RoomAutocomplete(AutocompleteViewMixin): | Q(dorm_full_colon_name__icontains=self.q) ) - return qs - #@can_view_all(Dormitory) class DormitoryAutocomplete(AutocompleteViewMixin): @@ -88,20 +86,20 @@ class DormitoryAutocomplete(AutocompleteViewMixin): class BuildingAutocomplete(AutocompleteViewMixin): obj_type = Building - def get_queryset(self): + # Precision on search to add annotations so search behaves more like users expect it to + def filter_results(self): # We want to be able to filter by dorm so it's easier - qs = self.obj_type.objects.annotate( + 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: - qs = qs.filter( + self.query_set = self.query_set.filter( Q(full_name__icontains=self.q) | Q(full_name_colon__icontains=self.q) ) - return qs class SwitchAutocomplete(AutocompleteViewMixin): obj_type = Switch @@ -110,38 +108,38 @@ class SwitchAutocomplete(AutocompleteViewMixin): class PortAutocomplete(AutocompleteViewMixin): obj_type = Port - def get_queryset(self): + # Precision on search to add annotations so search behaves more like users expect it to + 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 sepcify the output_field - qs = self.obj_type.objects.annotate( + 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: - qs = qs.filter( + 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) ) - return qs - class SwitchBayAutocomplete(AutocompleteViewMixin): obj_type = SwitchBay - def get_queryset(self): + # 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 - qs = self.obj_type.objects.annotate( + self.query_set = self.query_set.annotate( full_name=Concat("building__name", Value(" "), "name"), # Match when the user searches "" dorm_name=Concat("building__dormitory__name", Value(" "), "name"), # Match "Dorm Local Sud" dorm_full_name=Concat("building__dormitory__name", Value(" "), "building__name", Value(" "), "name"), # Match "Dorm J Local Sud" ).all() if self.q: - qs = qs.filter( + 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) diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index b565a4ce..f0cfc3ae 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -61,24 +61,23 @@ class SchoolAutocomplete(AutocompleteViewMixin): #@can_view_all(User) class UserAutocomplete(AutocompleteViewMixin): obj_type = User - # Override get_queryset to add annotations so search behaves more like users expect it to - def get_queryset(self): + + # 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 - qs = self.obj_type.objects.annotate( + 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: - qs = qs.filter( + 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) ) - return qs - -#@can_view_all(Adherent) +#can_view_all(Adherent) class AdherentAutocomplete(AutocompleteViewMixin): obj_type = Adherent From 3f7af6189fbb9cc5633466965899756977087ee4 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 30 Dec 2020 19:24:12 +0100 Subject: [PATCH 41/55] Remove massive_bf_form --- users/templates/users/plugin_out.html | 1 - users/templates/users/user_autocapture.html | 1 - 2 files changed, 2 deletions(-) diff --git a/users/templates/users/plugin_out.html b/users/templates/users/plugin_out.html index 585c1a0a..6b96896d 100644 --- a/users/templates/users/plugin_out.html +++ b/users/templates/users/plugin_out.html @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load static %} {% load i18n %} diff --git a/users/templates/users/user_autocapture.html b/users/templates/users/user_autocapture.html index c396a288..44fa1f41 100644 --- a/users/templates/users/user_autocapture.html +++ b/users/templates/users/user_autocapture.html @@ -24,7 +24,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} -{% load massive_bootstrap_form %} {% load static %} {% load i18n %} {% block title %}{% trans "Users" %}{% endblock %} From 9df9179c4ed0bd1f526311941591edb9f4f834b4 Mon Sep 17 00:00:00 2001 From: chirac Date: Wed, 30 Dec 2020 19:29:56 +0100 Subject: [PATCH 42/55] Add autocomplete on multiop --- multi_op/preferences/forms.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/multi_op/preferences/forms.py b/multi_op/preferences/forms.py index 406a9c3b..30aceb36 100644 --- a/multi_op/preferences/forms.py +++ b/multi_op/preferences/forms.py @@ -29,6 +29,7 @@ each. from django import forms from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ +from re2o.mixins import AutocompleteMultipleModelMixin from .models import MultiopOption @@ -39,3 +40,8 @@ class EditMultiopOptionForm(ModelForm): class Meta: model = MultiopOption fields = "__all__" + widgets = { + "enabled_dorm": AutocompleteMultipleModelMixin( + url="/topologie/dormitory-autocomplete", + ), + } From 52f31f02e2500ab57926b4e8b1a94b919e75689b Mon Sep 17 00:00:00 2001 From: chirac Date: Thu, 31 Dec 2020 14:55:10 +0100 Subject: [PATCH 43/55] Add custom can_list acl for unpriviged views --- machines/models.py | 56 +++++++++++++++++++++++++++++++++++++++++++++ topologie/models.py | 48 ++++++++++++++++++++++++++++++++++++++ users/models.py | 32 ++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) diff --git a/machines/models.py b/machines/models.py index 890b414d..c6275bb8 100644 --- a/machines/models.py +++ b/machines/models.py @@ -378,6 +378,34 @@ class MachineType(RevMixin, AclMixin, models.Model): ) return True, None, None + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list unprivileged machinetypes + Only members of privileged groups can list all. + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + can, _message, _group = cls.can_use_all(user_request) + if can: + return ( + True, + None, + None, + cls.objects.all() + ) + else: + return ( + False, + _("You don't have the right to use all machine types."), + ("machines.use_all_machinetype",), + cls.objects.filter( + ip_type__in=IpType.objects.filter(need_infra=False) + ), + ) + def __str__(self): return self.name @@ -2130,6 +2158,34 @@ class IpList(RevMixin, AclMixin, models.Model): self.clean() super(IpList, self).save(*args, **kwargs) + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """Only privilged users can list all ipv4. + Others can list Ipv4 related with unprivileged type. + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + can, _message, _group = IpType.can_use_all(user_request) + if can: + return ( + True, + None, + None, + cls.objects.all() + ) + else: + return ( + False, + _("You don't have the right to use all machine types."), + ("machines.use_all_machinetype",), + cls.objects.filter( + ip_type__in=IpType.objects.filter(need_infra=False) + ), + ) + def __str__(self): return self.ipv4 diff --git a/topologie/models.py b/topologie/models.py index c0362480..b7219d27 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -731,6 +731,22 @@ class Dormitory(AclMixin, RevMixin, models.Model): else: return cache.get_or_set("multiple_dorms", cls.objects.count() > 1) + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list dormitory + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + return ( + True, + None, + None, + cls.objects.all() + ) + def __str__(self): return self.name @@ -762,6 +778,22 @@ class Building(AclMixin, RevMixin, models.Model): else: return self.name + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list building + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + return ( + True, + None, + None, + cls.objects.all() + ) + @cached_property def cached_name(self): return self.get_name() @@ -944,6 +976,22 @@ class Room(AclMixin, RevMixin, models.Model): verbose_name_plural = _("rooms") unique_together = ("name", "building") + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list room + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + return ( + True, + None, + None, + cls.objects.all() + ) + def __str__(self): return self.building.cached_name + " " + self.name diff --git a/users/models.py b/users/models.py index 14215fdf..fc0c711a 100755 --- a/users/models.py +++ b/users/models.py @@ -2364,6 +2364,22 @@ class School(RevMixin, AclMixin, models.Model): verbose_name = _("school") verbose_name_plural = _("schools") + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list schools + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + return ( + True, + None, + None, + cls.objects.all() + ) + def __str__(self): return self.name @@ -2487,6 +2503,22 @@ class ListShell(RevMixin, AclMixin, models.Model): """ return self.shell.split("/")[-1] + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list shells + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + return ( + True, + None, + None, + cls.objects.all() + ) + def __str__(self): return self.shell From 15bcb0c280bc6ea1551622ab1982708a38f56135 Mon Sep 17 00:00:00 2001 From: chirac Date: Thu, 31 Dec 2020 15:31:49 +0100 Subject: [PATCH 44/55] Add club manager acl for user search --- re2o/views.py | 5 ++++- users/models.py | 27 +++++++++++++++++++++++++++ users/views_autocomplete.py | 15 +++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/re2o/views.py b/re2o/views.py index 359c3e81..a689aab8 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -184,8 +184,11 @@ class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView def get_queryset(self): can, reason, _permission, query_set = self.obj_type.can_list(self.request.user) + if query_set: + self.query_set = query_set + else: + self.query_set = self.obj_type.objects.none() - self.query_set = query_set if hasattr(self, "filter_results"): self.filter_results() else: diff --git a/users/models.py b/users/models.py index fc0c711a..9fb10972 100755 --- a/users/models.py +++ b/users/models.py @@ -2065,6 +2065,33 @@ class Adherent(User): ("users.add_user",), ) + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """Users can list adherent only if they are : + - Members of view acl, + - Club administrator. + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + can, _message, _group = Club.can_view_all(user_request) + if user_request.has_perm("users.view_user") or can: + return ( + True, + None, + None, + cls.objects.all() + ) + else: + return ( + False, + _("You don't have the right to list all adherents."), + ("users.view_user",), + cls.objects.none(), + ) + def clean(self, *args, **kwargs): """Method, clean and validate the gpgfp value. diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index f0cfc3ae..bc34a582 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -81,6 +81,21 @@ class UserAutocomplete(AutocompleteViewMixin): 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) + ) + #@can_view_all(Club) class ClubAutocomplete(AutocompleteViewMixin): obj_type = Club From 4f4b71c5b4e160ef0febe78e86647297c936f2e9 Mon Sep 17 00:00:00 2001 From: chirac Date: Thu, 31 Dec 2020 19:42:08 +0100 Subject: [PATCH 45/55] Remove useless imports --- machines/views_autocomplete.py | 4 ---- topologie/views_autocomplete.py | 7 ------- users/views_autocomplete.py | 14 -------------- 3 files changed, 25 deletions(-) diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py index ef259da3..6d3b7d71 100644 --- a/machines/views_autocomplete.py +++ b/machines/views_autocomplete.py @@ -48,10 +48,6 @@ from .models import ( from re2o.views import AutocompleteViewMixin -from re2o.acl import ( - can_view_all, -) - class VlanAutocomplete(AutocompleteViewMixin): obj_type = Vlan diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index 7f8553c1..eb3e7f2b 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -46,12 +46,7 @@ from .models import ( from re2o.views import AutocompleteViewMixin -from re2o.acl import ( - can_view_all, -) - -#@can_view_all(School) class RoomAutocomplete(AutocompleteViewMixin): obj_type = Room @@ -77,12 +72,10 @@ class RoomAutocomplete(AutocompleteViewMixin): ) -#@can_view_all(Dormitory) class DormitoryAutocomplete(AutocompleteViewMixin): obj_type = Dormitory -#@can_view_all(Building) class BuildingAutocomplete(AutocompleteViewMixin): obj_type = Building diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index bc34a582..68ad58ad 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -33,32 +33,20 @@ from __future__ import unicode_literals from .models import ( User, - Ban, - Whitelist, School, - ListRight, - Request, - ServiceUser, Adherent, Club, ListShell, - EMailAddress, ) from re2o.views import AutocompleteViewMixin -from re2o.acl import ( - can_view_all, -) - from django.db.models import Q, Value, CharField from django.db.models.functions import Concat -#@can_view_all(School) class SchoolAutocomplete(AutocompleteViewMixin): obj_type = School -#@can_view_all(User) class UserAutocomplete(AutocompleteViewMixin): obj_type = User @@ -77,7 +65,6 @@ class UserAutocomplete(AutocompleteViewMixin): | Q(full_name_reverse__icontains=self.q) ) -#can_view_all(Adherent) class AdherentAutocomplete(AutocompleteViewMixin): obj_type = Adherent @@ -96,7 +83,6 @@ class AdherentAutocomplete(AutocompleteViewMixin): | Q(full_name_reverse__icontains=self.q) ) -#@can_view_all(Club) class ClubAutocomplete(AutocompleteViewMixin): obj_type = Club From 62561b27de103aa16786e8731269e62880b18fdf Mon Sep 17 00:00:00 2001 From: chirac Date: Thu, 31 Dec 2020 19:50:41 +0100 Subject: [PATCH 46/55] Correct customs acl list for extensions --- machines/models.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/machines/models.py b/machines/models.py index c6275bb8..4f359477 100644 --- a/machines/models.py +++ b/machines/models.py @@ -981,6 +981,32 @@ class Extension(RevMixin, AclMixin, models.Model): ("machines.use_all_extension",), ) + @classmethod + def can_list(cls, user_request, *_args, **_kwargs): + """All users can list unprivileged extensions + Only members of privileged groups can list all. + + :param user_request: The user who wants to view the list. + :return: True if the user can view the list and an explanation + message. + + """ + can, _message, _group = cls.can_use_all(user_request) + if can: + return ( + True, + None, + None, + cls.objects.all() + ) + else: + return ( + False, + _("You don't have the right to list all extensions."), + ("machines.use_all_extension",), + cls.objects.filter(need_infra=False), + ) + def __str__(self): return self.name From 7ed7a57014fa924d8006b14787612255dff6bc5e Mon Sep 17 00:00:00 2001 From: chirac Date: Thu, 31 Dec 2020 20:12:36 +0100 Subject: [PATCH 47/55] Black on files --- cotisations/forms.py | 14 ++-- logs/forms.py | 22 ++---- machines/forms.py | 91 ++++++++--------------- machines/views_autocomplete.py | 19 +++-- re2o/mixins.py | 16 ++-- re2o/views.py | 2 +- topologie/forms.py | 44 +++++------ topologie/views_autocomplete.py | 61 ++++++++++----- users/forms.py | 128 +++++++++++++++----------------- users/views_autocomplete.py | 29 +++++--- 10 files changed, 204 insertions(+), 222 deletions(-) diff --git a/cotisations/forms.py b/cotisations/forms.py index 926db818..4689e3cb 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -45,7 +45,11 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin +from re2o.mixins import ( + FormRevMixin, + AutocompleteModelMixin, + AutocompleteMultipleModelMixin, +) from .models import ( Article, Paiement, @@ -80,12 +84,8 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): model = Facture fields = "__all__" widgets = { - "user": AutocompleteModelMixin( - url="/users/user-autocomplete", - ), - "banque": AutocompleteModelMixin( - url="/cotisations/banque-autocomplete", - ), + "user": AutocompleteModelMixin(url="/users/user-autocomplete"), + "banque": AutocompleteModelMixin(url="/cotisations/banque-autocomplete"), } def clean(self): diff --git a/logs/forms.py b/logs/forms.py index eb623827..03045e4a 100644 --- a/logs/forms.py +++ b/logs/forms.py @@ -47,10 +47,7 @@ CHOICES_ACTION_TYPE = ( ("all", _("All")), ) -CHOICES_TYPE = ( - ("ip", _("IPv4")), - ("mac", _("MAC address")), -) +CHOICES_TYPE = (("ip", _("IPv4")), ("mac", _("MAC address"))) def all_classes(module): @@ -88,14 +85,11 @@ def classes_for_action_type(action_type): users.models.User.__name__, users.models.Adherent.__name__, users.models.Club.__name__, - users.models.EMailAddress.__name__ + users.models.EMailAddress.__name__, ] if action_type == "machines": - return [ - machines.models.Machine.__name__, - machines.models.Interface.__name__ - ] + return [machines.models.Machine.__name__, machines.models.Interface.__name__] if action_type == "subscriptions": return all_classes(cotisations.models) @@ -115,6 +109,7 @@ def classes_for_action_type(action_type): class ActionsSearchForm(Form): """Form used to do an advanced search through the logs.""" + user = forms.ModelChoiceField( label=_("Performed by"), queryset=users.models.User.objects.all(), @@ -143,13 +138,10 @@ class ActionsSearchForm(Form): class MachineHistorySearchForm(Form): """Form used to do a search through the machine histories.""" - q = forms.CharField( - label=_("Search"), - max_length=100, - ) + + q = forms.CharField(label=_("Search"), max_length=100) t = forms.CharField( - label=_("Search type"), - widget=forms.Select(choices=CHOICES_TYPE) + label=_("Search type"), widget=forms.Select(choices=CHOICES_TYPE) ) s = forms.DateField(required=False, label=_("Start date")) e = forms.DateField(required=False, label=_("End date")) diff --git a/machines/forms.py b/machines/forms.py index 3f6a3864..9a563d9a 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -40,7 +40,11 @@ from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin +from re2o.mixins import ( + FormRevMixin, + AutocompleteModelMixin, + AutocompleteMultipleModelMixin, +) from .models import ( Domain, Machine, @@ -71,11 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class Meta: model = Machine fields = "__all__" - widgets = { - "user": AutocompleteModelMixin( - url="/users/user-autocomplete", - ), - } + widgets = {"user": AutocompleteModelMixin(url="/users/user-autocomplete")} def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -97,17 +97,16 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): model = Interface fields = ["machine", "machine_type", "ipv4", "mac_address", "details"] widgets = { - "machine": AutocompleteModelMixin( - url="/machines/machine-autocomplete", - ), + "machine": AutocompleteModelMixin(url="/machines/machine-autocomplete"), "machine_type": AutocompleteModelMixin( - url="/machines/machinetype-autocomplete", + url="/machines/machinetype-autocomplete" ), "ipv4": AutocompleteModelMixin( - url="/machines/iplist-autocomplete", forward=['machine_type'], + url="/machines/iplist-autocomplete", + forward=["machine_type"], attrs={ - 'data-placeholder': 'Automatic assigment. Type to choose specific ip.', - } + "data-placeholder": "Automatic assigment. Type to choose specific ip." + }, ), } @@ -159,9 +158,7 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): model = Domain fields = ["name", "extension", "ttl"] widgets = { - "extension": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), + "extension": AutocompleteModelMixin(url="/machines/extension-autocomplete") } def __init__(self, *args, **kwargs): @@ -213,9 +210,7 @@ class MachineTypeForm(FormRevMixin, ModelForm): model = MachineType fields = ["name", "ip_type"] widgets = { - "ip_type": AutocompleteModelMixin( - url="/machines/iptype-autocomplete", - ), + "ip_type": AutocompleteModelMixin(url="/machines/iptype-autocomplete") } def __init__(self, *args, **kwargs): @@ -252,14 +247,10 @@ class IpTypeForm(FormRevMixin, ModelForm): model = IpType fields = "__all__" widgets = { - "vlan": AutocompleteModelMixin( - url="/machines/vlan-autocomplete", - ), - "extension": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), + "vlan": AutocompleteModelMixin(url="/machines/vlan-autocomplete"), + "extension": AutocompleteModelMixin(url="/machines/extension-autocomplete"), "ouverture_ports": AutocompleteModelMixin( - url="/machines/ouvertureportlist-autocomplete", + url="/machines/ouvertureportlist-autocomplete" ), } @@ -392,12 +383,8 @@ class MxForm(FormRevMixin, ModelForm): model = Mx fields = ["zone", "priority", "name", "ttl"] widgets = { - "zone": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), - "name": AutocompleteModelMixin( - url="/machines/domain-autocomplete", - ), + "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete"), + "name": AutocompleteModelMixin(url="/machines/domain-autocomplete"), } def __init__(self, *args, **kwargs): @@ -435,12 +422,8 @@ class NsForm(FormRevMixin, ModelForm): model = Ns fields = ["zone", "ns", "ttl"] widgets = { - "zone": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), - "ns": AutocompleteModelMixin( - url="/machines/domain-autocomplete", - ), + "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete"), + "ns": AutocompleteModelMixin(url="/machines/domain-autocomplete"), } def __init__(self, *args, **kwargs): @@ -476,9 +459,7 @@ class TxtForm(FormRevMixin, ModelForm): model = Txt fields = "__all__" widgets = { - "zone": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), + "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete") } def __init__(self, *args, **kwargs): @@ -511,9 +492,7 @@ class DNameForm(FormRevMixin, ModelForm): model = DName fields = "__all__" widgets = { - "zone": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), + "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete") } def __init__(self, *args, **kwargs): @@ -546,12 +525,8 @@ class SrvForm(FormRevMixin, ModelForm): model = Srv fields = "__all__" widgets = { - "extension": AutocompleteModelMixin( - url="/machines/extension-autocomplete", - ), - "target": AutocompleteModelMixin( - url="/machines/domain-autocomplete", - ), + "extension": AutocompleteModelMixin(url="/machines/extension-autocomplete"), + "target": AutocompleteModelMixin(url="/machines/domain-autocomplete"), } def __init__(self, *args, **kwargs): @@ -585,10 +560,10 @@ class NasForm(FormRevMixin, ModelForm): fields = "__all__" widgets = { "nas_type": AutocompleteModelMixin( - url="/machines/machinetype-autocomplete", + url="/machines/machinetype-autocomplete" ), "machine_type": AutocompleteModelMixin( - url="/machines/machinetype-autocomplete", + url="/machines/machinetype-autocomplete" ), } @@ -623,8 +598,8 @@ class RoleForm(FormRevMixin, ModelForm): fields = "__all__" widgets = { "servers": AutocompleteMultipleModelMixin( - url="/machines/interface-autocomplete", - ), + url="/machines/interface-autocomplete" + ) } def __init__(self, *args, **kwargs): @@ -661,8 +636,8 @@ class ServiceForm(FormRevMixin, ModelForm): fields = "__all__" widgets = { "servers": AutocompleteMultipleModelMixin( - url="/machines/interface-autocomplete", - ), + url="/machines/interface-autocomplete" + ) } def __init__(self, *args, **kwargs): @@ -750,8 +725,8 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm): fields = ["port_lists"] widgets = { "port_lists": AutocompleteMultipleModelMixin( - url="/machines/ouvertureportlist-autocomplete", - ), + url="/machines/ouvertureportlist-autocomplete" + ) } def __init__(self, *args, **kwargs): diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py index 6d3b7d71..076d13a2 100644 --- a/machines/views_autocomplete.py +++ b/machines/views_autocomplete.py @@ -43,7 +43,7 @@ from .models import ( Extension, Domain, OuverturePortList, - IpList + IpList, ) from re2o.views import AutocompleteViewMixin @@ -66,15 +66,15 @@ class IpTypeAutocomplete(AutocompleteViewMixin): class ExtensionAutocomplete(AutocompleteViewMixin): - obj_type = Extension + obj_type = Extension class DomainAutocomplete(AutocompleteViewMixin): - obj_type = Domain + obj_type = Domain class OuverturePortListAutocomplete(AutocompleteViewMixin): - obj_type = OuverturePortList + obj_type = OuverturePortList class InterfaceAutocomplete(AutocompleteViewMixin): @@ -84,8 +84,7 @@ class InterfaceAutocomplete(AutocompleteViewMixin): 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) + Q(domain__name__icontains=self.q) | Q(machine__name__icontains=self.q) ) @@ -94,11 +93,11 @@ class IpListAutocomplete(AutocompleteViewMixin): # 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) + 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) + ip_type__machinetype__id=machine_type ) + if self.q: + self.query_set = self.query_set.filter(Q(ipv4__startswith=self.q)) diff --git a/re2o/mixins.py b/re2o/mixins.py index df5d7310..9aa39081 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -29,7 +29,6 @@ from django.utils.translation import ugettext as _ from dal import autocomplete - class RevMixin(object): """A mixin to subclass the save and delete function of a model to enforce the versioning of the object before those actions @@ -260,6 +259,7 @@ class AutocompleteModelMixin(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) @@ -271,9 +271,11 @@ class AutocompleteModelMixin(autocomplete.ModelSelect2): See https://select2.org/configuration/options-api """ # By default, only trigger autocompletion after 3 characters have been typed - #attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) + # attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) # 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) + attrs["data-minimum-results-for-search"] = attrs.get( + "data-minimum-results-for-search", 10 + ) return attrs @@ -281,6 +283,7 @@ class AutocompleteMultipleModelMixin(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) @@ -292,8 +295,9 @@ class AutocompleteMultipleModelMixin(autocomplete.ModelSelect2Multiple): See https://select2.org/configuration/options-api """ # By default, only trigger autocompletion after 3 characters have been typed - #attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) + # attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) # 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) + attrs["data-minimum-results-for-search"] = attrs.get( + "data-minimum-results-for-search", 10 + ) return attrs - diff --git a/re2o/views.py b/re2o/views.py index a689aab8..ed7d2cff 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -193,5 +193,5 @@ class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView self.filter_results() else: if self.q: - self.query_set = self.query_set.filter(**{ self.query_filter: self.q}) + self.query_set = self.query_set.filter(**{self.query_filter: self.q}) return self.query_set diff --git a/topologie/forms.py b/topologie/forms.py index fc7a4106..e88218fd 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -37,7 +37,11 @@ from django.utils.translation import ugettext_lazy as _ from machines.models import Interface from machines.forms import EditMachineForm, NewMachineForm -from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin +from re2o.mixins import ( + FormRevMixin, + AutocompleteModelMixin, + AutocompleteMultipleModelMixin, +) from .models import ( Port, @@ -63,20 +67,14 @@ class PortForm(FormRevMixin, ModelForm): model = Port fields = "__all__" widgets = { - "switch": AutocompleteModelMixin( - url="/topologie/switch-autocomplete", - ), - "room": AutocompleteModelMixin( - url="/topologie/room-autocomplete", - ), + "switch": AutocompleteModelMixin(url="/topologie/switch-autocomplete"), + "room": AutocompleteModelMixin(url="/topologie/room-autocomplete"), "machine_interface": AutocompleteModelMixin( - url="/machine/machine-autocomplete", - ), - "related": AutocompleteModelMixin( - url="/topologie/port-autocomplete", + url="/machine/machine-autocomplete" ), + "related": AutocompleteModelMixin(url="/topologie/port-autocomplete"), "custom_profile": AutocompleteModelMixin( - url="/topologie/portprofile-autocomplete", + url="/topologie/portprofile-autocomplete" ), } @@ -184,11 +182,9 @@ class EditSwitchForm(EditMachineForm): fields = "__all__" widgets = { "switchbay": AutocompleteModelMixin( - url="/topologie/switchbay-autocomplete", - ), - "user": AutocompleteModelMixin( - url="/users/user-autocomplete", + url="/topologie/switchbay-autocomplete" ), + "user": AutocompleteModelMixin(url="/users/user-autocomplete"), } @@ -206,9 +202,7 @@ class EditRoomForm(FormRevMixin, ModelForm): model = Room fields = "__all__" widgets = { - "building": AutocompleteModelMixin( - url="/topologie/building-autocomplete", - ), + "building": AutocompleteModelMixin(url="/topologie/building-autocomplete") } def __init__(self, *args, **kwargs): @@ -229,7 +223,7 @@ class EditModelSwitchForm(FormRevMixin, ModelForm): members = forms.ModelMultipleChoiceField( Switch.objects.all(), widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), - required=False + required=False, ) class Meta: @@ -274,9 +268,7 @@ class EditSwitchBayForm(FormRevMixin, ModelForm): model = SwitchBay fields = "__all__" widgets = { - "building": AutocompleteModelMixin( - url="/topologie/building-autocomplete", - ), + "building": AutocompleteModelMixin(url="/topologie/building-autocomplete") } def __init__(self, *args, **kwargs): @@ -324,11 +316,9 @@ class EditPortProfileForm(FormRevMixin, ModelForm): fields = "__all__" widgets = { "vlan_tagged": AutocompleteMultipleModelMixin( - url="/machines/vlan-autocomplete", - ), - "vlan_untagged": AutocompleteModelMixin( - url="/machines/vlan-autocomplete", + url="/machines/vlan-autocomplete" ), + "vlan_untagged": AutocompleteModelMixin(url="/machines/vlan-autocomplete"), } def __init__(self, *args, **kwargs): diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index eb3e7f2b..e5c6ee6d 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -34,15 +34,7 @@ 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 .models import Room, Dormitory, Building, Switch, PortProfile, Port, SwitchBay from re2o.views import AutocompleteViewMixin @@ -55,11 +47,27 @@ class RoomAutocomplete(AutocompleteViewMixin): # Suppose we have a dorm named Dorm, a building name 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=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) + 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: @@ -89,8 +97,7 @@ class BuildingAutocomplete(AutocompleteViewMixin): if self.q: self.query_set = self.query_set.filter( - Q(full_name__icontains=self.q) - | Q(full_name_colon__icontains=self.q) + Q(full_name__icontains=self.q) | Q(full_name_colon__icontains=self.q) ) @@ -106,9 +113,13 @@ class PortAutocomplete(AutocompleteViewMixin): # We want to enter the switch name, not just the port number # Because we're concatenating a CharField and an Integer, we have to sepcify the output_field self.query_set = self.query_set.annotate( - full_name=Concat("switch__name", Value(" "), "port", output_field=CharField()), + 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()), + full_name_dash=Concat( + "switch__name", Value(" - "), "port", output_field=CharField() + ), ).all() if self.q: @@ -126,9 +137,19 @@ class SwitchBayAutocomplete(AutocompleteViewMixin): def filter_results(self): # 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 "" - dorm_name=Concat("building__dormitory__name", Value(" "), "name"), # Match "Dorm Local Sud" - dorm_full_name=Concat("building__dormitory__name", Value(" "), "building__name", Value(" "), "name"), # Match "Dorm J Local Sud" + full_name=Concat( + "building__name", Value(" "), "name" + ), # Match when the user searches "" + dorm_name=Concat( + "building__dormitory__name", Value(" "), "name" + ), # Match "Dorm Local Sud" + dorm_full_name=Concat( + "building__dormitory__name", + Value(" "), + "building__name", + Value(" "), + "name", + ), # Match "Dorm J Local Sud" ).all() if self.q: diff --git a/users/forms.py b/users/forms.py index 195807a3..dbe1cf7e 100644 --- a/users/forms.py +++ b/users/forms.py @@ -46,7 +46,10 @@ from os import walk, path from django import forms from django.forms import ModelForm, Form from django.contrib.auth.forms import ReadOnlyPasswordHashField -from django.contrib.auth.password_validation import validate_password, password_validators_help_text_html +from django.contrib.auth.password_validation import ( + validate_password, + password_validators_help_text_html, +) from django.core.validators import MinLengthValidator from django.conf import settings from django.utils import timezone @@ -60,7 +63,11 @@ from topologie.models import Port from preferences.models import OptionalUser from re2o.utils import remove_user_room from re2o.base import get_input_formats_help_text -from re2o.mixins import FormRevMixin, AutocompleteMultipleModelMixin, AutocompleteModelMixin +from re2o.mixins import ( + FormRevMixin, + AutocompleteMultipleModelMixin, + AutocompleteModelMixin, +) from re2o.field_permissions import FieldPermissionFormMixin from preferences.models import GeneralOption @@ -156,14 +163,10 @@ class ServiceUserAdminForm(FormRevMixin, forms.ModelForm): """ password1 = forms.CharField( - label=_("Password"), - widget=forms.PasswordInput, - max_length=255, + label=_("Password"), widget=forms.PasswordInput, max_length=255 ) password2 = forms.CharField( - label=_("Password confirmation"), - widget=forms.PasswordInput, - max_length=255, + label=_("Password confirmation"), widget=forms.PasswordInput, max_length=255 ) def __init__(self, *args, **kwargs): @@ -215,6 +218,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): DjangoForm : Inherit from basic django form """ + selfpasswd = forms.CharField( label=_("Current password"), max_length=255, widget=forms.PasswordInput ) @@ -222,12 +226,10 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): label=_("New password"), max_length=255, widget=forms.PasswordInput, - help_text=password_validators_help_text_html() + help_text=password_validators_help_text_html(), ) passwd2 = forms.CharField( - label=_("New password confirmation"), - max_length=255, - widget=forms.PasswordInput, + label=_("New password confirmation"), max_length=255, widget=forms.PasswordInput ) class Meta: @@ -280,7 +282,7 @@ class ResetPasswordForm(forms.Form): Parameters: DjangoForm : Inherit from basic django form - """ + """ pseudo = forms.CharField(label=_("Username"), max_length=255) email = forms.EmailField(max_length=255) @@ -292,13 +294,11 @@ class MassArchiveForm(forms.Form): Parameters: DjangoForm : Inherit from basic django form - """ + """ date = forms.DateTimeField(help_text="%d/%m/%y") full_archive = forms.BooleanField( - label=_( - "Fully archive users? WARNING: CRITICAL OPERATION IF TRUE" - ), + label=_("Fully archive users? WARNING: CRITICAL OPERATION IF TRUE"), initial=False, required=False, ) @@ -323,7 +323,7 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -351,18 +351,14 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): "room", ] widgets = { - "school": AutocompleteModelMixin( - url="/users/school-autocomplete", - ), + "school": AutocompleteModelMixin(url="/users/school-autocomplete"), "room": AutocompleteModelMixin( url="/topologie/room-autocomplete", - attrs = { - "data-minimum-input-length": 3 # Only trigger autocompletion after 3 characters have been typed - } + attrs={ + "data-minimum-input-length": 3 # Only trigger autocompletion after 3 characters have been typed + }, ), - "shell": AutocompleteModelMixin( - url="/users/shell-autocomplete", - ) + "shell": AutocompleteModelMixin(url="/users/shell-autocomplete"), } force = forms.BooleanField( @@ -426,7 +422,8 @@ class AdherentCreationForm(AdherentForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ + # Champ pour choisir si un lien est envoyé par mail pour le mot de passe init_password_by_mail_info = _( "If this options is set, you will receive a link to set" @@ -439,9 +436,7 @@ class AdherentCreationForm(AdherentForm): ) init_password_by_mail = forms.BooleanField( - help_text=init_password_by_mail_info, - required=False, - initial=True + help_text=init_password_by_mail_info, required=False, initial=True ) init_password_by_mail.label = _("Send password reset link by email.") @@ -452,7 +447,7 @@ class AdherentCreationForm(AdherentForm): label=_("Password"), widget=forms.PasswordInput, max_length=255, - help_text=password_validators_help_text_html() + help_text=password_validators_help_text_html(), ) password2 = forms.CharField( required=False, @@ -542,8 +537,12 @@ class AdherentCreationForm(AdherentForm): # Save the provided password in hashed format user = super(AdherentForm, self).save(commit=False) - is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation") - set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail") + is_set_password_allowed = OptionalUser.get_cached_value( + "allow_set_password_during_user_creation" + ) + set_passwd = is_set_password_allowed and not self.cleaned_data.get( + "init_password_by_mail" + ) if set_passwd: user.set_password(self.cleaned_data["password1"]) @@ -558,7 +557,7 @@ class AdherentEditForm(AdherentForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): super(AdherentEditForm, self).__init__(*args, **kwargs) @@ -594,7 +593,7 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -624,15 +623,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): "mailing", ] widgets = { - "school": AutocompleteModelMixin( - url="/users/school-autocomplete", - ), - "room": AutocompleteModelMixin( - url="/topologie/room-autocomplete", - ), - "shell": AutocompleteModelMixin( - url="/users/shell-autocomplete", - ) + "school": AutocompleteModelMixin(url="/users/school-autocomplete"), + "room": AutocompleteModelMixin(url="/topologie/room-autocomplete"), + "shell": AutocompleteModelMixin(url="/users/shell-autocomplete"), } def clean_telephone(self): @@ -657,18 +650,18 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ class Meta: model = Club fields = ["administrators", "members"] widgets = { "administrators": AutocompleteMultipleModelMixin( - url="/users/adherent-autocomplete", + url="/users/adherent-autocomplete" ), "members": AutocompleteMultipleModelMixin( - url="/users/adherent-autocomplete", - ) + url="/users/adherent-autocomplete" + ), } def __init__(self, *args, **kwargs): @@ -681,7 +674,7 @@ class PasswordForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ class Meta: model = User @@ -698,7 +691,7 @@ class ServiceUserForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ password = forms.CharField( label=_("New password"), @@ -737,7 +730,7 @@ class EditServiceUserForm(ServiceUserForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ password = forms.CharField( label=_("New password"), @@ -757,7 +750,7 @@ class StateForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ class Meta: model = User @@ -775,7 +768,7 @@ class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ groups = forms.ModelMultipleChoiceField( Group.objects.all(), widget=forms.CheckboxSelectMultiple, required=False @@ -797,7 +790,7 @@ class SchoolForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ class Meta: model = School @@ -814,7 +807,7 @@ class ShellForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ class Meta: model = ListShell @@ -833,7 +826,7 @@ class ListRightForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ permissions = forms.ModelMultipleChoiceField( Permission.objects.all().select_related("content_type"), @@ -857,7 +850,7 @@ class NewListRightForm(ListRightForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ class Meta(ListRightForm.Meta): fields = ("name", "unix_name", "gid", "critical", "permissions", "details") @@ -875,7 +868,7 @@ class DelListRightForm(Form): Parameters: DjangoForm : Inherit from basic django form - """ + """ listrights = forms.ModelMultipleChoiceField( queryset=ListRight.objects.none(), @@ -898,7 +891,7 @@ class DelSchoolForm(Form): Parameters: DjangoForm : Inherit from basic django form - """ + """ schools = forms.ModelMultipleChoiceField( queryset=School.objects.none(), @@ -920,7 +913,7 @@ class BanForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -939,7 +932,7 @@ class WhitelistForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -959,7 +952,7 @@ class EMailAddressForm(FormRevMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -980,7 +973,7 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): Parameters: DjangoForm : Inherit from basic django form - """ + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -1004,7 +997,8 @@ class InitialRegisterForm(forms.Form): Parameters: DjangoForm : Inherit from basic django form - """ + """ + register_room = forms.BooleanField(required=False) register_machine = forms.BooleanField(required=False) @@ -1085,8 +1079,8 @@ class ThemeForm(FormRevMixin, forms.Form): theme = forms.ChoiceField(widget=forms.Select()) def __init__(self, *args, **kwargs): - _, _ ,themes = next(walk(path.join(settings.STATIC_ROOT, "css/themes"))) + _, _, themes = next(walk(path.join(settings.STATIC_ROOT, "css/themes"))) if not themes: themes = ["default.css"] super(ThemeForm, self).__init__(*args, **kwargs) - self.fields['theme'].choices = [(theme, theme) for theme in themes] + self.fields["theme"].choices = [(theme, theme) for theme in themes] diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index 68ad58ad..e202ec4d 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -31,22 +31,18 @@ Here are defined the autocomplete class based view. """ from __future__ import unicode_literals -from .models import ( - User, - School, - Adherent, - Club, - ListShell, -) +from .models import User, School, Adherent, Club, ListShell from re2o.views import AutocompleteViewMixin from django.db.models import Q, Value, CharField from django.db.models.functions import Concat + class SchoolAutocomplete(AutocompleteViewMixin): obj_type = School + class UserAutocomplete(AutocompleteViewMixin): obj_type = User @@ -54,8 +50,12 @@ class UserAutocomplete(AutocompleteViewMixin): 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" + 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: @@ -65,6 +65,7 @@ class UserAutocomplete(AutocompleteViewMixin): | Q(full_name_reverse__icontains=self.q) ) + class AdherentAutocomplete(AutocompleteViewMixin): obj_type = Adherent @@ -72,8 +73,12 @@ class AdherentAutocomplete(AutocompleteViewMixin): 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" + 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: @@ -83,9 +88,11 @@ class AdherentAutocomplete(AutocompleteViewMixin): | Q(full_name_reverse__icontains=self.q) ) + class ClubAutocomplete(AutocompleteViewMixin): obj_type = Club + class ShellAutocomplete(AutocompleteViewMixin): obj_type = ListShell query_filter = "shell__icontains" From d1d75209a998a00cb3c02d4944918b96db05fabb Mon Sep 17 00:00:00 2001 From: chirac Date: Fri, 1 Jan 2021 20:28:44 +0100 Subject: [PATCH 48/55] Remove unusefull dependency --- pip_requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/pip_requirements.txt b/pip_requirements.txt index 148a3b5b..b067c4c5 100644 --- a/pip_requirements.txt +++ b/pip_requirements.txt @@ -1,4 +1,3 @@ django-bootstrap3==11.1.0 django-macaddress==1.6.0 -#django-autocomplete-light==3.2.10 # Until Django 2.0+ django-autocomplete-light From 4d37b4506c31f65f683d1d55b7f6a0e0a1eb2f4b Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Sat, 2 Jan 2021 00:27:31 +0100 Subject: [PATCH 49/55] Fix minor issues with DAL --- machines/views_autocomplete.py | 2 ++ re2o/mixins.py | 8 +++---- re2o/views.py | 3 ++- static/css/autocomplete.css | 2 +- topologie/templates/topologie/topo_more.html | 2 +- topologie/views_autocomplete.py | 25 +++++++++++--------- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/machines/views_autocomplete.py b/machines/views_autocomplete.py index 076d13a2..94b55a59 100644 --- a/machines/views_autocomplete.py +++ b/machines/views_autocomplete.py @@ -95,9 +95,11 @@ class IpListAutocomplete(AutocompleteViewMixin): 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)) diff --git a/re2o/mixins.py b/re2o/mixins.py index 9aa39081..471f7414 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -270,8 +270,8 @@ class AutocompleteModelMixin(autocomplete.ModelSelect2): """ See https://select2.org/configuration/options-api """ - # By default, only trigger autocompletion after 3 characters have been typed - # attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) + # 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 @@ -294,8 +294,8 @@ class AutocompleteMultipleModelMixin(autocomplete.ModelSelect2Multiple): """ See https://select2.org/configuration/options-api """ - # By default, only trigger autocompletion after 3 characters have been typed - # attrs["data-minimum-input-length"] = attrs.get("data-minimum-input-length", 3) + # 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 diff --git a/re2o/views.py b/re2o/views.py index ed7d2cff..aa8ddc4b 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -182,8 +182,8 @@ class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView query_filter = "name__icontains" # Override this if necessary def get_queryset(self): - can, reason, _permission, query_set = self.obj_type.can_list(self.request.user) + if query_set: self.query_set = query_set else: @@ -194,4 +194,5 @@ class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView else: if self.q: self.query_set = self.query_set.filter(**{self.query_filter: self.q}) + return self.query_set diff --git a/static/css/autocomplete.css b/static/css/autocomplete.css index dea68af3..41b355fa 100644 --- a/static/css/autocomplete.css +++ b/static/css/autocomplete.css @@ -39,7 +39,7 @@ See github.com/yourlabs/django-autocomplete-light/issues/1149 .select2-container--default .select2-selection--multiple .select2-selection__rendered { height: 100% !important; - display: inline !imoortant; + display: inline !important; overflow-x: hidden !important; overflow-y: auto !important; } diff --git a/topologie/templates/topologie/topo_more.html b/topologie/templates/topologie/topo_more.html index 74a5265a..be97aa12 100644 --- a/topologie/templates/topologie/topo_more.html +++ b/topologie/templates/topologie/topo_more.html @@ -31,7 +31,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %} {% if topoform %} {% bootstrap_form_errors topoform %} - {{ topoform.media }} +{{ topoform.media }} {% endif %} {% if machineform %} {% bootstrap_form_errors machineform %} diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index e5c6ee6d..e1069b18 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -44,7 +44,7 @@ class RoomAutocomplete(AutocompleteViewMixin): # 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 name B, and rooms from 001 - 999 + # 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( @@ -87,7 +87,6 @@ class DormitoryAutocomplete(AutocompleteViewMixin): class BuildingAutocomplete(AutocompleteViewMixin): obj_type = Building - # Precision on search to add annotations so search behaves more like users expect it to def filter_results(self): # We want to be able to filter by dorm so it's easier self.query_set = self.query_set.annotate( @@ -108,10 +107,9 @@ class SwitchAutocomplete(AutocompleteViewMixin): class PortAutocomplete(AutocompleteViewMixin): obj_type = Port - # Precision on search to add annotations so search behaves more like users expect it to 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 sepcify the output_field + # 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() @@ -133,23 +131,29 @@ class PortAutocomplete(AutocompleteViewMixin): class SwitchBayAutocomplete(AutocompleteViewMixin): obj_type = SwitchBay - # 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 + # See RoomAutocomplete.filter_results self.query_set = self.query_set.annotate( full_name=Concat( "building__name", Value(" "), "name" - ), # Match when the user searches "" + ), dorm_name=Concat( "building__dormitory__name", Value(" "), "name" - ), # Match "Dorm Local Sud" + ), dorm_full_name=Concat( "building__dormitory__name", Value(" "), "building__name", Value(" "), "name", - ), # Match "Dorm J Local Sud" + ), + dorm_full_colon_name=Concat( + "building__dormitory__name", + Value(" : "), + "building__name", + Value(" "), + "name", + ), ).all() if self.q: @@ -157,10 +161,9 @@ class SwitchBayAutocomplete(AutocompleteViewMixin): 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) ) - return qs - class PortProfileAutocomplete(AutocompleteViewMixin): obj_type = PortProfile From a989624ce6d6d2f1e84b5d2b9badd24b3a3541cd Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 2 Jan 2021 23:03:13 +0100 Subject: [PATCH 50/55] Return True when list is returned --- machines/models.py | 6 +++--- users/models.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/machines/models.py b/machines/models.py index 4f359477..9fd524be 100644 --- a/machines/models.py +++ b/machines/models.py @@ -398,7 +398,7 @@ class MachineType(RevMixin, AclMixin, models.Model): ) else: return ( - False, + True, _("You don't have the right to use all machine types."), ("machines.use_all_machinetype",), cls.objects.filter( @@ -1001,7 +1001,7 @@ class Extension(RevMixin, AclMixin, models.Model): ) else: return ( - False, + True, _("You don't have the right to list all extensions."), ("machines.use_all_extension",), cls.objects.filter(need_infra=False), @@ -2204,7 +2204,7 @@ class IpList(RevMixin, AclMixin, models.Model): ) else: return ( - False, + True, _("You don't have the right to use all machine types."), ("machines.use_all_machinetype",), cls.objects.filter( diff --git a/users/models.py b/users/models.py index 9fb10972..2b6eaacf 100755 --- a/users/models.py +++ b/users/models.py @@ -2086,7 +2086,7 @@ class Adherent(User): ) else: return ( - False, + True, _("You don't have the right to list all adherents."), ("users.view_user",), cls.objects.none(), From cbc99aca5412647b462606a0adf3dff9be9489b6 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 2 Jan 2021 23:05:03 +0100 Subject: [PATCH 51/55] Pin specific version --- pip_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip_requirements.txt b/pip_requirements.txt index b067c4c5..b0ceffe5 100644 --- a/pip_requirements.txt +++ b/pip_requirements.txt @@ -1,3 +1,3 @@ django-bootstrap3==11.1.0 django-macaddress==1.6.0 -django-autocomplete-light +django-autocomplete-light==3.8.1 From a0612728ee271b7211e5333db37cfe483fabf966 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 2 Jan 2021 23:19:49 +0100 Subject: [PATCH 52/55] Move new autocomplete widgets on widget file --- cotisations/forms.py | 11 ++--- logs/forms.py | 4 +- machines/forms.py | 52 +++++++++++------------ multi_op/preferences/forms.py | 4 +- preferences/forms.py | 18 ++++---- re2o/mixins.py | 48 ---------------------- re2o/views.py | 3 -- re2o/widgets.py | 77 +++++++++++++++++++++++++++++++++++ tickets/forms.py | 5 ++- topologie/forms.py | 34 ++++++++-------- users/forms.py | 24 +++++------ 11 files changed, 154 insertions(+), 126 deletions(-) create mode 100644 re2o/widgets.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 4689e3cb..f9e44686 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -45,11 +45,8 @@ from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import ( - FormRevMixin, - AutocompleteModelMixin, - AutocompleteMultipleModelMixin, -) +from re2o.mixins import FormRevMixin +from re2o.widgets import AutocompleteModelWidget from .models import ( Article, Paiement, @@ -84,8 +81,8 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): model = Facture fields = "__all__" widgets = { - "user": AutocompleteModelMixin(url="/users/user-autocomplete"), - "banque": AutocompleteModelMixin(url="/cotisations/banque-autocomplete"), + "user": AutocompleteModelWidget(url="/users/user-autocomplete"), + "banque": AutocompleteModelWidget(url="/cotisations/banque-autocomplete"), } def clean(self): diff --git a/logs/forms.py b/logs/forms.py index 03045e4a..0a2ca09c 100644 --- a/logs/forms.py +++ b/logs/forms.py @@ -25,7 +25,7 @@ from django import forms from django.forms import Form from django.utils.translation import ugettext_lazy as _ from re2o.base import get_input_formats_help_text -from re2o.mixins import AutocompleteModelMixin +from re2o.widgets import AutocompleteModelWidget import inspect @@ -114,7 +114,7 @@ class ActionsSearchForm(Form): label=_("Performed by"), queryset=users.models.User.objects.all(), required=False, - widget=AutocompleteModelMixin(url="/users/user-autocomplete"), + widget=AutocompleteModelWidget(url="/users/user-autocomplete"), ) action_type = forms.MultipleChoiceField( label=_("Action type"), diff --git a/machines/forms.py b/machines/forms.py index 9a563d9a..ed5975e3 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -40,10 +40,10 @@ from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import ( - FormRevMixin, - AutocompleteModelMixin, - AutocompleteMultipleModelMixin, +from re2o.mixins import FormRevMixin +from re2o.widgets import ( + AutocompleteModelWidget, + AutocompleteMultipleModelWidget, ) from .models import ( Domain, @@ -75,7 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class Meta: model = Machine fields = "__all__" - widgets = {"user": AutocompleteModelMixin(url="/users/user-autocomplete")} + widgets = {"user": AutocompleteModelWidget(url="/users/user-autocomplete")} def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -97,11 +97,11 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): model = Interface fields = ["machine", "machine_type", "ipv4", "mac_address", "details"] widgets = { - "machine": AutocompleteModelMixin(url="/machines/machine-autocomplete"), - "machine_type": AutocompleteModelMixin( + "machine": AutocompleteModelWidget(url="/machines/machine-autocomplete"), + "machine_type": AutocompleteModelWidget( url="/machines/machinetype-autocomplete" ), - "ipv4": AutocompleteModelMixin( + "ipv4": AutocompleteModelWidget( url="/machines/iplist-autocomplete", forward=["machine_type"], attrs={ @@ -158,7 +158,7 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): model = Domain fields = ["name", "extension", "ttl"] widgets = { - "extension": AutocompleteModelMixin(url="/machines/extension-autocomplete") + "extension": AutocompleteModelWidget(url="/machines/extension-autocomplete") } def __init__(self, *args, **kwargs): @@ -210,7 +210,7 @@ class MachineTypeForm(FormRevMixin, ModelForm): model = MachineType fields = ["name", "ip_type"] widgets = { - "ip_type": AutocompleteModelMixin(url="/machines/iptype-autocomplete") + "ip_type": AutocompleteModelWidget(url="/machines/iptype-autocomplete") } def __init__(self, *args, **kwargs): @@ -247,9 +247,9 @@ class IpTypeForm(FormRevMixin, ModelForm): model = IpType fields = "__all__" widgets = { - "vlan": AutocompleteModelMixin(url="/machines/vlan-autocomplete"), - "extension": AutocompleteModelMixin(url="/machines/extension-autocomplete"), - "ouverture_ports": AutocompleteModelMixin( + "vlan": AutocompleteModelWidget(url="/machines/vlan-autocomplete"), + "extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"), + "ouverture_ports": AutocompleteModelWidget( url="/machines/ouvertureportlist-autocomplete" ), } @@ -383,8 +383,8 @@ class MxForm(FormRevMixin, ModelForm): model = Mx fields = ["zone", "priority", "name", "ttl"] widgets = { - "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete"), - "name": AutocompleteModelMixin(url="/machines/domain-autocomplete"), + "zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"), + "name": AutocompleteModelWidget(url="/machines/domain-autocomplete"), } def __init__(self, *args, **kwargs): @@ -422,8 +422,8 @@ class NsForm(FormRevMixin, ModelForm): model = Ns fields = ["zone", "ns", "ttl"] widgets = { - "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete"), - "ns": AutocompleteModelMixin(url="/machines/domain-autocomplete"), + "zone": AutocompleteModelWidget(url="/machines/extension-autocomplete"), + "ns": AutocompleteModelWidget(url="/machines/domain-autocomplete"), } def __init__(self, *args, **kwargs): @@ -459,7 +459,7 @@ class TxtForm(FormRevMixin, ModelForm): model = Txt fields = "__all__" widgets = { - "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete") + "zone": AutocompleteModelWidget(url="/machines/extension-autocomplete") } def __init__(self, *args, **kwargs): @@ -492,7 +492,7 @@ class DNameForm(FormRevMixin, ModelForm): model = DName fields = "__all__" widgets = { - "zone": AutocompleteModelMixin(url="/machines/extension-autocomplete") + "zone": AutocompleteModelWidget(url="/machines/extension-autocomplete") } def __init__(self, *args, **kwargs): @@ -525,8 +525,8 @@ class SrvForm(FormRevMixin, ModelForm): model = Srv fields = "__all__" widgets = { - "extension": AutocompleteModelMixin(url="/machines/extension-autocomplete"), - "target": AutocompleteModelMixin(url="/machines/domain-autocomplete"), + "extension": AutocompleteModelWidget(url="/machines/extension-autocomplete"), + "target": AutocompleteModelWidget(url="/machines/domain-autocomplete"), } def __init__(self, *args, **kwargs): @@ -559,10 +559,10 @@ class NasForm(FormRevMixin, ModelForm): model = Nas fields = "__all__" widgets = { - "nas_type": AutocompleteModelMixin( + "nas_type": AutocompleteModelWidget( url="/machines/machinetype-autocomplete" ), - "machine_type": AutocompleteModelMixin( + "machine_type": AutocompleteModelWidget( url="/machines/machinetype-autocomplete" ), } @@ -597,7 +597,7 @@ class RoleForm(FormRevMixin, ModelForm): model = Role fields = "__all__" widgets = { - "servers": AutocompleteMultipleModelMixin( + "servers": AutocompleteMultipleModelWidget( url="/machines/interface-autocomplete" ) } @@ -635,7 +635,7 @@ class ServiceForm(FormRevMixin, ModelForm): model = Service fields = "__all__" widgets = { - "servers": AutocompleteMultipleModelMixin( + "servers": AutocompleteMultipleModelWidget( url="/machines/interface-autocomplete" ) } @@ -724,7 +724,7 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm): model = Interface fields = ["port_lists"] widgets = { - "port_lists": AutocompleteMultipleModelMixin( + "port_lists": AutocompleteMultipleModelWidget( url="/machines/ouvertureportlist-autocomplete" ) } diff --git a/multi_op/preferences/forms.py b/multi_op/preferences/forms.py index 30aceb36..45941007 100644 --- a/multi_op/preferences/forms.py +++ b/multi_op/preferences/forms.py @@ -29,7 +29,7 @@ each. from django import forms from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ -from re2o.mixins import AutocompleteMultipleModelMixin +from re2o.widgets import AutocompleteMultipleModelWidget from .models import MultiopOption @@ -41,7 +41,7 @@ class EditMultiopOptionForm(ModelForm): model = MultiopOption fields = "__all__" widgets = { - "enabled_dorm": AutocompleteMultipleModelMixin( + "enabled_dorm": AutocompleteMultipleModelWidget( url="/topologie/dormitory-autocomplete", ), } diff --git a/preferences/forms.py b/preferences/forms.py index a7d03162..e19737c1 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -29,7 +29,11 @@ from django.forms import ModelForm, Form from django.db.models import Q from django import forms from django.utils.translation import ugettext_lazy as _ -from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin +from re2o.mixins import FormRevMixin +from re2o.widgets import ( + AutocompleteModelWidget, + AutocompleteMultipleModelWidget +) from .models import ( OptionalUser, OptionalMachine, @@ -110,14 +114,14 @@ class EditOptionalTopologieForm(ModelForm): automatic_provision_switchs = forms.ModelMultipleChoiceField( Switch.objects.all(), required=False, - widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"), ) class Meta: model = OptionalTopologie fields = "__all__" widgets = { - "switchs_ip_type": AutocompleteModelMixin( + "switchs_ip_type": AutocompleteModelWidget( url="/machines/iptype-autocomplete", ), } @@ -176,7 +180,7 @@ class EditAssoOptionForm(ModelForm): model = AssoOption fields = "__all__" widgets = { - "utilisateur_asso": AutocompleteModelMixin( + "utilisateur_asso": AutocompleteModelWidget( url="/users/user-autocomplete", ), } @@ -267,7 +271,7 @@ class MandateForm(ModelForm): model = Mandate fields = "__all__" widgets = { - "president": AutocompleteModelMixin( + "president": AutocompleteModelWidget( url="/users/user-autocomplete", ), } @@ -387,7 +391,7 @@ class RadiusKeyForm(FormRevMixin, ModelForm): members = forms.ModelMultipleChoiceField( queryset=Switch.objects.all(), required=False, - widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"), ) class Meta: @@ -413,7 +417,7 @@ class SwitchManagementCredForm(FormRevMixin, ModelForm): members = forms.ModelMultipleChoiceField( Switch.objects.all(), required=False, - widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"), ) class Meta: diff --git a/re2o/mixins.py b/re2o/mixins.py index 471f7414..22933e67 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -26,7 +26,6 @@ A set of mixins used all over the project to avoid duplicating code from reversion import revisions as reversion from django.db import transaction from django.utils.translation import ugettext as _ -from dal import autocomplete class RevMixin(object): @@ -254,50 +253,3 @@ class AclMixin(object): (permission,), ) - -class AutocompleteModelMixin(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 AutocompleteMultipleModelMixin(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 diff --git a/re2o/views.py b/re2o/views.py index aa8ddc4b..ea22e97c 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -32,8 +32,6 @@ from django.shortcuts import render from django.template.context_processors import csrf from django.conf import settings from django.utils.translation import ugettext as _ -from django.views.decorators.cache import cache_page -from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.decorators import method_decorator from dal import autocomplete @@ -47,7 +45,6 @@ from preferences.models import ( Mandate, ) -from .acl import can_list from .contributors import CONTRIBUTORS from importlib import import_module from re2o.settings_local import OPTIONNAL_APPS_RE2O diff --git a/re2o/widgets.py b/re2o/widgets.py new file mode 100644 index 00000000..935806fe --- /dev/null +++ b/re2o/widgets.py @@ -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 diff --git a/tickets/forms.py b/tickets/forms.py index ecde5492..aad78837 100644 --- a/tickets/forms.py +++ b/tickets/forms.py @@ -28,7 +28,8 @@ from django import forms from django.template.loader import render_to_string from django.forms import ModelForm, Form from re2o.field_permissions import FieldPermissionFormMixin -from re2o.mixins import FormRevMixin, AutocompleteModelMixin, AutocompleteMultipleModelMixin +from re2o.mixins import FormRevMixin +from re2o.widgets import AutocompleteModelWidget from django.utils.translation import ugettext_lazy as _ from .models import Ticket, CommentTicket @@ -59,7 +60,7 @@ class EditTicketForm(FormRevMixin, ModelForm): model = Ticket fields = "__all__" widgets = { - "user": AutocompleteModelMixin( + "user": AutocompleteModelWidget( url="/users/user-autocomplete", ), } diff --git a/topologie/forms.py b/topologie/forms.py index e88218fd..300253f6 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -37,10 +37,10 @@ from django.utils.translation import ugettext_lazy as _ from machines.models import Interface from machines.forms import EditMachineForm, NewMachineForm -from re2o.mixins import ( - FormRevMixin, - AutocompleteModelMixin, - AutocompleteMultipleModelMixin, +from re2o.mixins import FormRevMixin +from re2o.widgets import ( + AutocompleteModelWidget, + AutocompleteMultipleModelWidget, ) from .models import ( @@ -67,13 +67,13 @@ class PortForm(FormRevMixin, ModelForm): model = Port fields = "__all__" widgets = { - "switch": AutocompleteModelMixin(url="/topologie/switch-autocomplete"), - "room": AutocompleteModelMixin(url="/topologie/room-autocomplete"), - "machine_interface": AutocompleteModelMixin( + "switch": AutocompleteModelWidget(url="/topologie/switch-autocomplete"), + "room": AutocompleteModelWidget(url="/topologie/room-autocomplete"), + "machine_interface": AutocompleteModelWidget( url="/machine/machine-autocomplete" ), - "related": AutocompleteModelMixin(url="/topologie/port-autocomplete"), - "custom_profile": AutocompleteModelMixin( + "related": AutocompleteModelWidget(url="/topologie/port-autocomplete"), + "custom_profile": AutocompleteModelWidget( url="/topologie/portprofile-autocomplete" ), } @@ -181,10 +181,10 @@ class EditSwitchForm(EditMachineForm): model = Switch fields = "__all__" widgets = { - "switchbay": AutocompleteModelMixin( + "switchbay": AutocompleteModelWidget( url="/topologie/switchbay-autocomplete" ), - "user": AutocompleteModelMixin(url="/users/user-autocomplete"), + "user": AutocompleteModelWidget(url="/users/user-autocomplete"), } @@ -202,7 +202,7 @@ class EditRoomForm(FormRevMixin, ModelForm): model = Room fields = "__all__" widgets = { - "building": AutocompleteModelMixin(url="/topologie/building-autocomplete") + "building": AutocompleteModelWidget(url="/topologie/building-autocomplete") } def __init__(self, *args, **kwargs): @@ -222,7 +222,7 @@ class EditModelSwitchForm(FormRevMixin, ModelForm): members = forms.ModelMultipleChoiceField( Switch.objects.all(), - widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"), required=False, ) @@ -261,14 +261,14 @@ class EditSwitchBayForm(FormRevMixin, ModelForm): members = forms.ModelMultipleChoiceField( Switch.objects.all(), required=False, - widget=AutocompleteMultipleModelMixin(url="/topologie/switch-autocomplete"), + widget=AutocompleteMultipleModelWidget(url="/topologie/switch-autocomplete"), ) class Meta: model = SwitchBay fields = "__all__" widgets = { - "building": AutocompleteModelMixin(url="/topologie/building-autocomplete") + "building": AutocompleteModelWidget(url="/topologie/building-autocomplete") } def __init__(self, *args, **kwargs): @@ -315,10 +315,10 @@ class EditPortProfileForm(FormRevMixin, ModelForm): model = PortProfile fields = "__all__" widgets = { - "vlan_tagged": AutocompleteMultipleModelMixin( + "vlan_tagged": AutocompleteMultipleModelWidget( url="/machines/vlan-autocomplete" ), - "vlan_untagged": AutocompleteModelMixin(url="/machines/vlan-autocomplete"), + "vlan_untagged": AutocompleteModelWidget(url="/machines/vlan-autocomplete"), } def __init__(self, *args, **kwargs): diff --git a/users/forms.py b/users/forms.py index dbe1cf7e..172a8c54 100644 --- a/users/forms.py +++ b/users/forms.py @@ -63,10 +63,10 @@ from topologie.models import Port from preferences.models import OptionalUser from re2o.utils import remove_user_room from re2o.base import get_input_formats_help_text -from re2o.mixins import ( - FormRevMixin, - AutocompleteMultipleModelMixin, - AutocompleteModelMixin, +from re2o.mixins import FormRevMixin +from re2o.widgets import ( + AutocompleteMultipleModelWidget, + AutocompleteModelWidget, ) from re2o.field_permissions import FieldPermissionFormMixin @@ -351,14 +351,14 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): "room", ] widgets = { - "school": AutocompleteModelMixin(url="/users/school-autocomplete"), - "room": AutocompleteModelMixin( + "school": AutocompleteModelWidget(url="/users/school-autocomplete"), + "room": AutocompleteModelWidget( url="/topologie/room-autocomplete", attrs={ "data-minimum-input-length": 3 # Only trigger autocompletion after 3 characters have been typed }, ), - "shell": AutocompleteModelMixin(url="/users/shell-autocomplete"), + "shell": AutocompleteModelWidget(url="/users/shell-autocomplete"), } force = forms.BooleanField( @@ -623,9 +623,9 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): "mailing", ] widgets = { - "school": AutocompleteModelMixin(url="/users/school-autocomplete"), - "room": AutocompleteModelMixin(url="/topologie/room-autocomplete"), - "shell": AutocompleteModelMixin(url="/users/shell-autocomplete"), + "school": AutocompleteModelWidget(url="/users/school-autocomplete"), + "room": AutocompleteModelWidget(url="/topologie/room-autocomplete"), + "shell": AutocompleteModelWidget(url="/users/shell-autocomplete"), } def clean_telephone(self): @@ -656,10 +656,10 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm): model = Club fields = ["administrators", "members"] widgets = { - "administrators": AutocompleteMultipleModelMixin( + "administrators": AutocompleteMultipleModelWidget( url="/users/adherent-autocomplete" ), - "members": AutocompleteMultipleModelMixin( + "members": AutocompleteMultipleModelWidget( url="/users/adherent-autocomplete" ), } From dceaf9d60fd2e284aac92b59bcc085bca6e3e308 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 2 Jan 2021 23:35:56 +0100 Subject: [PATCH 53/55] Add an unlogged view for registration view --- re2o/views.py | 7 ++++++- topologie/views_autocomplete.py | 4 ++-- users/views_autocomplete.py | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/re2o/views.py b/re2o/views.py index ea22e97c..f5c6e476 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -173,7 +173,7 @@ def handler404(request): return render(request, "errors/404.html", status=404) -class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView): +class AutocompleteUnloggedViewMixin(autocomplete.Select2QuerySetView): obj_type = None # This MUST be overridden by child class query_set = None query_filter = "name__icontains" # Override this if necessary @@ -193,3 +193,8 @@ class AutocompleteViewMixin(LoginRequiredMixin, autocomplete.Select2QuerySetView self.query_set = self.query_set.filter(**{self.query_filter: self.q}) return self.query_set + + +class AutocompleteViewMixin(LoginRequiredMixin, AutocompleteUnloggedViewMixin): + pass + diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index e1069b18..5972da6d 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -36,10 +36,10 @@ from django.db.models.functions import Concat from .models import Room, Dormitory, Building, Switch, PortProfile, Port, SwitchBay -from re2o.views import AutocompleteViewMixin +from re2o.views import AutocompleteViewMixin, AutocompleteUnloggedViewMixin -class RoomAutocomplete(AutocompleteViewMixin): +class RoomAutocomplete(AutocompleteUnloggedViewMixin): obj_type = Room # Precision on search to add annotations so search behaves more like users expect it to diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index e202ec4d..219fe2bb 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -33,13 +33,13 @@ from __future__ import unicode_literals from .models import User, School, Adherent, Club, ListShell -from re2o.views import AutocompleteViewMixin +from re2o.views import AutocompleteViewMixin, AutocompleteUnloggedViewMixin from django.db.models import Q, Value, CharField from django.db.models.functions import Concat -class SchoolAutocomplete(AutocompleteViewMixin): +class SchoolAutocomplete(AutocompleteUnloggedViewMixin): obj_type = School From 6f76201da26d9d0be4e0847edb973d7c26849963 Mon Sep 17 00:00:00 2001 From: chirac Date: Sat, 2 Jan 2021 23:42:13 +0100 Subject: [PATCH 54/55] Unlogged->LoggedOut --- re2o/views.py | 4 ++-- topologie/views_autocomplete.py | 4 ++-- users/views_autocomplete.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/re2o/views.py b/re2o/views.py index f5c6e476..bf08009a 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -173,7 +173,7 @@ def handler404(request): return render(request, "errors/404.html", status=404) -class AutocompleteUnloggedViewMixin(autocomplete.Select2QuerySetView): +class AutocompleteLoggedOutViewMixin(autocomplete.Select2QuerySetView): obj_type = None # This MUST be overridden by child class query_set = None query_filter = "name__icontains" # Override this if necessary @@ -195,6 +195,6 @@ class AutocompleteUnloggedViewMixin(autocomplete.Select2QuerySetView): return self.query_set -class AutocompleteViewMixin(LoginRequiredMixin, AutocompleteUnloggedViewMixin): +class AutocompleteViewMixin(LoginRequiredMixin, AutocompleteLoggedOutViewMixin): pass diff --git a/topologie/views_autocomplete.py b/topologie/views_autocomplete.py index 5972da6d..3eb58db4 100644 --- a/topologie/views_autocomplete.py +++ b/topologie/views_autocomplete.py @@ -36,10 +36,10 @@ from django.db.models.functions import Concat from .models import Room, Dormitory, Building, Switch, PortProfile, Port, SwitchBay -from re2o.views import AutocompleteViewMixin, AutocompleteUnloggedViewMixin +from re2o.views import AutocompleteViewMixin, AutocompleteLoggedOutViewMixin -class RoomAutocomplete(AutocompleteUnloggedViewMixin): +class RoomAutocomplete(AutocompleteLoggedOutViewMixin): obj_type = Room # Precision on search to add annotations so search behaves more like users expect it to diff --git a/users/views_autocomplete.py b/users/views_autocomplete.py index 219fe2bb..014dd146 100644 --- a/users/views_autocomplete.py +++ b/users/views_autocomplete.py @@ -33,13 +33,13 @@ from __future__ import unicode_literals from .models import User, School, Adherent, Club, ListShell -from re2o.views import AutocompleteViewMixin, AutocompleteUnloggedViewMixin +from re2o.views import AutocompleteViewMixin, AutocompleteLoggedOutViewMixin from django.db.models import Q, Value, CharField from django.db.models.functions import Concat -class SchoolAutocomplete(AutocompleteUnloggedViewMixin): +class SchoolAutocomplete(AutocompleteLoggedOutViewMixin): obj_type = School From 39b6e8507eaca4ea520d60052550b56e54514493 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 5 Jan 2021 21:09:45 +0100 Subject: [PATCH 55/55] Fix accessing view queryset property in api/permissions.py Django would raise a RuntimeError indicating not to evaluate the .queryset attribute directly --- api/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/permissions.py b/api/permissions.py index 646682c4..7ab96f1e 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -241,7 +241,7 @@ class AutodetectACLPermission(permissions.BasePermission): # Bypass permission verifications if it is a functional view # (permissions are handled by ACL) - if not getattr(view, "queryset", getattr(view, "get_queryset", None)): + if not hasattr(view, "queryset") and not hasattr(view, "get_queryset"): return True if not request.user or not request.user.is_authenticated: