diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py index 514515ef..19b52a47 100644 --- a/apps/activity/api/serializers.py +++ b/apps/activity/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import ActivityType, Activity, Guest +from ..models import ActivityType, Activity, Guest, Entry class ActivityTypeSerializer(serializers.ModelSerializer): @@ -37,3 +37,14 @@ class GuestSerializer(serializers.ModelSerializer): class Meta: model = Guest fields = '__all__' + + +class EntrySerializer(serializers.ModelSerializer): + """ + REST API Serializer for Entries. + The djangorestframework plugin will analyse the model `Entry` and parse all fields in the API. + """ + + class Meta: + model = Entry + fields = '__all__' diff --git a/apps/activity/api/urls.py b/apps/activity/api/urls.py index 79e0ba30..3a2495fb 100644 --- a/apps/activity/api/urls.py +++ b/apps/activity/api/urls.py @@ -1,7 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet +from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet, EntryViewSet def register_activity_urls(router, path): @@ -11,3 +11,4 @@ def register_activity_urls(router, path): router.register(path + '/activity', ActivityViewSet) router.register(path + '/type', ActivityTypeViewSet) router.register(path + '/guest', GuestViewSet) + router.register(path + '/entry', EntryViewSet) diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 9d106ee5..764f2ac3 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -5,8 +5,8 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter from api.viewsets import ReadProtectedModelViewSet -from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer -from ..models import ActivityType, Activity, Guest +from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer, EntrySerializer +from ..models import ActivityType, Activity, Guest, Entry class ActivityTypeViewSet(ReadProtectedModelViewSet): @@ -43,3 +43,15 @@ class GuestViewSet(ReadProtectedModelViewSet): serializer_class = GuestSerializer filter_backends = [SearchFilter] search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] + + +class EntryViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer, + then render it on /api/activity/entry/ + """ + queryset = Entry.objects.all() + serializer_class = EntrySerializer + filter_backends = [SearchFilter] + search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] diff --git a/apps/activity/models.py b/apps/activity/models.py index 4bf92e23..9e3ea296 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -2,7 +2,9 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models +from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import ValidationError from note.models import NoteUser, Transaction @@ -103,7 +105,14 @@ class Activity(models.Model): class Entry(models.Model): + activity = models.ForeignKey( + Activity, + on_delete=models.PROTECT, + verbose_name=_("activity"), + ) + time = models.DateTimeField( + auto_now_add=True, verbose_name=_("entry time"), ) @@ -113,6 +122,47 @@ class Entry(models.Model): verbose_name=_("note"), ) + guest = models.OneToOneField( + 'activity.Guest', + on_delete=models.PROTECT, + null=True, + ) + + class Meta: + unique_together = (('activity', 'note', 'guest', ), ) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) + if qs.exists(): + raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) + + if self.guest: + self.note = self.guest.inviter + + insert = not self.pk + if insert: + if self.note.balance < 0: + raise ValidationError(_("The balance is negative.")) + + ret = super().save(force_insert, force_update, using, update_fields) + + if insert and self.guest: + GuestTransaction.objects.create( + source=self.note, + source_alias=self.note.user.username, + destination=self.activity.organizer.note, + destination_alias=self.activity.organizer.name, + quantity=1, + amount=self.activity.activity_type.guest_entry_fee, + reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name, + valid=True, + guest=self.guest, + ).save() + + return ret + class Guest(models.Model): """ @@ -141,11 +191,14 @@ class Guest(models.Model): verbose_name=_("inviter"), ) - entry = models.OneToOneField( - Entry, - on_delete=models.PROTECT, - null=True, - ) + @property + def has_entry(self): + try: + if self.entry: + return True + return False + except AttributeError: + return False class Meta: verbose_name = _("guest") diff --git a/apps/activity/tables.py b/apps/activity/tables.py index 0e9466e4..449ee321 100644 --- a/apps/activity/tables.py +++ b/apps/activity/tables.py @@ -35,8 +35,8 @@ class GuestTable(tables.Table): empty_values=(), attrs={ "td": { - "class": lambda record: "" if record.entry else "validate btn btn-danger", - "onclick": lambda record: "" if record.entry else "remove_guest(" + str(record.pk) + ")" + "class": lambda record: "" if record.has_entry else "validate btn btn-danger", + "onclick": lambda record: "" if record.has_entry else "remove_guest(" + str(record.pk) + ")" } } ) @@ -50,8 +50,8 @@ class GuestTable(tables.Table): fields = ("last_name", "first_name", "inviter", ) def render_entry(self, record): - if record.entry: - return str(record.date) + if record.has_entry: + return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, ))) return _("remove").capitalize() @@ -83,6 +83,8 @@ class EntryTable(tables.Table): template_name = 'django_tables2/bootstrap4.html' row_attrs = { 'class': 'table-row', - 'id': lambda record: "row-" + str(record.type), - 'data-href': lambda record: record.type + 'id': lambda record: "row-" + ("guest-" if isinstance(record, Guest) else "membership-") + str(record.pk), + 'data-type': lambda record: "guest" if isinstance(record, Guest) else "membership", + 'data-id': lambda record: record.pk, + 'data-inviter': lambda record: record.inviter.pk if isinstance(record, Guest) else "", } diff --git a/apps/activity/views.py b/apps/activity/views.py index 01b0e7f8..bb97c2c3 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -86,13 +86,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): if "search" in self.request.GET: pattern = self.request.GET["search"] - print(pattern) + if not pattern: + pattern = "^$" + + if pattern[0] != "^": + pattern = "^" + pattern guest_qs = Guest.objects\ .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ .filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern) | Q(inviter__alias__name__regex=pattern) - | Q(inviter__alias__normalized_name__startswith=Alias.normalize(pattern)))\ + | Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern)))\ .distinct()[:20] for guest in guest_qs: guest.type = "Invité" @@ -106,9 +110,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): .filter(Q(note__polymorphic_ctype__model="noteuser") & (Q(note__noteuser__user__first_name__regex=pattern) | Q(note__noteuser__user__last_name__regex=pattern) - | Q(name__regex="^" + pattern) - | Q(normalized_name__startswith=Alias.normalize(pattern))))\ - .distinct()[:20] + | Q(name__regex=pattern) + | Q(normalized_name__regex=Alias.normalize(pattern))))\ + .distinct("username")[:20] for note in note_qs: note.type = "Adhérent" matched.append(note) diff --git a/templates/activity/activity_entry.html b/templates/activity/activity_entry.html index e09d0e8f..c34d2b0a 100644 --- a/templates/activity/activity_entry.html +++ b/templates/activity/activity_entry.html @@ -8,6 +8,8 @@ {% block content %} +
+
{% render_table table %}
@@ -18,13 +20,55 @@ old_pattern = null; alias_obj = $("#alias"); - alias_obj.keyup(function() { + function reloadTable(force=false) { let pattern = alias_obj.val(); - if (pattern === old_pattern || pattern === "") + if ((pattern === old_pattern || pattern === "") && !force) return; - $("#entry_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #entry_table"); - }); + $("#entry_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init); + refreshBalance(); + } + + alias_obj.keyup(reloadTable); + + $(document).ready(init); + + function init() { + $(".table-row").click(function(e) { + let target = e.target.parentElement; + target = $("#" + target.id); + + let type = target.attr("data-type"); + let id = target.attr("data-id"); + + if (type === "membership") { + $.post("/api/activity/entry/?format=json", { + csrfmiddlewaretoken: CSRF_TOKEN, + activity: {{ activity.id }}, + note: id, + guest: null + }).done(function () { + addMsg("Entrée effectuée !", "success"); + reloadTable(true); + }).fail(function(xhr) { + errMsg(xhr.responseJSON); + }); + } + else { + } + $.post("/api/activity/entry/?format=json", { + csrfmiddlewaretoken: CSRF_TOKEN, + activity: {{ activity.id }}, + note: target.attr("data-inviter"), + guest: id + }).done(function () { + addMsg("Entrée effectuée !", "success"); + reloadTable(true); + }).fail(function(xhr) { + errMsg(xhr.responseJSON); + }); + }); + } {% endblock %}