From f09364d3d8853b55b1de07ff5d2aaa871b1aabee Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO 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//update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), path('user//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//', 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 - (aka. ) - """ - # 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 @@ + + +
    +
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 @@
{% trans 'name'|capfirst %}
{{ club.name}}
-
{% trans 'Club Parent'|capfirst %}
-
{{ club.parent_club.name}}
+ {% if club.parent_club %} +
{% trans 'Club Parent'|capfirst %}
+
{{ club.parent_club.name}}
+ {% endif %}
{% trans 'membership start'|capfirst %}
{{ club.membership_start }}