diff --git a/aleksis/apps/paweljong/filters.py b/aleksis/apps/paweljong/filters.py index d8783fcb6b0285101b723fbc1059ffdc543cf7c3..15b5df5a74773f918bbfdb48436edaab847809e3 100644 --- a/aleksis/apps/paweljong/filters.py +++ b/aleksis/apps/paweljong/filters.py @@ -9,16 +9,23 @@ from .models import Event, EventRegistration, Terms, Voucher class EventRegistrationFilter(FilterSet): + person = MultipleCharFilter( + [ + "person__first_name__icontains", + "person__last_name__icontains", + ], + label=_("Search by name"), + ) + class Meta: model = EventRegistration - fields = ["person", "event", "date_registered", "states", "retracted"] + fields = ["states", "retracted"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.form.layout = Layout( - Row("person", "event", "states", "retracted"), - Row("date_registered"), + Row("person", "states", "retracted"), ) diff --git a/aleksis/apps/paweljong/migrations/0020_check_in.py b/aleksis/apps/paweljong/migrations/0020_check_in.py new file mode 100644 index 0000000000000000000000000000000000000000..4f092f7d06a8a95b009b392a5d091b0ebbb4214a --- /dev/null +++ b/aleksis/apps/paweljong/migrations/0020_check_in.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.13 on 2022-06-22 19:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('paweljong', '0019_retractions'), + ] + + operations = [ + migrations.AddField( + model_name='eventregistration', + name='checked_in', + field=models.BooleanField(default=False, verbose_name='Checked in'), + ), + migrations.AddField( + model_name='eventregistration', + name='checked_in_date', + field=models.DateTimeField(blank=True, null=True, verbose_name='Checked in at'), + ), + ] diff --git a/aleksis/apps/paweljong/models.py b/aleksis/apps/paweljong/models.py index 57c1dc3ab9bb2f61d725be959fb665e615b280d8..0e4cd37e18abfacaa9c8014070cb2c9a720c906b 100644 --- a/aleksis/apps/paweljong/models.py +++ b/aleksis/apps/paweljong/models.py @@ -2,6 +2,7 @@ from datetime import datetime from decimal import Decimal from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.text import slugify @@ -285,6 +286,17 @@ class EventRegistration(ExtensibleModel): retracted = models.BooleanField(verbose_name=_("Retracted"), default=False) retracted_date = models.DateField(verbose_name=_("Retracted at"), null=True, blank=True) + checked_in = models.BooleanField(verbose_name=_("Checked in"), default=False) + checked_in_date = models.DateTimeField(verbose_name=_("Checked in at"), null=True, blank=True) + + def mark_checked_in(self): + if not self.checked_in: + self.checked_in = True + self.checked_in_date = now() + self.save() + else: + raise ValidationError(_("Person is already checked in!")) + def retract(self): # Remove person from group self.event.linked_group.members.remove(self.person) diff --git a/aleksis/apps/paweljong/rules.py b/aleksis/apps/paweljong/rules.py index 5f0ee3bfddf8898d8b52f5806a2df47d7dba49bd..b524f2c3bd30cb59f5b37f2309fabb2239184393 100644 --- a/aleksis/apps/paweljong/rules.py +++ b/aleksis/apps/paweljong/rules.py @@ -1,4 +1,4 @@ -import rules; +import rules from aleksis.core.util.predicates import ( has_any_object, @@ -8,7 +8,13 @@ from aleksis.core.util.predicates import ( ) from .models import Event, EventRegistration, Terms, Voucher -from .predicates import is_event_published, is_organiser, is_own_registration, is_own_voucher, is_participant +from .predicates import ( + is_event_published, + is_organiser, + is_own_registration, + is_own_voucher, + is_participant, +) ## Vouchers @@ -66,7 +72,7 @@ view_event_predicate = ( rules.add_perm("paweljong.view_event_rule", view_event_predicate) # Event organiser view -view_event_detail_predicate = (has_person & is_organiser) +view_event_detail_predicate = has_person & is_organiser rules.add_perm("paweljong.view_event_detail_rule", view_event_detail_predicate) # Delete event @@ -110,6 +116,7 @@ rules.add_perm("paweljong.delete_registration_rule", delete_registration_predica change_registration_predicate = has_person & ( has_global_perm("paweljong.change_eventregistration") | has_object_perm("paweljong.change_eventregistration") + | is_organiser ) rules.add_perm("paweljong.change_registration_rule", change_registration_predicate) diff --git a/aleksis/apps/paweljong/tables.py b/aleksis/apps/paweljong/tables.py index f380dc94318e707a320407873fd7ccc54c40b59e..b93ff3ce92c4fbb75b37cd44c47b788d15539a70 100644 --- a/aleksis/apps/paweljong/tables.py +++ b/aleksis/apps/paweljong/tables.py @@ -63,9 +63,8 @@ class EventRegistrationsTable(tables.Table): attrs = {"class": "responsive-table highlight"} person = tables.Column() - event = tables.Column() - date_registered = tables.Column() states = tables.Column() + checked_in_date = tables.Column() retracted = tables.Column() view = tables.LinkColumn( "registration_by_pk", @@ -73,6 +72,12 @@ class EventRegistrationsTable(tables.Table): verbose_name=_("View registration"), text=_("View"), ) + check_in = tables.LinkColumn( + "check_in_registration_by_pk", + args=[A("pk")], + verbose_name=_("Check in"), + text=_("Check in"), + ) edit = tables.LinkColumn( "edit_registration_by_pk", args=[A("pk")], diff --git a/aleksis/apps/paweljong/templates/paweljong/event/detail.html b/aleksis/apps/paweljong/templates/paweljong/event/detail.html index 1adf1eb19146dbd5e621ee7b7aadb28936c8a262..f553959001486dc0e850f2a090d0a614c02cb880 100644 --- a/aleksis/apps/paweljong/templates/paweljong/event/detail.html +++ b/aleksis/apps/paweljong/templates/paweljong/event/detail.html @@ -65,12 +65,6 @@ {% endfor %} </td> </tr> - <tr> - <td>{% trans "Description" %}</td> - <td colspan="3"> - {{ event.information|add_class_to_el:"ul, browser-default"|safe }} - </td> - </tr> </table> </div> </div> @@ -89,6 +83,15 @@ </div> <h5>{% blocktrans %}Registrations{% endblocktrans %}</h5> + <form method="get"> + {% form form=registrations_filter.form %}{% endform %} + {% trans "Search" as caption %} + {% include "core/partials/save_button.html" with caption=caption icon="search" %} + <button type="reset" class="btn red waves-effect waves-light"> + <i class="material-icons left">clear</i> + {% trans "Clear" %} + </button> + </form> {% render_table registrations_table %} {% endblock %} diff --git a/aleksis/apps/paweljong/templates/paweljong/event_registration/full.html b/aleksis/apps/paweljong/templates/paweljong/event_registration/full.html index 64b19ce5f0c9c54423224b362a69c15232e57cc4..a077f759dbc303df89ce9fd60607c1f7e585ec38 100644 --- a/aleksis/apps/paweljong/templates/paweljong/event_registration/full.html +++ b/aleksis/apps/paweljong/templates/paweljong/event_registration/full.html @@ -24,6 +24,10 @@ <i class="material-icons left iconify" data-icon="mdi:edit"></i> {% trans "Edit" %} </a> + <a href="{% url 'check_in_registration_by_pk' registration.pk %}" class="btn waves-effect waves-light"> + <i class="material-icons left iconify" data-icon="akar-icons:check-in"></i> + {% trans "Check in" %} + </a> {% endif %} {% if can_retract_registration %} <a href="{% url 'retract_registration_by_pk' registration.pk %}" class="btn waves-effect waves-light"> @@ -215,10 +219,99 @@ </td> </tr> </tr> + <tr> + <tr> + <td> + <i class="material-icons iconify" data-icon="akar-icons:check-in"></i> + </td> + <td> + {% if registration.checked_in %} + {{ registration.checked_in_date }} + {% else %} + {% trans "No checked in yet." %} + {% endif %} + </td> + </tr> + </tr> </table> </div> </div> + <h5>{% trans "Invoice details" %}</h5> + <div class="col s12 m8"> + <div class="row"> + <div class="col s12 m6"> + <div class="card"> + <div class="card-content"> + <span class="card-title">{% trans "Billing information" %}</span> + <table class="highlight"> + <tr> + <td> + <i class="material-icons small iconify" data-icon="mdi:account-outline"></i> + </td> + <td>{{ invoice.billing_first_name }} {{invoice.billing_last_name }}</td> + </tr> + <tr> + <td rowspan="2"> + <i class="material-icons small iconify" data-icon="mdi:map-marker-outline"></i> + </td> + <td>{{ invoice.billing_address_1 }} {{ invoice.billing_address_2 }}</td> + </tr> + <tr> + <td>{{ invoice.billing_postcode }} {{ invoice.billing_city}}</td> + </tr> + <tr> + <td> + <i class="material-icons small iconify" data-icon="mdi:email-outline"></i> + </td> + <td> + <a href="mailto:{{ invoice.billing_email }}">{{ invoice.billing_email }}</a> + </td> + </tr> + </table> + </div> + </div> + </div> + <div class="col s12 m6"> + <div class="card"> + <div class="card-content"> + <span class="card-title">{% trans "Payment" %}</span> + <table class="highlight"> + <tr> + <td> + <i class="material-icons iconify" data-icon="{{ invoice.get_variant_icon }}"></i> + </td> + <td> + <select name="variant" disabled> + {% for choice in invoice.get_variant_choices %} + <option value="{{ choice.0 }}" {% if invoice.variant == choice.0 %}selected{% endif %}>{{ choice.1 }}</option> + {% endfor %} + </select> + </td> + </tr> + <tr> + <td> + <i class="material-icons iconify" data-icon="{{ invoice.get_status_icon }}"></i> + </td> + <td> + {{ invoice.get_status_display }} + </td> + </tr> + <tr> + <td> + <i class="material-icons iconify" data-icon="mdi:calendar-end"></i> + </td> + <td> + {{ invoice.due_date }} + </td> + </tr> + </table> + </div> + </div> + </div> + </div> + </div> + {% if registration.person.guardians.all %} <h5>{% trans "Guardians / Parents "%}</h5> {% for person in registration.person.guardians.all %} diff --git a/aleksis/apps/paweljong/urls.py b/aleksis/apps/paweljong/urls.py index 361c2424ea7fde61338543959d69b584e2c5e6ac..a47f695f70ac5c038d3c0e4e16b795a3e1627fad 100644 --- a/aleksis/apps/paweljong/urls.py +++ b/aleksis/apps/paweljong/urls.py @@ -78,6 +78,11 @@ urlpatterns = [ path("vouchers/<int:pk>/print", views.print_voucher, name="print_voucher_by_pk"), path("vouchers/", views.vouchers, name="vouchers"), path("event/lists/generate", views.generate_lists, name="generate_lists"), + path( + "event/registrations/<int:pk>/check_in", + views.CheckInRegistration.as_view(), + name="check_in_registration_by_pk", + ), path( "event/registrations/<int:pk>/retract", views.RetractRegistration.as_view(), diff --git a/aleksis/apps/paweljong/views.py b/aleksis/apps/paweljong/views.py index c7b240f5f485624bead3635b33517e9b4f7a0e78..3b1f54228b131fe5ce8cdce5d6d6b771ff3016e9 100644 --- a/aleksis/apps/paweljong/views.py +++ b/aleksis/apps/paweljong/views.py @@ -3,6 +3,7 @@ from typing import Optional from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.syndication.views import Feed +from django.core.exceptions import ValidationError from django.http import HttpRequest, HttpResponse from django.shortcuts import redirect, render from django.urls import reverse, reverse_lazy @@ -28,7 +29,7 @@ from aleksis.core.models import Activity, Group, Person from aleksis.core.util import messages from aleksis.core.util.core_helpers import get_site_preferences, objectgetter_optional -from .filters import EventFilter, VoucherFilter +from .filters import EventFilter, EventRegistrationFilter, VoucherFilter from .forms import ( EditEventForm, EditEventRegistrationForm, @@ -208,6 +209,14 @@ class EventRegistrationDetailView(PermissionRequiredMixin, DetailView): def get_queryset(self): return EventRegistration.objects.all() + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + + invoice = self.get_object().get_invoice() + context["invoice"] = invoice + + return context + class EventRegistrationDeleteView(PermissionRequiredMixin, AdvancedDeleteView): """Delete view for registrations.""" @@ -891,7 +900,7 @@ class RetractRegistration(PermissionRequiredMixin, View): registration.retract() messages.success(self.request, _("Registration successfully retracted.")) - return redirect("registrations") + return redirect("event_detail_by_name", slug=registration.event.slug) class EventDetailView(PermissionRequiredMixin, DetailView): @@ -910,9 +919,12 @@ class EventDetailView(PermissionRequiredMixin, DetailView): context = super().get_context_data(**kwargs) - # Registrations table registrations = EventRegistration.objects.filter(event=self.object) - registrations_table = EventRegistrationsTable(registrations) + registrations_filter = EventRegistrationFilter(self.request.GET, queryset=registrations) + context["registrations_filter"] = registrations_filter + + # Registrations table + registrations_table = EventRegistrationsTable(registrations_filter.qs) RequestConfig(self.request).configure(registrations_table) context["registrations_table"] = registrations_table @@ -961,3 +973,19 @@ class ViewTerms(PermissionRequiredMixin, DetailView): permission_required = "paweljong.can_view_terms_rule" model = Event slug_field = "slug" + + +class CheckInRegistration(PermissionRequiredMixin, View): + + permission_required = "paweljong.change_registration_rule" + + def get(self, *args, **kwargs): + registration = EventRegistration.objects.get(id=self.kwargs["pk"]) + + try: + registration.mark_checked_in() + messages.success(self.request, _("Successfully checked in.")) + except ValidationError: + messages.error(self.request, _("Person is already checked in!")) + + return redirect("event_detail_by_name", slug=registration.event.slug)