mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Store all check-in attempts, not only successful ones (#2074)
This commit is contained in:
60
src/pretix/base/migrations/0192_checkin_more_fields.py
Normal file
60
src/pretix/base/migrations/0192_checkin_more_fields.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 3.2.2 on 2021-05-11 16:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0191_event_last_modified'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='error_explanation',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='error_reason',
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='raw_barcode',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='raw_item',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='raw_subevent',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.subevent'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='raw_variation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='checkins', to='pretixbase.itemvariation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
|
||||
name='successful',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='checkin',
|
||||
name='position',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='all_checkins', to='pretixbase.orderposition'),
|
||||
),
|
||||
]
|
||||
@@ -31,6 +31,7 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -231,9 +232,14 @@ class CheckinList(LoggedModel):
|
||||
return rules
|
||||
|
||||
|
||||
class SuccessfulCheckinManager(ScopedManager(organizer='list__event__organizer').__class__):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(successful=True)
|
||||
|
||||
|
||||
class Checkin(models.Model):
|
||||
"""
|
||||
A check-in object is created when a person enters or exits the event.
|
||||
A check-in object is created when a ticket is scanned with our scanning apps.
|
||||
"""
|
||||
TYPE_ENTRY = 'entry'
|
||||
TYPE_EXIT = 'exit'
|
||||
@@ -241,13 +247,82 @@ class Checkin(models.Model):
|
||||
(TYPE_ENTRY, _('Entry')),
|
||||
(TYPE_EXIT, _('Exit')),
|
||||
)
|
||||
position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins', on_delete=models.CASCADE)
|
||||
|
||||
REASON_CANCELED = 'canceled'
|
||||
REASON_INVALID = 'invalid'
|
||||
REASON_UNPAID = 'unpaid'
|
||||
REASON_PRODUCT = 'product'
|
||||
REASON_RULES = 'rules'
|
||||
REASON_REVOKED = 'revoked'
|
||||
REASON_INCOMPLETE = 'incomplete'
|
||||
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
||||
REASON_ERROR = 'error'
|
||||
REASONS = (
|
||||
(REASON_CANCELED, _('Order canceled')),
|
||||
(REASON_INVALID, _('Unknown ticket')),
|
||||
(REASON_UNPAID, _('Ticket not paid')),
|
||||
(REASON_RULES, _('Forbidden by custom rule')),
|
||||
(REASON_REVOKED, _('Ticket code revoked/changed')),
|
||||
(REASON_INCOMPLETE, _('Information required')),
|
||||
(REASON_ALREADY_REDEEMED, _('Ticket already used')),
|
||||
(REASON_ERROR, _('Server error')),
|
||||
)
|
||||
|
||||
successful = models.BooleanField(
|
||||
default=True,
|
||||
)
|
||||
error_reason = models.CharField(
|
||||
max_length=100,
|
||||
choices=REASONS,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
error_explanation = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
position = models.ForeignKey(
|
||||
'pretixbase.OrderPosition',
|
||||
related_name='all_checkins',
|
||||
on_delete=models.CASCADE,
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
# For "raw" scans where we do not know which position they belong to (e.g. scan of signed
|
||||
# barcode that is not in database).
|
||||
raw_barcode = models.TextField(null=True, blank=True)
|
||||
raw_item = models.ForeignKey(
|
||||
'pretixbase.Item',
|
||||
related_name='checkins',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
)
|
||||
raw_variation = models.ForeignKey(
|
||||
'pretixbase.ItemVariation',
|
||||
related_name='checkins',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
)
|
||||
raw_subevent = models.ForeignKey(
|
||||
'pretixbase.SubEvent',
|
||||
related_name='checkins',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
# Datetime of checkin, might be different from created if past scans are uploaded
|
||||
datetime = models.DateTimeField(default=now)
|
||||
nonce = models.CharField(max_length=190, null=True, blank=True)
|
||||
|
||||
# Datetime of creation on server
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
|
||||
list = models.ForeignKey(
|
||||
'pretixbase.CheckinList', related_name='checkins', on_delete=models.PROTECT,
|
||||
)
|
||||
type = models.CharField(max_length=100, choices=CHECKIN_TYPES, default=TYPE_ENTRY)
|
||||
|
||||
nonce = models.CharField(max_length=190, null=True, blank=True)
|
||||
forced = models.BooleanField(default=False)
|
||||
device = models.ForeignKey(
|
||||
'pretixbase.Device', related_name='checkins', on_delete=models.PROTECT, null=True, blank=True
|
||||
@@ -257,7 +332,8 @@ class Checkin(models.Model):
|
||||
)
|
||||
auto_checked_in = models.BooleanField(default=False)
|
||||
|
||||
objects = ScopedManager(organizer='position__order__event__organizer')
|
||||
all = ScopedManager(organizer='list__event__organizer')
|
||||
objects = SuccessfulCheckinManager()
|
||||
|
||||
class Meta:
|
||||
ordering = (('-datetime'),)
|
||||
@@ -269,7 +345,8 @@ class Checkin(models.Model):
|
||||
|
||||
def save(self, **kwargs):
|
||||
super().save(**kwargs)
|
||||
self.position.order.touch()
|
||||
if self.position:
|
||||
self.position.order.touch()
|
||||
self.list.event.cache.delete('checkin_count')
|
||||
self.list.touch()
|
||||
|
||||
@@ -277,3 +354,7 @@ class Checkin(models.Model):
|
||||
super().delete(**kwargs)
|
||||
self.position.order.touch()
|
||||
self.list.touch()
|
||||
|
||||
@property
|
||||
def is_late_upload(self):
|
||||
return self.created and abs(self.created - self.datetime) > timedelta(minutes=2)
|
||||
|
||||
@@ -2054,6 +2054,14 @@ class OrderPosition(AbstractPosition):
|
||||
def sort_key(self):
|
||||
return self.addon_to.positionid if self.addon_to else self.positionid, self.addon_to_id or 0
|
||||
|
||||
@property
|
||||
def checkins(self):
|
||||
"""
|
||||
Related manager for all successful checkins. Use ``all_checkins`` instead if you want
|
||||
canceled positions as well.
|
||||
"""
|
||||
return self.all_checkins(manager='objects')
|
||||
|
||||
@property
|
||||
def generate_ticket(self):
|
||||
if self.item.generate_tickets is not None:
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
import base64
|
||||
import inspect
|
||||
import struct
|
||||
from collections import namedtuple
|
||||
from typing import Optional
|
||||
|
||||
from cryptography.hazmat.backends.openssl.backend import Backend
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
@@ -37,6 +39,8 @@ from pretix.base.models import Item, ItemVariation, SubEvent
|
||||
from pretix.base.secretgenerators import pretix_sig1_pb2
|
||||
from pretix.base.signals import register_ticket_secret_generators
|
||||
|
||||
ParsedSecret = namedtuple('AnalyzedSecret', 'item variation subevent attendee_name opaque_id')
|
||||
|
||||
|
||||
class BaseTicketSecretGenerator:
|
||||
"""
|
||||
@@ -72,6 +76,14 @@ class BaseTicketSecretGenerator:
|
||||
"""
|
||||
return False
|
||||
|
||||
def parse_secret(self, secret: str) -> Optional[ParsedSecret]:
|
||||
"""
|
||||
Given a ``secret``, return an ``ParsedSecret`` with the information decoded from the secret, if possible.
|
||||
Any value of ``ParsedSecret`` may be ``None``, and if parsing is not possible at all, you can ``None`` (as
|
||||
the default implementation does).
|
||||
"""
|
||||
return None
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
attendee_name: str = None, current_secret: str = None, force_invalidate=False) -> str:
|
||||
"""
|
||||
@@ -181,6 +193,15 @@ class Sig1TicketSecretGenerator(BaseTicketSecretGenerator):
|
||||
except:
|
||||
return None
|
||||
|
||||
def parse_secret(self, secret: str) -> Optional[ParsedSecret]:
|
||||
ticket = self._parse(secret)
|
||||
if ticket:
|
||||
item = self.event.items.filter(pk=ticket.item).first() if ticket.item else None
|
||||
subevent = self.event.subevents.filter(pk=ticket.subevent).first() if ticket.subevent else None
|
||||
variation = item.variations.filter(pk=ticket.variation).first() if item and ticket.subevent else None
|
||||
opaque_id = ticket.seed
|
||||
return self.ParsedSecret(item=item, subevent=subevent, variation=variation, opaque_id=opaque_id, attendee_name=None)
|
||||
|
||||
def generate_secret(self, item: Item, variation: ItemVariation = None, subevent: SubEvent = None,
|
||||
current_secret: str = None, force_invalidate=False):
|
||||
if current_secret and not force_invalidate:
|
||||
|
||||
@@ -566,7 +566,8 @@ def _save_answers(op, answers, given_answers):
|
||||
|
||||
def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict, force=False,
|
||||
ignore_unpaid=False, nonce=None, datetime=None, questions_supported=True,
|
||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY):
|
||||
user=None, auth=None, canceled_supported=False, type=Checkin.TYPE_ENTRY,
|
||||
raw_barcode=None):
|
||||
"""
|
||||
Create a checkin for this particular order position and check-in list. Fails with CheckInError if the check in is
|
||||
not valid at this time.
|
||||
@@ -623,12 +624,6 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
_('This order is not marked as paid.'),
|
||||
'unpaid'
|
||||
)
|
||||
elif require_answers and not force and questions_supported:
|
||||
raise RequiredQuestionsError(
|
||||
_('You need to answer questions to complete this check-in.'),
|
||||
'incomplete',
|
||||
require_answers
|
||||
)
|
||||
|
||||
if type == Checkin.TYPE_ENTRY and clist.rules and not force:
|
||||
rule_data = LazyRuleVars(op, clist, dt)
|
||||
@@ -643,6 +638,13 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
reason=reason
|
||||
)
|
||||
|
||||
if require_answers and not force and questions_supported:
|
||||
raise RequiredQuestionsError(
|
||||
_('You need to answer questions to complete this check-in.'),
|
||||
'incomplete',
|
||||
require_answers
|
||||
)
|
||||
|
||||
device = None
|
||||
if isinstance(auth, Device):
|
||||
device = auth
|
||||
@@ -668,6 +670,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
gate=device.gate if device else None,
|
||||
nonce=nonce,
|
||||
forced=force and not entry_allowed,
|
||||
raw_barcode=raw_barcode,
|
||||
)
|
||||
op.order.log_action('pretix.event.checkin', data={
|
||||
'position': op.id,
|
||||
@@ -676,6 +679,7 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
'forced': force or op.order.status != Order.STATUS_PAID,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'answers': {k.pk: str(v) for k, v in given_answers.items()},
|
||||
'list': clist.pk
|
||||
}, user=user, auth=auth)
|
||||
checkin_created.send(op.order.event, checkin=ci)
|
||||
|
||||
@@ -454,7 +454,9 @@ Arguments: ``checkin``
|
||||
|
||||
This signal is sent out every time a check-in is created (i.e. an order position is marked as
|
||||
checked in). It is not send if the position was already checked in and is force-checked-in a second time.
|
||||
The check-in object is given as the first argument
|
||||
The check-in object is given as the first argument.
|
||||
|
||||
For backwards compatibility reasons, this signal is only sent when a **successful** scan is saved.
|
||||
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user