mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Custom auto-complete fields, remove DAL requirement
This commit is contained in:
		| @@ -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(), | ||||
|         } | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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 ...', | ||||
|                     }, | ||||
|                 ), | ||||
|         } | ||||
|   | ||||
| @@ -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"), | ||||
| ] | ||||
|   | ||||
| @@ -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               # | ||||
| # ******************************* # | ||||
|   | ||||
| @@ -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'] | ||||
|  | ||||
|   | ||||
| @@ -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 ...', | ||||
|                     }, | ||||
|                 ), | ||||
|         } | ||||
|   | ||||
| @@ -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'), | ||||
| ] | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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.""" | ||||
|  | ||||
|   | ||||
| @@ -52,9 +52,6 @@ INSTALLED_APPS = [ | ||||
|     # API | ||||
|     'rest_framework', | ||||
|     'rest_framework.authtoken', | ||||
|     # Autocomplete | ||||
|     'dal', | ||||
|     'dal_select2', | ||||
|  | ||||
|     # Note apps | ||||
|     'activity', | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										34
									
								
								static/js/autocomplete_model.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								static/js/autocomplete_model.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										9
									
								
								templates/member/autocomplete_model.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/member/autocomplete_model.html
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| @@ -13,8 +13,10 @@ | ||||
|             <dt class="col-xl-6">{% trans 'name'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ 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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user