diff --git a/apps/activity/forms.py b/apps/activity/forms.py index b7dc6de9e50b0d985bd2d9280ba659cddb8e2423..081f611cc256ad61e7d3bfb0d952f98cab09e35d 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 f1ecc1b3da039298de1af554e06270792af78a17..be3db16d3a12e4593c9c8c456b8720008e978f07 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 5f2d58388f3e4bd1eb3c427a9649ebb224cd82bb..52dde34a676ea3044a51f53cdd9f3f1a7c00833d 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 0b705bfde3a06330e84a4ddda17c19341f80adb6..085a3fecf69b1bc6c2df208d5b746689a9f5e95b 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 bcaca3352fb6a49d75407928324d2bd717d4c470..8145b5e9346548d351ddbf30903a7df65cfacb2b 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 049f873663108a88ebfee9d57cd39a22ed290085..23bed1c92114bc592bbe7d915f6e04ceb12c47f1 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 18bcf7120f0d5d5157570cace0156a3f602794ec..d819644fe41ba0359626269238493bc36a4810c4 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 59316069479d84f980aba8d64ae9fe206db10dbc..1bc12b4d163a80b3363b5085aab9555677175a88 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 c8f57924a0c9f76b3e12acf3dc36fd5158117683..252792815f3ae58eacb42ded15b70376fe70c273 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 ca3a13c7f1aacfb8bd4fea4efddc02d37be92c32..35b8b605e9617981e66e7e813bb24a616e506bbe 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 bdd4d9a38bc19824b6c1df73b1fbcb3895b8289a..61e5ea519acf223b1155242fc077040ec79c15c8 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 6c5fbc4cb9bbfca24e7480341054745182a89483..9c978ed03288cd7c53da8a9be59c82bb3e548bbb 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 0000000000000000000000000000000000000000..e2a3f0cb1ec765df2da11a38a3c4f155ccf316a4 --- /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 0000000000000000000000000000000000000000..c45f13c745684374c0a4b126a9205c8542df61e4 --- /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 539d9867abe81522550ede08531d6cee272580c4..1c8e86611b3876b918ce1d0aff9fdeddc26106de 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>