Improve activity interface

This commit is contained in:
Yohann D'ANELLO 2020-08-06 17:41:30 +02:00
parent dd4b24d999
commit 9c7cb07dec
13 changed files with 772 additions and 683 deletions

View File

@ -4,6 +4,7 @@
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "Pot", "name": "Pot",
"manage_entries": true,
"can_invite": true, "can_invite": true,
"guest_entry_fee": 500 "guest_entry_fee": 500
} }
@ -13,6 +14,7 @@
"pk": 2, "pk": 2,
"fields": { "fields": {
"name": "Soir\u00e9e de club", "name": "Soir\u00e9e de club",
"manage_entries": false,
"can_invite": false, "can_invite": false,
"guest_entry_fee": 0 "guest_entry_fee": 0
} }

View File

@ -43,7 +43,7 @@ class GuestForm(forms.ModelForm):
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
if self.activity.date_start > timezone.now(): if timezone.now() > timezone.localtime(self.activity.date_start):
self.add_error("inviter", _("You can't invite someone once the activity is started.")) self.add_error("inviter", _("You can't invite someone once the activity is started."))
if not self.activity.valid: if not self.activity.valid:
@ -52,20 +52,21 @@ class GuestForm(forms.ModelForm):
one_year = timedelta(days=365) one_year = timedelta(days=365)
qs = Guest.objects.filter( qs = Guest.objects.filter(
first_name=cleaned_data["first_name"], first_name__iexact=cleaned_data["first_name"],
last_name=cleaned_data["last_name"], last_name__iexact=cleaned_data["last_name"],
activity__date_start__gte=self.activity.date_start - one_year, activity__date_start__gte=self.activity.date_start - one_year,
entry__isnull=False,
) )
if len(qs) >= 5: if qs.count() >= 5:
self.add_error("last_name", _("This person has been already invited 5 times this year.")) self.add_error("last_name", _("This person has been already invited 5 times this year."))
qs = qs.filter(activity=self.activity) qs = qs.filter(activity=self.activity)
if qs.exists(): if qs.exists():
self.add_error("last_name", _("This person is already invited.")) self.add_error("last_name", _("This person is already invited."))
qs = Guest.objects.filter(inviter=cleaned_data["inviter"], activity=self.activity) if "inviter" in cleaned_data:
if len(qs) >= 3: if Guest.objects.filter(inviter=cleaned_data["inviter"], activity=self.activity).count() >= 3:
self.add_error("inviter", _("You can't invite more than 3 people to this activity.")) self.add_error("inviter", _("You can't invite more than 3 people to this activity."))
return cleaned_data return cleaned_data

View File

@ -27,11 +27,21 @@ class ActivityType(models.Model):
verbose_name=_('name'), verbose_name=_('name'),
max_length=255, max_length=255,
) )
manage_entries = models.BooleanField(
verbose_name=_('manage entries'),
help_text=_('Enable the support of entries for this activity.'),
default=False,
)
can_invite = models.BooleanField( can_invite = models.BooleanField(
verbose_name=_('can invite'), verbose_name=_('can invite'),
default=False,
) )
guest_entry_fee = models.PositiveIntegerField( guest_entry_fee = models.PositiveIntegerField(
verbose_name=_('guest entry fee'), verbose_name=_('guest entry fee'),
default=0,
) )
class Meta: class Meta:
@ -236,18 +246,18 @@ class Guest(models.Model):
one_year = timedelta(days=365) one_year = timedelta(days=365)
if not force_insert: if not force_insert:
if self.activity.date_start > timezone.now(): if timezone.now() > timezone.localtime(self.activity.date_start):
raise ValidationError(_("You can't invite someone once the activity is started.")) raise ValidationError(_("You can't invite someone once the activity is started."))
if not self.activity.valid: if not self.activity.valid:
raise ValidationError(_("This activity is not validated yet.")) raise ValidationError(_("This activity is not validated yet."))
qs = Guest.objects.filter( qs = Guest.objects.filter(
first_name=self.first_name, first_name__iexact=self.first_name,
last_name=self.last_name, last_name__iexact=self.last_name,
activity__date_start__gte=self.activity.date_start - one_year, activity__date_start__gte=self.activity.date_start - one_year,
) )
if len(qs) >= 5: if qs.count() >= 5:
raise ValidationError(_("This person has been already invited 5 times this year.")) raise ValidationError(_("This person has been already invited 5 times this year."))
qs = qs.filter(activity=self.activity) qs = qs.filter(activity=self.activity)
@ -255,7 +265,7 @@ class Guest(models.Model):
raise ValidationError(_("This person is already invited.")) raise ValidationError(_("This person is already invited."))
qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity) qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity)
if len(qs) >= 3: if qs.count() >= 3:
raise ValidationError(_("You can't invite more than 3 people to this activity.")) raise ValidationError(_("You can't invite more than 3 people to this activity."))
return super().save(force_insert, force_update, using, update_fields) return super().save(force_insert, force_update, using, update_fields)

View File

@ -1,6 +1,6 @@
# 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 django.utils import timezone
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables import django_tables2 as tables
@ -66,6 +66,10 @@ def get_row_class(record):
qs = Entry.objects.filter(note=record.note, activity=record.activity, guest=None) qs = Entry.objects.filter(note=record.note, activity=record.activity, guest=None)
if qs.exists(): if qs.exists():
c += " table-success" c += " table-success"
elif not record.note.user.memberships.filter(club=record.activity.attendees_club,
date_start__lte=timezone.now(),
date_end__gte=timezone.now()).exists():
c += " table-info"
elif record.note.balance < 0: elif record.note.balance < 0:
c += " table-danger" c += " table-danger"
return c return c

View File

@ -1,13 +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 datetime import datetime, timezone
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.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import F, Q from django.db.models import F, Q
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone
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
@ -46,12 +45,17 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now()) upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
context['upcoming'] = ActivityTable( context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")), data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
prefix='upcoming-', prefix='upcoming-',
) )
started_activities = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.filter(open=True, valid=True).all()
context["started_activities"] = started_activities
return context return context
@ -67,7 +71,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))) .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
context["guests"] = table context["guests"] = table
context["activity_started"] = datetime.now(timezone.utc) > self.object.date_start context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
return context return context
@ -148,7 +152,12 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
username=F("note__noteuser__user__username"), username=F("note__noteuser__user__username"),
note_name=F("name"), note_name=F("name"),
balance=F("note__balance"))\ balance=F("note__balance"))\
.filter(note__polymorphic_ctype__model="noteuser")\ .filter(note__noteuser__isnull=False)\
.filter(
note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now(),
)\
.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")) .filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
if pattern: if pattern:
note_qs = note_qs.filter( note_qs = note_qs.filter(

@ -1 +1 @@
Subproject commit 3806feb67fcb1fe822cfdedddbbc4ca7eeef3829 Subproject commit 4984159a61642a0d3668e85daf39472b59b86447

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,70 +7,7 @@
{% block content %} {% block content %}
<div id="activity_info" class="card bg-light shadow"> {% include "activity/activity_info.html" %}
<div class="card-header text-center">
<h4>{{ activity.name }}</h4>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.description }}</dd>
<dt class="col-xl-6">{% trans 'type'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.activity_type }}</dd>
<dt class="col-xl-6">{% trans 'start date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_start }}</dd>
<dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_end }}</dd>
{% if ".view_"|has_perm:activity.creater %}
<dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
{% endif %}
<dt class="col-xl-6">{% trans 'organizer'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.organizer.pk %}">{{ activity.organizer }}</a></dd>
<dt class="col-xl-6">{% trans 'attendees club'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.attendees_club.pk %}">{{ activity.attendees_club }}</a></dd>
<dt class="col-xl-6">{% trans 'can invite'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.can_invite|yesno }}</dd>
{% if activity.activity_type.can_invite %}
<dt class="col-xl-6">{% trans 'guest entry fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.guest_entry_fee|pretty_money }}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'valid'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.valid|yesno }}</dd>
<dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.open|yesno }}</dd>
</dl>
</div>
<div class="card-footer text-center">
{% if activity.open and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
{% endif %}
{% if activity.valid and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
{% endif %}
{% if not activity.open and ".change__valid"|has_perm:activity %}
<a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a>
{% endif %}
{% if ".change_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}"> {% trans "Invite" %}</a>
{% endif %}
</div>
</div>
{% if guests.data %} {% if guests.data %}
<hr> <hr>

View File

@ -76,7 +76,10 @@
note: id, note: id,
guest: null guest: null
}).done(function () { }).done(function () {
addMsg("Entrée effectuée !", "success", 4000); if (target.hasClass("table-info"))
addMsg("Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.", "warning", 10000);
else
addMsg("Entrée effectuée !", "success", 4000);
reloadTable(true); reloadTable(true);
}).fail(function(xhr) { }).fail(function(xhr) {
errMsg(xhr.responseJSON, 4000); errMsg(xhr.responseJSON, 4000);
@ -104,7 +107,10 @@
note: target.attr("data-inviter"), note: target.attr("data-inviter"),
guest: id guest: id
}).done(function () { }).done(function () {
addMsg("Entrée effectuée !", "success", 4000); if (target.hasClass("table-info"))
addMsg("Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.", "warning", 10000);
else
addMsg("Entrée effectuée !", "success", 4000);
reloadTable(true); reloadTable(true);
}).fail(function (xhr) { }).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000); errMsg(xhr.responseJSON, 4000);

View File

@ -0,0 +1,78 @@
{% load i18n %}
{% load perms %}
{% load pretty_money %}
{% url 'activity:activity_detail' activity.pk as activity_detail_url %}
<div id="activity_info" class="card bg-light shadow">
<div class="card-header text-center">
<h4>
{% if request.path_info != activity_detail_url %}
<a href="{{ activity_detail_url }}">{{ activity.name }}</a>
{% else %}
{{ activity.name }}
{% endif %}
</h4>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.description }}</dd>
<dt class="col-xl-6">{% trans 'type'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.activity_type }}</dd>
<dt class="col-xl-6">{% trans 'start date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_start }}</dd>
<dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_end }}</dd>
{% if ".view_"|has_perm:activity.creater %}
<dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
{% endif %}
<dt class="col-xl-6">{% trans 'organizer'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.organizer.pk %}">{{ activity.organizer }}</a></dd>
<dt class="col-xl-6">{% trans 'attendees club'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.attendees_club.pk %}">{{ activity.attendees_club }}</a></dd>
<dt class="col-xl-6">{% trans 'can invite'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.can_invite|yesno }}</dd>
{% if activity.activity_type.can_invite %}
<dt class="col-xl-6">{% trans 'guest entry fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.guest_entry_fee|pretty_money }}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'valid'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.valid|yesno }}</dd>
<dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.open|yesno }}</dd>
</dl>
</div>
<div class="card-footer text-center">
{% if activity.open and activity.activity_type.manage_entries and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
{% endif %}
{% if request.path_info == activity_detail_url %}
{% if activity.valid and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
{% endif %}
{% if not activity.open and ".change__valid"|has_perm:activity %}
<a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a>
{% endif %}
{% if ".change_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}"> {% trans "Invite" %}</a>
{% endif %}
{% endif %}
</div>
</div>

View File

@ -2,6 +2,14 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load i18n crispy_forms_tags%} {% load i18n crispy_forms_tags%}
{% block content %} {% block content %}
{% if started_activities %}
<h2>{% trans "Current activity" %}</h2>
{% for activity in started_activities %}
{% include "activity/activity_info.html" %}
<hr>
{% endfor %}
{% endif %}
<h2>{% trans "Upcoming activities" %}</h2> <h2>{% trans "Upcoming activities" %}</h2>
{% if upcoming.data %} {% if upcoming.data %}
{% render_table upcoming %} {% render_table upcoming %}

View File

@ -19,10 +19,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
<input type="radio" name="transaction_type" id="type_credit"> <input type="radio" name="transaction_type" id="type_credit">
{% trans "Credit" %} {% trans "Credit" %}
</label> </label>
<label type="type_debit" class="btn btn-sm btn-outline-primary"> {% if not activities_open %}
<input type="radio" name="transaction_type" id="type_debit"> <label type="type_debit" class="btn btn-sm btn-outline-primary">
{% trans "Debit" %} <input type="radio" name="transaction_type" id="type_debit">
</label> {% trans "Debit" %}
</label>
{% endif %}
{% endif %} {% endif %}
{% for activity in activities_open %} {% for activity in activities_open %}
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary"> <a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">