1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

People can join activities

This commit is contained in:
Yohann D'ANELLO 2020-03-28 13:38:31 +01:00
parent a805e41367
commit 81cfaf12fa
7 changed files with 151 additions and 24 deletions

View File

@ -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__'

View File

@ -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)

View File

@ -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', ]

View File

@ -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")

View File

@ -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 "",
}

View File

@ -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)

View File

@ -8,6 +8,8 @@
{% block content %}
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<hr>
<div id="entry_table">
{% render_table table %}
</div>
@ -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);
});
});
}
</script>
{% endblock %}