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 rest_framework import serializers
from ..models import ActivityType, Activity, Guest from ..models import ActivityType, Activity, Guest, Entry
class ActivityTypeSerializer(serializers.ModelSerializer): class ActivityTypeSerializer(serializers.ModelSerializer):
@ -37,3 +37,14 @@ class GuestSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Guest model = Guest
fields = '__all__' 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 # 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 .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet, EntryViewSet
def register_activity_urls(router, path): def register_activity_urls(router, path):
@ -11,3 +11,4 @@ def register_activity_urls(router, path):
router.register(path + '/activity', ActivityViewSet) router.register(path + '/activity', ActivityViewSet)
router.register(path + '/type', ActivityTypeViewSet) router.register(path + '/type', ActivityTypeViewSet)
router.register(path + '/guest', GuestViewSet) 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 rest_framework.filters import SearchFilter
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer, EntrySerializer
from ..models import ActivityType, Activity, Guest from ..models import ActivityType, Activity, Guest, Entry
class ActivityTypeViewSet(ReadProtectedModelViewSet): class ActivityTypeViewSet(ReadProtectedModelViewSet):
@ -43,3 +43,15 @@ class GuestViewSet(ReadProtectedModelViewSet):
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [SearchFilter] filter_backends = [SearchFilter]
search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] 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 # SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from note.models import NoteUser, Transaction from note.models import NoteUser, Transaction
@ -103,7 +105,14 @@ class Activity(models.Model):
class Entry(models.Model): class Entry(models.Model):
activity = models.ForeignKey(
Activity,
on_delete=models.PROTECT,
verbose_name=_("activity"),
)
time = models.DateTimeField( time = models.DateTimeField(
auto_now_add=True,
verbose_name=_("entry time"), verbose_name=_("entry time"),
) )
@ -113,6 +122,47 @@ class Entry(models.Model):
verbose_name=_("note"), 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): class Guest(models.Model):
""" """
@ -141,11 +191,14 @@ class Guest(models.Model):
verbose_name=_("inviter"), verbose_name=_("inviter"),
) )
entry = models.OneToOneField( @property
Entry, def has_entry(self):
on_delete=models.PROTECT, try:
null=True, if self.entry:
) return True
return False
except AttributeError:
return False
class Meta: class Meta:
verbose_name = _("guest") verbose_name = _("guest")

View File

@ -35,8 +35,8 @@ class GuestTable(tables.Table):
empty_values=(), empty_values=(),
attrs={ attrs={
"td": { "td": {
"class": lambda record: "" if record.entry else "validate btn btn-danger", "class": lambda record: "" if record.has_entry else "validate btn btn-danger",
"onclick": lambda record: "" if record.entry else "remove_guest(" + str(record.pk) + ")" "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", ) fields = ("last_name", "first_name", "inviter", )
def render_entry(self, record): def render_entry(self, record):
if record.entry: if record.has_entry:
return str(record.date) return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
return _("remove").capitalize() return _("remove").capitalize()
@ -83,6 +83,8 @@ class EntryTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'id': lambda record: "row-" + str(record.type), 'id': lambda record: "row-" + ("guest-" if isinstance(record, Guest) else "membership-") + str(record.pk),
'data-href': lambda record: record.type '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: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
print(pattern) if not pattern:
pattern = "^$"
if pattern[0] != "^":
pattern = "^" + pattern
guest_qs = Guest.objects\ guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern) .filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern)
| Q(inviter__alias__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] .distinct()[:20]
for guest in guest_qs: for guest in guest_qs:
guest.type = "Invité" guest.type = "Invité"
@ -106,9 +110,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
.filter(Q(note__polymorphic_ctype__model="noteuser") .filter(Q(note__polymorphic_ctype__model="noteuser")
& (Q(note__noteuser__user__first_name__regex=pattern) & (Q(note__noteuser__user__first_name__regex=pattern)
| Q(note__noteuser__user__last_name__regex=pattern) | Q(note__noteuser__user__last_name__regex=pattern)
| Q(name__regex="^" + pattern) | Q(name__regex=pattern)
| Q(normalized_name__startswith=Alias.normalize(pattern))))\ | Q(normalized_name__regex=Alias.normalize(pattern))))\
.distinct()[:20] .distinct("username")[:20]
for note in note_qs: for note in note_qs:
note.type = "Adhérent" note.type = "Adhérent"
matched.append(note) matched.append(note)

View File

@ -8,6 +8,8 @@
{% block content %} {% block content %}
<input id="alias" type="text" class="form-control" placeholder="Nom/note ..."> <input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<hr>
<div id="entry_table"> <div id="entry_table">
{% render_table table %} {% render_table table %}
</div> </div>
@ -18,13 +20,55 @@
old_pattern = null; old_pattern = null;
alias_obj = $("#alias"); alias_obj = $("#alias");
alias_obj.keyup(function() { function reloadTable(force=false) {
let pattern = alias_obj.val(); let pattern = alias_obj.val();
if (pattern === old_pattern || pattern === "") if ((pattern === old_pattern || pattern === "") && !force)
return; 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> </script>
{% endblock %} {% endblock %}