Custom auto-complete fields, remove DAL requirement

This commit is contained in:
Yohann D'ANELLO 2020-03-27 16:19:33 +01:00
parent 823bcfe781
commit f09364d3d8
15 changed files with 117 additions and 117 deletions

View File

@ -3,7 +3,8 @@
from django import forms from django import forms
from activity.models import Activity 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): class ActivityForm(forms.ModelForm):
@ -11,6 +12,14 @@ class ActivityForm(forms.ModelForm):
model = Activity model = Activity
fields = '__all__' fields = '__all__'
widgets = { 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_start": DateTimePickerInput(),
"date_end": DateTimePickerInput(), "date_end": DateTimePickerInput(),
} }

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
@ -13,6 +14,7 @@ from .models import Activity
class ActivityCreateView(LoginRequiredMixin, CreateView): class ActivityCreateView(LoginRequiredMixin, CreateView):
model = Activity model = Activity
form_class = ActivityForm form_class = ActivityForm
success_url = reverse_lazy('activity:activity_list')
class ActivityListView(LoginRequiredMixin, SingleTableView): class ActivityListView(LoginRequiredMixin, SingleTableView):
@ -33,6 +35,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView):
class ActivityUpdateView(LoginRequiredMixin, UpdateView): class ActivityUpdateView(LoginRequiredMixin, UpdateView):
model = Activity model = Activity
form_class = ActivityForm form_class = ActivityForm
success_url = reverse_lazy('activity:activity_list')
class ActivityEntryView(LoginRequiredMixin, TemplateView): class ActivityEntryView(LoginRequiredMixin, TemplateView):

View File

@ -4,10 +4,11 @@
from crispy_forms.bootstrap import Div from crispy_forms.bootstrap import Div
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout from crispy_forms.layout import Layout
from dal import autocomplete
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from note_kfet.inputs import AutocompleteModelSelect
from permission.models import PermissionMask from permission.models import PermissionMask
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
@ -63,11 +64,12 @@ class MembershipForm(forms.ModelForm):
# et récupère les noms d'utilisateur valides # et récupère les noms d'utilisateur valides
widgets = { widgets = {
'user': 'user':
autocomplete.ModelSelect2( AutocompleteModelSelect(
url='member:user_autocomplete', User,
attrs={ attrs={
'data-placeholder': 'Nom ...', 'api_url': '/api/user/',
'data-minimum-input-length': 1, 'name_field': 'username',
'placeholder': 'Nom ...',
}, },
), ),
} }

View File

@ -21,6 +21,4 @@ urlpatterns = [
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), 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('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), 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"),
] ]

View File

@ -4,7 +4,6 @@
import io import io
from PIL import Image from PIL import Image
from dal import autocomplete
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -253,28 +252,6 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
return context 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 # # CLUB #
# ******************************* # # ******************************* #

View File

@ -24,7 +24,8 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
""" """
queryset = Note.objects.all() queryset = Note.objects.all()
serializer_class = NotePolymorphicSerializer 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', ] search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
ordering_fields = ['alias__name', 'alias__normalized_name'] ordering_fields = ['alias__name', 'alias__normalized_name']

View File

@ -1,11 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ 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): 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} # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
widgets = { widgets = {
'destination': 'destination':
autocomplete.ModelSelect2( AutocompleteModelSelect(
url='note:note_autocomplete', NoteClub,
attrs={ attrs={
'data-placeholder': 'Note ...', 'api_url': '/api/note/note/',
'data-minimum-input-length': 1, 'api_url_suffix': '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteClub).pk),
'placeholder': 'Note ...',
}, },
), ),
} }

View File

@ -13,7 +13,4 @@ urlpatterns = [
path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'), path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
path('consos/', views.ConsoView.as_view(), name='consos'), 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'),
] ]

View File

@ -1,10 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, UpdateView from django.views.generic import CreateView, UpdateView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
@ -13,7 +11,7 @@ from note_kfet.inputs import AmountInput
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .forms import TransactionTemplateForm 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 .models.transactions import SpecialTransaction
from .tables import HistoryTable, ButtonTable from .tables import HistoryTable, ButtonTable
@ -49,62 +47,6 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
return context 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): class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
""" """
Create TransactionTemplate Create TransactionTemplate

View File

@ -1,19 +1,9 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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 json import dumps as json_dumps
from django.forms.widgets import DateTimeBaseInput, NumberInput from django.forms.widgets import DateTimeBaseInput, NumberInput, Select
class AmountInput(NumberInput): class AmountInput(NumberInput):
@ -30,6 +20,44 @@ class AmountInput(NumberInput):
return str(int(100 * float(val))) if val else val 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: class DatePickerDictionary:
"""Keeps track of all date-picker input classes.""" """Keeps track of all date-picker input classes."""

View File

@ -52,9 +52,6 @@ INSTALLED_APPS = [
# API # API
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
# Autocomplete
'dal',
'dal_select2',
# Note apps # Note apps
'activity', 'activity',

View File

@ -3,7 +3,6 @@ chardet==3.0.4
defusedxml==0.6.0 defusedxml==0.6.0
Django~=2.2 Django~=2.2
django-allauth==0.39.1 django-allauth==0.39.1
django-autocomplete-light==3.5.1
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-extensions==2.1.9 django-extensions==2.1.9
django-filter==2.2.0 django-filter==2.2.0

View 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);
});
});
});
});

View 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>

View File

@ -13,8 +13,10 @@
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.name}}</dd> <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> {% if club.parent_club %}
<dd class="col-xl-6"> {{ club.parent_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>
{% endif %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd> <dd class="col-xl-6">{{ club.membership_start }}</dd>