From f09364d3d8853b55b1de07ff5d2aaa871b1aabee Mon Sep 17 00:00:00 2001
From: Yohann D'ANELLO <yohann.danello@gmail.com>
Date: Fri, 27 Mar 2020 16:19:33 +0100
Subject: [PATCH] Custom auto-complete fields, remove DAL requirement

---
 apps/activity/forms.py                   | 11 ++++-
 apps/activity/views.py                   |  3 ++
 apps/member/forms.py                     | 12 +++--
 apps/member/urls.py                      |  2 -
 apps/member/views.py                     | 23 ---------
 apps/note/api/views.py                   |  3 +-
 apps/note/forms.py                       | 14 +++---
 apps/note/urls.py                        |  3 --
 apps/note/views.py                       | 60 +-----------------------
 note_kfet/inputs.py                      | 50 +++++++++++++++-----
 note_kfet/settings/base.py               |  3 --
 requirements/base.txt                    |  1 -
 static/js/autocomplete_model.js          | 34 ++++++++++++++
 templates/member/autocomplete_model.html |  9 ++++
 templates/member/club_info.html          |  6 ++-
 15 files changed, 117 insertions(+), 117 deletions(-)
 create mode 100644 static/js/autocomplete_model.js
 create mode 100644 templates/member/autocomplete_model.html

diff --git a/apps/activity/forms.py b/apps/activity/forms.py
index b7dc6de9..081f611c 100644
--- a/apps/activity/forms.py
+++ b/apps/activity/forms.py
@@ -3,7 +3,8 @@
 
 from django import forms
 from activity.models import Activity
-from note_kfet.inputs import DateTimePickerInput
+from member.models import Club
+from note_kfet.inputs import DateTimePickerInput, AutocompleteModelSelect
 
 
 class ActivityForm(forms.ModelForm):
@@ -11,6 +12,14 @@ class ActivityForm(forms.ModelForm):
         model = Activity
         fields = '__all__'
         widgets = {
+            "organizer": AutocompleteModelSelect(
+                model=Club,
+                attrs={"api_url": "/api/members/club/"},
+            ),
+            "attendees_club": AutocompleteModelSelect(
+                model=Club,
+                attrs={"api_url": "/api/members/club/"},
+            ),
             "date_start": DateTimePickerInput(),
             "date_end": DateTimePickerInput(),
         }
diff --git a/apps/activity/views.py b/apps/activity/views.py
index f1ecc1b3..be3db16d 100644
--- a/apps/activity/views.py
+++ b/apps/activity/views.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 from django.contrib.auth.mixins import LoginRequiredMixin
+from django.urls import reverse_lazy
 from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
 from django.utils.translation import gettext_lazy as _
 from django_tables2.views import SingleTableView
@@ -13,6 +14,7 @@ from .models import Activity
 class ActivityCreateView(LoginRequiredMixin, CreateView):
     model = Activity
     form_class = ActivityForm
+    success_url = reverse_lazy('activity:activity_list')
 
 
 class ActivityListView(LoginRequiredMixin, SingleTableView):
@@ -33,6 +35,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView):
 class ActivityUpdateView(LoginRequiredMixin, UpdateView):
     model = Activity
     form_class = ActivityForm
+    success_url = reverse_lazy('activity:activity_list')
 
 
 class ActivityEntryView(LoginRequiredMixin, TemplateView):
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 5f2d5838..52dde34a 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -4,10 +4,11 @@
 from crispy_forms.bootstrap import Div
 from crispy_forms.helper import FormHelper
 from crispy_forms.layout import Layout
-from dal import autocomplete
 from django import forms
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
 from django.contrib.auth.models import User
+
+from note_kfet.inputs import AutocompleteModelSelect
 from permission.models import PermissionMask
 
 from .models import Profile, Club, Membership
@@ -63,11 +64,12 @@ class MembershipForm(forms.ModelForm):
         # et récupère les noms d'utilisateur valides
         widgets = {
             'user':
-                autocomplete.ModelSelect2(
-                    url='member:user_autocomplete',
+                AutocompleteModelSelect(
+                    User,
                     attrs={
-                        'data-placeholder': 'Nom ...',
-                        'data-minimum-input-length': 1,
+                        'api_url': '/api/user/',
+                        'name_field': 'username',
+                        'placeholder': 'Nom ...',
                     },
                 ),
         }
diff --git a/apps/member/urls.py b/apps/member/urls.py
index 0b705bfd..085a3fec 100644
--- a/apps/member/urls.py
+++ b/apps/member/urls.py
@@ -21,6 +21,4 @@ urlpatterns = [
     path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
     path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
     path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
-    # API for the user autocompleter
-    path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"),
 ]
diff --git a/apps/member/views.py b/apps/member/views.py
index bcaca335..8145b5e9 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -4,7 +4,6 @@
 import io
 
 from PIL import Image
-from dal import autocomplete
 from django.conf import settings
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.auth.models import User
@@ -253,28 +252,6 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
         return context
 
 
-class UserAutocomplete(autocomplete.Select2QuerySetView):
-    """
-    Auto complete users by usernames
-    """
-
-    def get_queryset(self):
-        """
-        Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion.
-        Cette fonction récupère la requête, et renvoie la liste filtrée des utilisateurs par pseudos.
-        """
-        #  Un utilisateur non connecté n'a accès à aucune information
-        if not self.request.user.is_authenticated:
-            return User.objects.none()
-
-        qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all()
-
-        if self.q:
-            qs = qs.filter(username__regex="^" + self.q)
-
-        return qs
-
-
 # ******************************* #
 #              CLUB               #
 # ******************************* #
diff --git a/apps/note/api/views.py b/apps/note/api/views.py
index 049f8736..23bed1c9 100644
--- a/apps/note/api/views.py
+++ b/apps/note/api/views.py
@@ -24,7 +24,8 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
     """
     queryset = Note.objects.all()
     serializer_class = NotePolymorphicSerializer
-    filter_backends = [SearchFilter, OrderingFilter]
+    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
+    filterset_fields = ['polymorphic_ctype', 'is_active', ]
     search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
     ordering_fields = ['alias__name', 'alias__normalized_name']
 
diff --git a/apps/note/forms.py b/apps/note/forms.py
index 18bcf712..d819644f 100644
--- a/apps/note/forms.py
+++ b/apps/note/forms.py
@@ -1,11 +1,12 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from dal import autocomplete
 from django import forms
+from django.contrib.contenttypes.models import ContentType
 from django.utils.translation import gettext_lazy as _
+from note_kfet.inputs import AutocompleteModelSelect
 
-from .models import TransactionTemplate
+from .models import TransactionTemplate, NoteClub
 
 
 class ImageForm(forms.Form):
@@ -30,11 +31,12 @@ class TransactionTemplateForm(forms.ModelForm):
         # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
         widgets = {
             'destination':
-                autocomplete.ModelSelect2(
-                    url='note:note_autocomplete',
+                AutocompleteModelSelect(
+                    NoteClub,
                     attrs={
-                        'data-placeholder': 'Note ...',
-                        'data-minimum-input-length': 1,
+                        'api_url': '/api/note/note/',
+                        'api_url_suffix': '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteClub).pk),
+                        'placeholder': 'Note ...',
                     },
                 ),
         }
diff --git a/apps/note/urls.py b/apps/note/urls.py
index 59316069..1bc12b4d 100644
--- a/apps/note/urls.py
+++ b/apps/note/urls.py
@@ -13,7 +13,4 @@ urlpatterns = [
     path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
     path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
     path('consos/', views.ConsoView.as_view(), name='consos'),
-
-    # API for the note autocompleter
-    path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'),
 ]
diff --git a/apps/note/views.py b/apps/note/views.py
index c8f57924..25279281 100644
--- a/apps/note/views.py
+++ b/apps/note/views.py
@@ -1,10 +1,8 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-from dal import autocomplete
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.contrib.contenttypes.models import ContentType
-from django.db.models import Q
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import CreateView, UpdateView
 from django_tables2 import SingleTableView
@@ -13,7 +11,7 @@ from note_kfet.inputs import AmountInput
 from permission.backends import PermissionBackend
 
 from .forms import TransactionTemplateForm
-from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
+from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
 from .models.transactions import SpecialTransaction
 from .tables import HistoryTable, ButtonTable
 
@@ -49,62 +47,6 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
         return context
 
 
-class NoteAutocomplete(autocomplete.Select2QuerySetView):
-    """
-    Auto complete note by aliases. Used in every search field for note
-    ex: :view:`ConsoView`, :view:`TransactionCreateView`
-    """
-
-    def get_queryset(self):
-        """
-        When someone look for an :models:`note.Alias`, a query is sent to the dedicated API.
-        This function handles the result and return a filtered list of aliases.
-        """
-        #  Un utilisateur non connecté n'a accès à aucune information
-        if not self.request.user.is_authenticated:
-            return Alias.objects.none()
-
-        qs = Alias.objects.all()
-
-        # self.q est le paramètre de la recherche
-        if self.q:
-            qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \
-                .order_by('normalized_name').distinct()
-
-        # Filtrage par type de note (user, club, special)
-        note_type = self.forwarded.get("note_type", None)
-        if note_type:
-            types = str(note_type).lower()
-            if "user" in types:
-                qs = qs.filter(note__polymorphic_ctype__model="noteuser")
-            elif "club" in types:
-                qs = qs.filter(note__polymorphic_ctype__model="noteclub")
-            elif "special" in types:
-                qs = qs.filter(note__polymorphic_ctype__model="notespecial")
-            else:
-                qs = qs.none()
-
-        return qs
-
-    def get_result_label(self, result):
-        """
-        Show the selected alias and the username associated
-        <Alias> (aka. <Username> )
-        """
-        # Gère l'affichage de l'alias dans la recherche
-        res = result.name
-        note_name = str(result.note)
-        if res != note_name:
-            res += " (aka. " + note_name + ")"
-        return res
-
-    def get_result_value(self, result):
-        """
-        The value used for the transactions will be the id of the Note.
-        """
-        return str(result.note.pk)
-
-
 class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
     """
     Create TransactionTemplate
diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py
index ca3a13c7..35b8b605 100644
--- a/note_kfet/inputs.py
+++ b/note_kfet/inputs.py
@@ -1,19 +1,9 @@
 # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-"""
-This file comes from the project `django-bootstrap-datepicker-plus` available on Github:
-https://github.com/monim67/django-bootstrap-datepicker-plus
-This is distributed under Apache License 2.0.
-
-This adds datetime pickers with bootstrap.
-"""
-
-"""Contains Base Date-Picker input class for widgets of this package."""
-
 from json import dumps as json_dumps
 
-from django.forms.widgets import DateTimeBaseInput, NumberInput
+from django.forms.widgets import DateTimeBaseInput, NumberInput, Select
 
 
 class AmountInput(NumberInput):
@@ -30,6 +20,44 @@ class AmountInput(NumberInput):
         return str(int(100 * float(val))) if val else val
 
 
+class AutocompleteModelSelect(Select):
+    template_name = "member/autocomplete_model.html"
+
+    def __init__(self, model, attrs=None, choices=()):
+        super().__init__(attrs, choices)
+
+        self.model = model
+        self.model_pk = None
+
+    class Media:
+        """JS/CSS resources needed to render the date-picker calendar."""
+
+        js = ('js/autocomplete_model.js', )
+
+    def format_value(self, value):
+        if value:
+            self.attrs["model_pk"] = int(value)
+            return str(self.model.objects.get(pk=int(value)))
+        return ""
+
+    def value_from_datadict(self, data, files, name):
+        val = super().value_from_datadict(data, files, name)
+        print(data)
+        print(self.attrs)
+        return val
+
+
+"""
+The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github:
+https://github.com/monim67/django-bootstrap-datepicker-plus
+This is distributed under Apache License 2.0.
+
+This adds datetime pickers with bootstrap.
+"""
+
+"""Contains Base Date-Picker input class for widgets of this package."""
+
+
 class DatePickerDictionary:
     """Keeps track of all date-picker input classes."""
 
diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py
index bdd4d9a3..61e5ea51 100644
--- a/note_kfet/settings/base.py
+++ b/note_kfet/settings/base.py
@@ -52,9 +52,6 @@ INSTALLED_APPS = [
     # API
     'rest_framework',
     'rest_framework.authtoken',
-    # Autocomplete
-    'dal',
-    'dal_select2',
 
     # Note apps
     'activity',
diff --git a/requirements/base.txt b/requirements/base.txt
index 6c5fbc4c..9c978ed0 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -3,7 +3,6 @@ chardet==3.0.4
 defusedxml==0.6.0
 Django~=2.2
 django-allauth==0.39.1
-django-autocomplete-light==3.5.1
 django-crispy-forms==1.7.2
 django-extensions==2.1.9
 django-filter==2.2.0
diff --git a/static/js/autocomplete_model.js b/static/js/autocomplete_model.js
new file mode 100644
index 00000000..e2a3f0cb
--- /dev/null
+++ b/static/js/autocomplete_model.js
@@ -0,0 +1,34 @@
+$(document).ready(function () {
+    $(".autocomplete").keyup(function(e) {
+        let target = $("#" + e.target.id);
+        let prefix = target.attr("id");
+        let api_url = target.attr("api_url");
+        let api_url_suffix = target.attr("api_url_suffix");
+        if (!api_url_suffix)
+            api_url_suffix = "";
+        let name_field = target.attr("name_field");
+        if (!name_field)
+            name_field = "name";
+        let input = target.val();
+
+        $.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) {
+            let html = "";
+
+            objects.results.forEach(function (obj) {
+                html += li(prefix + "_" + obj.id, obj[name_field]);
+            });
+
+            $("#" + prefix + "_list").html(html);
+
+            objects.results.forEach(function (obj) {
+                $("#" + prefix + "_" + obj.id).click(function() {
+                    target.val(obj[name_field]);
+                    $("#" + prefix + "_pk").val(obj.id);
+                });
+
+                if (input === obj[name_field])
+                    $("#" + prefix + "_pk").val(obj.id);
+            });
+        });
+    });
+});
\ No newline at end of file
diff --git a/templates/member/autocomplete_model.html b/templates/member/autocomplete_model.html
new file mode 100644
index 00000000..c45f13c7
--- /dev/null
+++ b/templates/member/autocomplete_model.html
@@ -0,0 +1,9 @@
+<input type="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
+<input class="form-control mx-auto d-block autocomplete" type="text"
+   {% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
+   name="{{ widget.name }}_name" autocomplete="off"
+    {% for name, value in widget.attrs.items %}
+        {% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
+    {% endfor %}>
+<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
+</ul>
diff --git a/templates/member/club_info.html b/templates/member/club_info.html
index 539d9867..1c8e8661 100644
--- a/templates/member/club_info.html
+++ b/templates/member/club_info.html
@@ -13,8 +13,10 @@
             <dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
             <dd class="col-xl-6">{{ club.name}}</dd>
 
-            <dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
-            <dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
+            {% if club.parent_club %}
+                <dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
+                <dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
+            {% endif %}
 
             <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
             <dd class="col-xl-6">{{ club.membership_start }}</dd>
-- 
GitLab