diff --git a/aleksis/apps/paweljong/forms.py b/aleksis/apps/paweljong/forms.py index c864f82da15981bb728a2a7ee16344ba70a950c5..c42d5b8f3b833ac300038e5285d9af75b9b4c4ee 100644 --- a/aleksis/apps/paweljong/forms.py +++ b/aleksis/apps/paweljong/forms.py @@ -501,3 +501,48 @@ class PersonGroupFormPerson(forms.Form): widget=forms.TextInput(attrs={"autofocus": "", "autocomplete": "off"}), help_text=_("Please enter a username."), ) + + +class EventCheckpointForm(forms.Form): + class Media: + js = ("https://unpkg.com/html5-qrcode", "js/paweljong/qrscanner.js", "js/paweljong/checkpoint.js") + + layout = Layout( + "comment", "use_latlon", + ) + + comment = forms.CharField( + required=True, + label=_("Comment"), + help_text=_("Please enter a comment describing the checkpoint (e.g. Dinner)."), + ) + + username = forms.CharField( + required=True, + label=_("Person"), + help_text=_("Please enter a username."), + widget=forms.HiddenInput(), + ) + + use_latlon = forms.BooleanField( + required=False, + label=_("Submit geolocation"), + initial=True, + ) + + lat = forms.DecimalField( + required=False, + min_value=-90.0, + max_value=90.0, + max_digits=10, + decimal_places=8, + widget=forms.HiddenInput(), + ) + lon = forms.DecimalField( + required=False, + min_value=-180.0, + max_value=180.0, + max_digits=11, + decimal_places=8, + widget=forms.HiddenInput(), + ) diff --git a/aleksis/apps/paweljong/migrations/0021_checkpoint.py b/aleksis/apps/paweljong/migrations/0021_checkpoint.py new file mode 100644 index 0000000000000000000000000000000000000000..aa2321d37c0d956b64c440fc58968effed2c48a2 --- /dev/null +++ b/aleksis/apps/paweljong/migrations/0021_checkpoint.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.13 on 2022-06-24 18:31 + +import django.contrib.sites.managers +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('core', '0041_update_gender_choices'), + ('paweljong', '0020_check_in'), + ] + + operations = [ + migrations.AlterField( + model_name='registrationstate', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.CreateModel( + name='Checkpoint', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('comment', models.CharField(max_length=60, verbose_name='Comment')), + ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Date and time of check')), + ('lat', models.DecimalField(blank=True, decimal_places=8, max_digits=10, null=True, verbose_name='Latitude of check')), + ('lon', models.DecimalField(blank=True, decimal_places=8, max_digits=11, null=True, verbose_name='Longitude of check')), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='checkpoints', to='paweljong.event', verbose_name='Related event')), + ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_checkpoints', to='core.person', verbose_name='Checked person')), + ('checked_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_checkpoints_created', to='core.person', verbose_name='Checked by person')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ], + options={ + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + ] diff --git a/aleksis/apps/paweljong/models.py b/aleksis/apps/paweljong/models.py index 0e4cd37e18abfacaa9c8014070cb2c9a720c906b..576dbaa05710d500c3070f91a46d522493322aef 100644 --- a/aleksis/apps/paweljong/models.py +++ b/aleksis/apps/paweljong/models.py @@ -406,3 +406,16 @@ class EventRegistration(ExtensibleModel): fields=["person", "event"], name="unique_person_registration_per_event" ) ] + + +class Checkpoint(ExtensibleModel): + event = models.ForeignKey(Event, verbose_name=_("Related event"), related_name="checkpoints", on_delete=models.CASCADE) + person = models.ForeignKey(Person, verbose_name=_("Checked person"), related_name="event_checkpoints", on_delete=models.CASCADE) + checked_by = models.ForeignKey(Person, verbose_name=_("Checked by person"), related_name="event_checkpoints_created", on_delete=models.CASCADE) + + + comment = models.CharField(max_length=60, verbose_name=_("Comment")) + + timestamp = models.DateTimeField(verbose_name=_("Date and time of check"), auto_now_add=True) + lat = models.DecimalField(max_digits=10, decimal_places=8, verbose_name=_("Latitude of check"), blank=True, null=True) + lon = models.DecimalField(max_digits=11, decimal_places=8, verbose_name=_("Longitude of check"), blank=True, null=True) diff --git a/aleksis/apps/paweljong/rules.py b/aleksis/apps/paweljong/rules.py index 7e52d04e137f80870edae358a8ebb5ab663a9824..1389ae879299d6a36c4ae8b3fe56198359cf3f5c 100644 --- a/aleksis/apps/paweljong/rules.py +++ b/aleksis/apps/paweljong/rules.py @@ -73,6 +73,10 @@ change_event_predicate = has_person & ( ) rules.add_perm("paweljong.change_event_rule", change_event_predicate) +# Checkpoint +checkpoint_predicate = change_event_predicate +rules.add_perm("paweljong.event_checkpoint_rule", checkpoint_predicate) + # View event view_event_predicate = ( is_event_published | (has_person & is_organiser) | has_object_perm("paweljong.view_event") diff --git a/aleksis/apps/paweljong/static/js/paweljong/checkpoint.js b/aleksis/apps/paweljong/static/js/paweljong/checkpoint.js new file mode 100644 index 0000000000000000000000000000000000000000..6ca40adbb2ae228eba2cd658acbb457850864521 --- /dev/null +++ b/aleksis/apps/paweljong/static/js/paweljong/checkpoint.js @@ -0,0 +1,18 @@ +function getCheckpointCoords() { + navigator.geolocation.getCurrentPosition(setCheckpointCoords); +} + +function setCheckpointCoords(position) { + $("[name='lat']").val(position.coords.latitude); + $("[name='lon']").val(position.coords.longitude); + + window.setTimeout(function() { + navigator.geolocation.getCurrentPosition(setCheckpointCoords); + }, 3000); +} + +$(document).ready(function($) { + if (navigator.geolocation) { + getCheckpointCoords(); + } +}); diff --git a/aleksis/apps/paweljong/static/js/paweljong/qrscanner.js b/aleksis/apps/paweljong/static/js/paweljong/qrscanner.js new file mode 100644 index 0000000000000000000000000000000000000000..85b5e4521e41a3c8592a7c00f91b3b5f5c4d00a8 --- /dev/null +++ b/aleksis/apps/paweljong/static/js/paweljong/qrscanner.js @@ -0,0 +1,20 @@ +const scannerDivId = "qr-reader"; +const scannerDiv = $("div#" + scannerDivId); +const scanner = new Html5Qrcode(scannerDivId); + +function onScanSuccess(decodedText, decodedResult) { + let targetId = scannerDiv.data("target-input"); + let target = $("[name='" + targetId + "']"); + + scanner.stop(); + + target.val(decodedText); + target.closest("form").submit(); +} + +$(document).ready(function($) { + let cameraConfig = { facingMode: "environment" }; + let scannerConfig = { fps: 10, qrbox: {width: 250, height: 250} }; + + scanner.start(cameraConfig, scannerConfig, onScanSuccess); +}); diff --git a/aleksis/apps/paweljong/templates/paweljong/event/checkpoint.html b/aleksis/apps/paweljong/templates/paweljong/event/checkpoint.html new file mode 100644 index 0000000000000000000000000000000000000000..deef5ccf5adf4104a031397c249169490c7064fa --- /dev/null +++ b/aleksis/apps/paweljong/templates/paweljong/event/checkpoint.html @@ -0,0 +1,23 @@ +{% extends "core/base.html" %} +{% load material_form i18n any_js %} + +{% block page_title %}{% blocktrans %}Checkpoint{% endblocktrans %}{% endblock %} +{% block browser_title %}{% blocktrans %}Checkpoint{% endblocktrans %}{% endblock %} + +{% block extra_head %} + {{ form.media.css }} +{% endblock %} + +{% block content %} + + <form method="post"> + {% csrf_token %} + {% form form=form %}{% form %} + {% include "core/partials/save_button.html" %} + </form> + + <div id="qr-reader" data-target-input="username"></div> + + {{ form.media.js }} + +{% endblock %} diff --git a/aleksis/apps/paweljong/templates/paweljong/event/detail.html b/aleksis/apps/paweljong/templates/paweljong/event/detail.html index 93ee3dad2341011009c09a54a0c2ed0c715ec649..032661c4d1bbf8880ec040542861e47f746bf2e6 100644 --- a/aleksis/apps/paweljong/templates/paweljong/event/detail.html +++ b/aleksis/apps/paweljong/templates/paweljong/event/detail.html @@ -15,15 +15,22 @@ <h4>{{ event }}</h4> {% has_perm 'paweljong.change_event_rule' user event as can_change_event_rule %} + {% has_perm 'paweljong.event_checkpoint_rulw' user event as can_checkpoint_rule %} - {% if can_change_event_rule %} <p> + {% if can_change_event_rule %} <a href="{% url 'edit_event_by_slug' event.slug %}" class="btn waves-effect waves-light"> <i class="material-icons left iconify" data-icon="mdi:edit"></i> {% trans "Edit" %} </a> + {% endif %} + {% if can_checkpoint %} + <a href="{% url 'event_by_name_checkpoint' event.slug %}" class="btn waves-effect waves-light"> + <i class="material-icons left iconify" data-icon="mdi:access-point-check"></i> + {% trans "Checkpoint" %} + </a> + {% endif %} </p> - {% endif %} <div class="card"> <div class="card-content"> diff --git a/aleksis/apps/paweljong/urls.py b/aleksis/apps/paweljong/urls.py index a47f695f70ac5c038d3c0e4e16b795a3e1627fad..a83430272092efded259e35760b0306e544c37d9 100644 --- a/aleksis/apps/paweljong/urls.py +++ b/aleksis/apps/paweljong/urls.py @@ -50,6 +50,7 @@ urlpatterns = [ ), path("event/<slug:slug>", views.EventFullView.as_view(), name="event_by_name"), path("event/<slug:slug>/detail", views.EventDetailView.as_view(), name="event_detail_by_name"), + path("event/<slug:slug>/checkpoint", views.EventCheckpointView.as_view(), name="event_by_name_checkpoint"), path( "event/<slug:slug>/start", views.RegisterEventStart.as_view(), diff --git a/aleksis/apps/paweljong/views.py b/aleksis/apps/paweljong/views.py index 47d228db56ab15f035c5deceb98719748c4f795f..23a067b5079395d00bd53b1e3d4c2815d34aac1f 100644 --- a/aleksis/apps/paweljong/views.py +++ b/aleksis/apps/paweljong/views.py @@ -5,10 +5,11 @@ 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.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator +from django.utils.http import urlencode from django.utils.text import slugify from django.utils.translation import ugettext as _ from django.views.decorators.cache import never_cache @@ -36,12 +37,13 @@ from .forms import ( EditInfoMailingForm, EditTermForm, EditVoucherForm, + EventCheckpointForm, GenerateListForm, PersonGroupFormPerson, RegistrationNotificationForm, RegistrationStatesForm, ) -from .models import Event, EventRegistration, InfoMailing, RegistrationState, Terms, Voucher +from .models import Checkpoint, Event, EventRegistration, InfoMailing, RegistrationState, Terms, Voucher from .tables import ( AdditionalFieldsTable, ChildGroupsTable, @@ -969,6 +971,43 @@ class PersonGroupView(PermissionRequiredMixin, FormView): return reverse("add_persons_to_group", kwargs={"pk": self.kwargs["pk"]}) +class EventCheckpointView(PermissionRequiredMixin, FormView): + + template_name = "paweljong/event/checkpoint.html" + permission_required = "paweljong.can_checkpoint" + form_class = EventCheckpointForm + + def get_initial(self): + initial = super().get_initial() or {} + if "comment" in self.request.GET: + initial["comment"] = self.request.GET.get("comment") + return initial + + def form_valid(self, form): + checkpoint = Checkpoint() + + checkpoint.event = get_object_or_404(Event, slug=self.kwargs["slug"]) + try: + checkpoint.person = Person.objects.get(user__username=form.cleaned_data["username"]) + except Person.DoesNotExist: + messages.error(self.request, _("The provided username is not linked to a person.")) + checkpoint.checked_by = self.request.user.person + + checkpoint.comment = form.cleaned_data["comment"] + checkpoint.timestamp = timezone.now() + if form.cleaned_data["use_latlon"]: + checkpoint.lat = form.cleaned_data["lat"] + checkpoint.lon = form.cleaned_data["lon"] + + checkpoint.save() + messages.success(self.request, _("{} successfully checked for {}.").format(str(checkpoint.person), str(checkpoint.comment))) + self._comment = checkpoint.comment + return super().form_valid(self) + + def get_success_url(self): + return reverse("event_by_name_checkpoint", kwargs={"slug": self.kwargs["slug"]}) + "?" + urlencode({"comment": self._comment}) + + class ViewTerms(PermissionRequiredMixin, DetailView): context_object_name = "event"