Skip to content

Commit

Permalink
Check-in: New flags for check-in lists (pretix#3577)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphaelm authored Oct 23, 2023
1 parent da9aa3e commit a083189
Show file tree
Hide file tree
Showing 14 changed files with 82 additions and 13 deletions.
8 changes: 7 additions & 1 deletion doc/api/resources/checkinlists.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,18 @@ allow_entry_after_exit boolean If ``true``, su
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
ignore_in_statistics boolean If ``true``, check-ins on this list will be ignored in most reporting features.
consider_tickets_used boolean If ``true`` (default), tickets checked in on this list will be considered "used" by other functionality, i.e. when checking if they can still be canceled.
===================================== ========================== =======================================================

.. versionchanged:: 4.12

The ``addon_match`` attribute has been added.

.. versionchanged:: 2023.9

The ``ignore_in_statistics`` and ``consider_tickets_used`` attributes have been added.

Endpoints
---------

Expand Down Expand Up @@ -767,4 +773,4 @@ Order position endpoints
:statuscode 404: The requested order position or check-in list does not exist.


.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
.. _security issues: https://pretix.eu/about/de/blog/20220705-release-4111/
2 changes: 1 addition & 1 deletion src/pretix/api/serializers/checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Meta:
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', 'exit_all_at', 'addon_match')
'rules', 'exit_all_at', 'addon_match', 'ignore_in_statistics', 'consider_tickets_used')

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
22 changes: 22 additions & 0 deletions src/pretix/base/migrations/0247_checkinlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-09-06 11:58

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0246_bigint"),
]

operations = [
migrations.AddField(
model_name="checkinlist",
name="consider_tickets_used",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="checkinlist",
name="ignore_in_statistics",
field=models.BooleanField(default=False),
),
]
10 changes: 10 additions & 0 deletions src/pretix/base/models/checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ class CheckinList(LoggedModel):
'and valid for check-in regardless of which date they are purchased for. '
'You can limit their validity through the advanced check-in rules, '
'though.'))
ignore_in_statistics = models.BooleanField(
verbose_name=pgettext_lazy('checkin', 'Ignore check-ins on this list in statistics'),
default=False
)
consider_tickets_used = models.BooleanField(
verbose_name=pgettext_lazy('checkin', 'Tickets with a check-in on this list should be considered "used"'),
help_text=_('This is relevant in various situations, e.g. for deciding if a ticket can still be canceled by '
'the customer.'),
default=True
)
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
default=False,
help_text=_('With this option, people will be able to check in even if the '
Expand Down
6 changes: 3 additions & 3 deletions src/pretix/base/models/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ def user_change_allowed(self) -> bool:
positions = list(
self.positions.all().annotate(
has_variations=Exists(ItemVariation.objects.filter(item_id=OuterRef('item_id'))),
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('issued_gift_cards')
)
if self.event.settings.change_allow_user_if_checked_in:
Expand Down Expand Up @@ -665,7 +665,7 @@ def user_cancel_allowed(self) -> bool:
return False
positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('issued_gift_cards')
)
cancelable = all([op.item.allow_cancel and not op.has_checkin and not op.blocked for op in positions])
Expand Down Expand Up @@ -820,7 +820,7 @@ def can_modify_answers(self) -> bool:

positions = list(
self.positions.all().annotate(
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk')))
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
).select_related('item').prefetch_related('item__questions')
)
if not self.event.settings.allow_modifications_after_checkin:
Expand Down
2 changes: 1 addition & 1 deletion src/pretix/base/services/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -1996,7 +1996,7 @@ def set_addons(self, addons, limit_main_positions=None):
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
if a.checkins.exists():
if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError(
error_messages['addon_already_checked_in'] % {
'addon': str(a.item.name),
Expand Down
3 changes: 2 additions & 1 deletion src/pretix/control/forms/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2072,7 +2072,8 @@ def filter_qs(self, qs):
qs = qs.filter(Q(valid_until__isnull=False) & Q(valid_until__lt=now())).filter(redeemed=0)
elif s == 'c':
checkins = Checkin.objects.filter(
position__voucher=OuterRef('pk')
position__voucher=OuterRef('pk'),
list__consider_tickets_used=True,
)
qs = qs.annotate(has_checkin=Exists(checkins)).filter(
redeemed__gt=0, has_checkin=True
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ <h3 class="panel-title">
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% else %}
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-check {% if c.list.consider_tickets_used %}text-success{% else %}text-muted{% endif %}" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% endfor %}
{% endif %}
Expand Down
4 changes: 2 additions & 2 deletions src/pretix/plugins/sendmail/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ def send(self):

if self.rule.checked_in_status == "no_checkin":
filter_orders_by_op = True
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
op_qs = op_qs.filter(~Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True)))
elif self.rule.checked_in_status == "checked_in":
filter_orders_by_op = True
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'))))
op_qs = op_qs.filter(Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True)))

status_q = Q(status__in=self.rule.restrict_to_status)
if 'n__pending_approval' in self.rule.restrict_to_status:
Expand Down
1 change: 1 addition & 0 deletions src/pretix/plugins/sendmail/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
any_checkins=Exists(
Checkin.objects.filter(
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
list__consider_tickets_used=True,
)
),
matching_checkins=Exists(
Expand Down
3 changes: 2 additions & 1 deletion src/pretix/plugins/sendmail/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ def get_object_queryset(self, form):
any_checkins=Exists(
Checkin.all.filter(
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
successful=True
successful=True,
list__consider_tickets_used=True,
)
)
)
Expand Down
10 changes: 8 additions & 2 deletions src/pretix/presale/views/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ def get_context_data(self, **kwargs):
qs = qs.annotate(
checkin_count=Subquery(
Checkin.objects.filter(
successful=True, type=Checkin.TYPE_ENTRY, position_id=OuterRef('pk')
successful=True,
type=Checkin.TYPE_ENTRY,
position_id=OuterRef('pk'),
list__consider_tickets_used=True,
).order_by().values('position').annotate(c=Count('*')).values('c')
)
)
Expand Down Expand Up @@ -358,7 +361,10 @@ def get_context_data(self, **kwargs):
qs = qs.annotate(
checkin_count=Subquery(
Checkin.objects.filter(
successful=True, type=Checkin.TYPE_ENTRY, position_id=OuterRef('pk')
successful=True,
type=Checkin.TYPE_ENTRY,
position_id=OuterRef('pk'),
list__consider_tickets_used=True,
).order_by().values('position').annotate(c=Count('*')).values('c')
)
)
Expand Down
2 changes: 2 additions & 0 deletions src/tests/api/test_checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ def order(event, item, other_item, taxrule):
"subevent": None,
"exit_all_at": None,
"addon_match": False,
"ignore_in_statistics": False,
"consider_tickets_used": True,
"rules": {}
}

Expand Down
20 changes: 20 additions & 0 deletions src/tests/presale/test_orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,26 @@ def test_orders_cancel_forbidden_if_any_payment_made(self):
self.order.refresh_from_db()
assert self.order.status == Order.STATUS_PENDING

def test_orders_cancel_paid_checkin_list(self):
self.order.status = Order.STATUS_PAID
self.order.save()
with scopes_disabled():
cl = self.event.checkin_lists.create(name="Foo")
self.order.positions.first().checkins.create(list=cl)
self.event.settings.cancel_allow_user_paid = True
response = self.client.get(
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 302

cl.consider_tickets_used = False
cl.save()

response = self.client.get(
'/%s/%s/order/%s/%s/cancel' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
)
assert response.status_code == 200

def test_orders_cancel_forbidden(self):
self.event.settings.set('cancel_allow_user', False)
self.client.post(
Expand Down

0 comments on commit a083189

Please sign in to comment.