mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Add option to automatically check out all attendees at night (#1819)
This commit is contained in:
@@ -15,7 +15,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules')
|
||||
'rules', 'exit_all_at')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
18
src/pretix/base/migrations/0167_checkinlist_exit_all_at.py
Normal file
18
src/pretix/base/migrations/0167_checkinlist_exit_all_at.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.10 on 2020-10-20 06:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0166_auto_20201015_2029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='exit_all_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -30,7 +30,10 @@ class CheckinList(LoggedModel):
|
||||
help_text=_('Use this option to turn off warnings if a ticket is scanned a second time.'),
|
||||
default=False
|
||||
)
|
||||
|
||||
exit_all_at = models.DateTimeField(
|
||||
verbose_name=_('Automatically check out everyone at'),
|
||||
null=True, blank=True
|
||||
)
|
||||
auto_checkin_sales_channels = MultiStringField(
|
||||
default=[],
|
||||
blank=True,
|
||||
@@ -62,7 +65,7 @@ class CheckinList(LoggedModel):
|
||||
return qs
|
||||
|
||||
@property
|
||||
def inside_count(self):
|
||||
def positions_inside(self):
|
||||
return self.positions.annotate(
|
||||
last_entry=Subquery(
|
||||
Checkin.objects.filter(
|
||||
@@ -87,7 +90,11 @@ class CheckinList(LoggedModel):
|
||||
& Q(
|
||||
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
|
||||
)
|
||||
).count()
|
||||
)
|
||||
|
||||
@property
|
||||
def inside_count(self):
|
||||
return self.positions_inside.count()
|
||||
|
||||
@property
|
||||
@scopes_disabled()
|
||||
|
||||
@@ -7,11 +7,12 @@ from django.dispatch import receiver
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now, override
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Device, Order, OrderPosition, QuestionOption,
|
||||
)
|
||||
from pretix.base.signals import checkin_created, order_placed
|
||||
from pretix.base.signals import checkin_created, order_placed, periodic_task
|
||||
from pretix.helpers.jsonlogic import Logic
|
||||
|
||||
|
||||
@@ -262,5 +263,23 @@ def order_placed(sender, **kwargs):
|
||||
for cl in cls:
|
||||
if cl.all_products or op.item_id in {i.pk for i in cl.limit_products.all()}:
|
||||
if not cl.subevent_id or cl.subevent_id == op.subevent_id:
|
||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True)
|
||||
ci = Checkin.objects.create(position=op, list=cl, auto_checked_in=True, type=Checkin.TYPE_ENTRY)
|
||||
checkin_created.send(event, checkin=ci)
|
||||
|
||||
|
||||
@receiver(periodic_task, dispatch_uid="autocheckin_exit_all")
|
||||
@scopes_disabled()
|
||||
def process_exit_all(sender, **kwargs):
|
||||
qs = CheckinList.objects.filter(
|
||||
exit_all_at__lte=now(),
|
||||
exit_all_at__isnull=False
|
||||
).select_related('event', 'event__organizer')
|
||||
for cl in qs:
|
||||
for p in cl.positions_inside:
|
||||
with scope(organizer=cl.event.organizer):
|
||||
ci = Checkin.objects.create(
|
||||
position=p, list=cl, auto_checked_in=True, type=Checkin.TYPE_EXIT, datetime=cl.exit_all_at
|
||||
)
|
||||
checkin_created.send(cl.event, checkin=ci)
|
||||
cl.exit_all_at = cl.exit_all_at + timedelta(days=1)
|
||||
cl.save(update_fields=['exit_all_at'])
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
@@ -10,6 +13,21 @@ from pretix.base.models.checkin import CheckinList
|
||||
from pretix.control.forms.widgets import Select2
|
||||
|
||||
|
||||
class NextTimeField(forms.TimeField):
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
if value is None:
|
||||
return
|
||||
tz = get_current_timezone()
|
||||
result = make_aware(datetime.combine(
|
||||
now().astimezone(tz).date(),
|
||||
value,
|
||||
), tz)
|
||||
if result <= now():
|
||||
result += timedelta(days=1)
|
||||
return result
|
||||
|
||||
|
||||
class CheckinListForm(forms.ModelForm):
|
||||
def __init__(self, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
@@ -55,16 +73,19 @@ class CheckinListForm(forms.ModelForm):
|
||||
'allow_multiple_entries',
|
||||
'allow_entry_after_exit',
|
||||
'rules',
|
||||
'exit_all_at',
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '<[name$=all_products]'
|
||||
}),
|
||||
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
|
||||
'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}),
|
||||
}
|
||||
field_classes = {
|
||||
'limit_products': SafeModelMultipleChoiceField,
|
||||
'subevent': SafeModelChoiceField,
|
||||
'exit_all_at': NextTimeField,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
|
||||
{% bootstrap_field form.allow_multiple_entries layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
||||
|
||||
<h3>{% trans "Custom check-in rule" %}</h3>
|
||||
|
||||
@@ -299,7 +299,11 @@
|
||||
{% if line.checkins.all %}
|
||||
{% for c in line.checkins.all %}
|
||||
{% if c.type == "exit" %}
|
||||
<span class="fa fa-fw fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}"></span>
|
||||
{% if c.auto_checked_in %}
|
||||
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}"></span>
|
||||
{% endif %}
|
||||
{% elif c.forced %}
|
||||
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
|
||||
{% elif c.auto_checked_in %}
|
||||
|
||||
@@ -143,6 +143,7 @@ TEST_LIST_RES = {
|
||||
"allow_multiple_entries": False,
|
||||
"allow_entry_after_exit": True,
|
||||
"subevent": None,
|
||||
"exit_all_at": None,
|
||||
"rules": {}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from freezegun import freeze_time
|
||||
|
||||
from pretix.base.models import Checkin, Event, Order, OrderPosition, Organizer
|
||||
from pretix.base.services.checkin import (
|
||||
CheckInError, RequiredQuestionsError, perform_checkin,
|
||||
CheckInError, RequiredQuestionsError, perform_checkin, process_exit_all,
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ def event():
|
||||
date_from=now(),
|
||||
plugins='pretix.plugins.banktransfer'
|
||||
)
|
||||
event.settings.timezone = 'Europe/Berlin'
|
||||
with scope(organizer=o):
|
||||
yield event
|
||||
|
||||
@@ -600,3 +601,39 @@ def test_position_queries(django_assert_num_queries, position, clist):
|
||||
perform_checkin(position, clist, {})
|
||||
if 'sqlite' not in settings.DATABASES['default']['ENGINE']:
|
||||
assert any('FOR UPDATE' in s['sql'] for s in captured)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_auto_checkout_at_correct_time(event, position, clist):
|
||||
clist.exit_all_at = event.timezone.localize(datetime(2020, 1, 2, 3, 0))
|
||||
clist.save()
|
||||
with freeze_time("2020-01-01 10:00:00+01:00"):
|
||||
perform_checkin(position, clist, {})
|
||||
with freeze_time("2020-01-02 02:55:00+01:00"):
|
||||
process_exit_all(sender=None)
|
||||
assert position.checkins.count() == 1
|
||||
with freeze_time("2020-01-02 03:05:00+01:00"):
|
||||
process_exit_all(sender=None)
|
||||
assert clist.inside_count == 0
|
||||
assert position.checkins.count() == 2
|
||||
assert position.checkins.first().type == Checkin.TYPE_EXIT
|
||||
clist.refresh_from_db()
|
||||
assert clist.exit_all_at == event.timezone.localize(datetime(2020, 1, 3, 3, 0))
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_auto_check_out_only_if_checked_in(event, position, clist):
|
||||
clist.exit_all_at = event.timezone.localize(datetime(2020, 1, 2, 3, 0))
|
||||
clist.save()
|
||||
with freeze_time("2020-01-02 03:05:00+01:00"):
|
||||
process_exit_all(sender=None)
|
||||
assert position.checkins.count() == 0
|
||||
|
||||
with freeze_time("2020-01-02 04:05:00+01:00"):
|
||||
perform_checkin(position, clist, {})
|
||||
with freeze_time("2020-01-02 04:10:00+01:00"):
|
||||
perform_checkin(position, clist, {}, type=Checkin.TYPE_EXIT)
|
||||
|
||||
with freeze_time("2020-01-03 03:05:00+01:00"):
|
||||
process_exit_all(sender=None)
|
||||
assert position.checkins.count() == 2
|
||||
|
||||
@@ -4,6 +4,7 @@ from decimal import Decimal
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
from freezegun import freeze_time
|
||||
|
||||
from pretix.base.models import (
|
||||
Checkin, Event, Item, ItemAddOn, ItemCategory, LogEntry, Order,
|
||||
@@ -398,6 +399,7 @@ class CheckinListFormTest(SoupTest):
|
||||
organizer=self.orga1, name='30C3', slug='30c3',
|
||||
date_from=datetime(2013, 12, 26, tzinfo=timezone.utc),
|
||||
)
|
||||
self.event1.settings.timezone = 'Europe/Berlin'
|
||||
t = Team.objects.create(organizer=self.orga1, can_change_event_settings=True, can_view_orders=True)
|
||||
t.members.add(self.user)
|
||||
t.limit_events.add(self.event1)
|
||||
@@ -432,6 +434,32 @@ class CheckinListFormTest(SoupTest):
|
||||
with scopes_disabled():
|
||||
assert list(cl.limit_products.all()) == [self.item_ticket]
|
||||
|
||||
@freeze_time("2020-01-02 02:55:00+01:00")
|
||||
def test_update_exit_all_at_current_day(self):
|
||||
with scopes_disabled():
|
||||
cl = self.event1.checkin_lists.create(name='All', all_products=True)
|
||||
doc = self.get_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id))
|
||||
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||
form_data['exit_all_at'] = '03:00:00'
|
||||
doc = self.post_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id),
|
||||
form_data)
|
||||
assert doc.select(".alert-success")
|
||||
cl.refresh_from_db()
|
||||
assert cl.exit_all_at == self.event1.timezone.localize(datetime(2020, 1, 2, 3, 0))
|
||||
|
||||
@freeze_time("2020-01-02 03:05:00+01:00")
|
||||
def test_update_exit_all_at_next_day(self):
|
||||
with scopes_disabled():
|
||||
cl = self.event1.checkin_lists.create(name='All', all_products=True)
|
||||
doc = self.get_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id))
|
||||
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
|
||||
form_data['exit_all_at'] = '03:00:00'
|
||||
doc = self.post_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id),
|
||||
form_data)
|
||||
assert doc.select(".alert-success")
|
||||
cl.refresh_from_db()
|
||||
assert cl.exit_all_at == self.event1.timezone.localize(datetime(2020, 1, 3, 3, 0))
|
||||
|
||||
def test_delete(self):
|
||||
with scopes_disabled():
|
||||
cl = self.event1.checkin_lists.create(name='All', all_products=True)
|
||||
|
||||
Reference in New Issue
Block a user