diff --git a/src/pretix/api/views/device.py b/src/pretix/api/views/device.py index b1aaf99598..cd17b524dc 100644 --- a/src/pretix/api/views/device.py +++ b/src/pretix/api/views/device.py @@ -8,7 +8,7 @@ from rest_framework.views import APIView from pretix.api.auth.device import DeviceTokenAuthentication from pretix.base.models import Device -from pretix.base.models.devices import generate_api_token +from pretix.base.models.devices import Gate, generate_api_token logger = logging.getLogger(__name__) @@ -28,14 +28,25 @@ class UpdateRequestSerializer(serializers.Serializer): software_version = serializers.CharField(max_length=190) +class GateSerializer(serializers.ModelSerializer): + class Meta: + model = Gate + fields = [ + 'id', + 'name', + 'identifier', + ] + + class DeviceSerializer(serializers.ModelSerializer): organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True) + gate = GateSerializer(read_only=True) class Meta: model = Device fields = [ 'organizer', 'device_id', 'unique_serial', 'api_token', - 'name', 'security_profile' + 'name', 'security_profile', 'gate' ] diff --git a/src/pretix/base/migrations/0168_auto_20201023_1447.py b/src/pretix/base/migrations/0168_auto_20201023_1447.py new file mode 100644 index 0000000000..f3166b3b4d --- /dev/null +++ b/src/pretix/base/migrations/0168_auto_20201023_1447.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.9 on 2020-10-23 14:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0167_checkinlist_exit_all_at'), + ] + + operations = [ + migrations.CreateModel( + name='Gate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=190)), + ('identifier', models.CharField(max_length=190)), + ('organizer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='gates', to='pretixbase.Organizer')), + ], + ), + migrations.AddField( + model_name='checkin', + name='gate', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.Gate'), + ), + migrations.AddField( + model_name='device', + name='gate', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='devices', to='pretixbase.Gate'), + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index a980d6aefc..ac4d3cae32 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -2,7 +2,7 @@ from ..settings import GlobalSettingsObject_SettingsStore from .auth import U2FDevice, User, WebAuthnDevice from .base import CachedFile, LoggedModel, cachedfile_name from .checkin import Checkin, CheckinList -from .devices import Device +from .devices import Device, Gate from .event import ( Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue, RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token, @@ -19,8 +19,8 @@ from .notifications import NotificationSetting from .orders import ( AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition, InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund, - QuestionAnswer, cachedcombinedticket_name, cachedticket_name, - generate_position_secret, generate_secret, + QuestionAnswer, RevokedTicketSecret, cachedcombinedticket_name, + cachedticket_name, generate_position_secret, generate_secret, ) from .organizer import ( Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite, diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index c350e703cc..8afad4360a 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -159,6 +159,9 @@ class Checkin(models.Model): device = models.ForeignKey( 'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True ) + gate = models.ForeignKey( + 'pretixbase.Gate', related_name='checkins', on_delete=models.SET_NULL, null=True, blank=True + ) auto_checked_in = models.BooleanField(default=False) objects = ScopedManager(organizer='position__order__event__organizer') diff --git a/src/pretix/base/models/devices.py b/src/pretix/base/models/devices.py index 5347e98acc..ae659a9d01 100644 --- a/src/pretix/base/models/devices.py +++ b/src/pretix/base/models/devices.py @@ -1,5 +1,6 @@ import string +from django.core.exceptions import ValidationError from django.db import models from django.db.models import Max from django.utils.crypto import get_random_string @@ -34,12 +35,64 @@ def generate_api_token(): return token +class Gate(LoggedModel): + organizer = models.ForeignKey( + 'pretixbase.Organizer', + on_delete=models.PROTECT, + related_name='gates' + ) + name = models.CharField( + verbose_name=_("Name"), + max_length=190, + ) + identifier = models.CharField( + max_length=190, blank=True, + verbose_name=_("Internal identifier"), + help_text=_('You can enter any value here to make it easier to match the data with other sources. If you do ' + 'not input one, we will generate one automatically.') + ) + + class Meta: + ordering = ('name',) + + def __str__(self): + return self.name + + def clean_identifier(self, code): + Gate._clean_identifier(self.organizer, code, self) + + @staticmethod + def _clean_identifier(organizer, code, instance=None): + qs = Gate.objects.filter(organizer=organizer, identifier__iexact=code) + if instance: + qs = qs.exclude(pk=instance.pk) + if qs.exists(): + raise ValidationError(_('This identifier is already used for a different question.')) + + def save(self, *args, **kwargs): + if not self.identifier: + charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789') + while True: + code = get_random_string(length=8, allowed_chars=charset) + if not Gate.objects.filter(organizer=self.organizer, identifier=code).exists(): + self.identifier = code + break + return super().save(*args, **kwargs) + + class Device(LoggedModel): organizer = models.ForeignKey( 'pretixbase.Organizer', on_delete=models.PROTECT, related_name='devices' ) + gate = models.ForeignKey( + 'pretixbase.Gate', + verbose_name=_('Gate'), + on_delete=models.SET_NULL, + null=True, blank=True, + related_name='devices' + ) device_id = models.PositiveIntegerField() unique_serial = models.CharField(max_length=190, default=generate_serial, unique=True) initialization_token = models.CharField(max_length=190, default=generate_initialization_token, unique=True) diff --git a/src/pretix/base/services/checkin.py b/src/pretix/base/services/checkin.py index 03c3e17491..dda99cb7d5 100644 --- a/src/pretix/base/services/checkin.py +++ b/src/pretix/base/services/checkin.py @@ -230,6 +230,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, list=clist, datetime=dt, device=device, + gate=device.gate if device else None, nonce=nonce, forced=force and not entry_allowed, ) diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 38bc312b62..edc76145a2 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -15,7 +15,7 @@ from pretix.api.models import WebHook from pretix.api.webhooks import get_all_webhook_events from pretix.base.forms import I18nModelForm, SettingsForm from pretix.base.forms.widgets import SplitDateTimePickerWidget -from pretix.base.models import Device, GiftCard, Organizer, Team +from pretix.base.models import Device, Gate, GiftCard, Organizer, Team from pretix.control.forms import ( ExtFileField, FontSelect, MultipleLanguagesWidget, SplitDateTimeField, ) @@ -175,6 +175,17 @@ class TeamForm(forms.ModelForm): return data +class GateForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + kwargs.pop('organizer') + super().__init__(*args, **kwargs) + + class Meta: + model = Gate + fields = ['name', 'identifier'] + + class DeviceForm(forms.ModelForm): def __init__(self, *args, **kwargs): @@ -183,6 +194,7 @@ class DeviceForm(forms.ModelForm): self.fields['limit_events'].queryset = organizer.events.all().order_by( '-has_subevents', '-date_from' ) + self.fields['gate'].queryset = organizer.gates.all() def clean(self): d = super().clean() @@ -193,7 +205,7 @@ class DeviceForm(forms.ModelForm): class Meta: model = Device - fields = ['name', 'all_events', 'limit_events', 'security_profile'] + fields = ['name', 'all_events', 'limit_events', 'security_profile', 'gate'] widgets = { 'limit_events': forms.CheckboxSelectMultiple(attrs={ 'data-inverse-dependency': '#id_all_events', diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 0767a93844..4ca2621245 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -412,6 +412,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.team.created': _('The team has been created.'), 'pretix.team.changed': _('The team settings have been changed.'), 'pretix.team.deleted': _('The team has been deleted.'), + 'pretix.gate.created': _('The gate has been created.'), + 'pretix.gate.changed': _('The gate has been changed.'), + 'pretix.gate.deleted': _('The gate has been deleted.'), 'pretix.subevent.deleted': pgettext_lazy('subevent', 'The event date has been deleted.'), 'pretix.subevent.canceled': pgettext_lazy('subevent', 'The event date has been canceled.'), 'pretix.subevent.changed': pgettext_lazy('subevent', 'The event date has been changed.'), diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index aae6aef891..5d59d368f7 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -454,8 +454,23 @@ def get_organizer_navigation(request): 'url': reverse('control:organizer.devices', kwargs={ 'organizer': request.organizer.slug }), - 'active': 'organizer.device' in url.url_name, 'icon': 'tablet', + 'children': [ + { + 'label': _('Devices'), + 'url': reverse('control:organizer.devices', kwargs={ + 'organizer': request.organizer.slug + }), + 'active': 'organizer.device' in url.url_name, + }, + { + 'label': _('Gates'), + 'url': reverse('control:organizer.gates', kwargs={ + 'organizer': request.organizer.slug + }), + 'active': 'organizer.gate' in url.url_name, + } + ] }) if 'can_manage_gift_cards' in request.orgapermset: nav.append({ diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index c7eb5e524e..41325dd689 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -302,14 +302,14 @@ {% if c.auto_checked_in %} {% else %} - + {% endif %} {% elif c.forced %} - + {% elif c.auto_checked_in %} - + {% else %} - + {% endif %} {% endfor %} {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html index 56075a7d56..45b2f6dbed 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html @@ -14,10 +14,17 @@ {% endif %} {% csrf_token %} {% bootstrap_form_errors form %} - {% bootstrap_field form.name layout="control" %} - {% bootstrap_field form.all_events layout="control" %} - {% bootstrap_field form.limit_events layout="control" %} - {% bootstrap_field form.security_profile layout="control" %} +
+