class CheckinList(LoggedModel): event = models.ForeignKey('Event', related_name='checkin_lists', on_delete=models.CASCADE) name = models.CharField(max_length=190) all_products = models.BooleanField( default=True, verbose_name=_("All products (including newly created ones)")) limit_products = models.ManyToManyField( 'Item', verbose_name=_("Limit to products"), blank=True) subevent = models.ForeignKey('SubEvent', null=True, blank=True, verbose_name=pgettext_lazy( 'subevent', 'Date'), on_delete=models.CASCADE) 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 ' 'order has not been paid.')) gates = models.ManyToManyField( 'Gate', verbose_name=_("Gates"), blank=True, help_text=_( "Does not have any effect for the validation of tickets, only for the automatic configuration of " "check-in devices.")) allow_entry_after_exit = models.BooleanField( verbose_name=_('Allow re-entering after an exit scan'), default=True) allow_multiple_entries = models.BooleanField( verbose_name=_('Allow multiple entries per ticket'), 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, verbose_name=_('Sales channels to automatically check in'), help_text= _('All items on this check-in list will be automatically marked as checked-in when purchased through ' 'any of the selected sales channels. This option can be useful when tickets sold at the box office ' 'are not checked again before entry and should be considered validated directly upon purchase.' )) rules = models.JSONField(default=dict, blank=True) objects = ScopedManager(organizer='event__organizer') class Meta: ordering = ('subevent__date_from', 'name') @property def positions(self): from . import Order, OrderPosition qs = OrderPosition.objects.filter( order__event=self.event, order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID], ) if self.subevent_id: qs = qs.filter(subevent_id=self.subevent_id) if not self.all_products: qs = qs.filter( item__in=self.limit_products.values_list('id', flat=True)) return qs @property def positions_inside(self): return self.positions.annotate( last_entry=Subquery( Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.pk, type=Checkin.TYPE_ENTRY, ).order_by().values('position_id').annotate( m=Max('datetime')).values('m')), last_exit=Subquery( Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.pk, type=Checkin.TYPE_EXIT, ).order_by().values('position_id').annotate( m=Max('datetime')).values('m')), ).filter( Q(last_entry__isnull=False) & Q(Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry')))) @property def inside_count(self): return self.positions_inside.count() @property @scopes_disabled() # Disable scopes, because this query is safe and the additional organizer filter in the EXISTS() subquery tricks PostgreSQL into a bad # subplan that sequentially scans all events def checkin_count(self): return self.event.cache.get_or_set( 'checkin_list_{}_checkin_count'.format(self.pk), lambda: self.positions.using(settings.DATABASE_REPLICA).annotate( checkedin=Exists( Checkin.objects.filter( list_id=self.pk, position=OuterRef('pk'), type=Checkin.TYPE_ENTRY, ))).filter(checkedin=True).count(), 60) @property def percent(self): pc = self.position_count return round(self.checkin_count * 100 / pc) if pc else 0 @property def position_count(self): return self.event.cache.get_or_set( 'checkin_list_{}_position_count'.format(self.pk), lambda: self.positions.count(), 60) def touch(self): self.event.cache.delete('checkin_list_{}_position_count'.format( self.pk)) self.event.cache.delete('checkin_list_{}_checkin_count'.format( self.pk)) @staticmethod def annotate_with_numbers(qs, event): # This is only kept for backwards-compatibility reasons. This method used to precompute .position_count # and .checkin_count through a huge subquery chain, but was dropped for performance reasons. return qs def __str__(self): return self.name @classmethod def validate_rules(cls, rules, seen_nonbool=False, depth=0): # While we implement a full jsonlogic machine on Python-level, we also use the logic rules to generate # SQL queries, which is not a full implementation of JSON logic right now, but makes some assumptions, # e.g. it does not support something like (a AND b) == (c OR D) # Every change to our supported JSON logic must be done # * in pretix.base.services.checkin # * in pretix.base.models.checkin # * in pretix.helpers.jsonlogic_boolalg # * in checkinrules.js # * in libpretixsync top_level_operators = { '<', '<=', '>', '>=', '==', '!=', 'inList', 'isBefore', 'isAfter', 'or', 'and' } allowed_operators = top_level_operators | { 'buildTime', 'objectList', 'lookup', 'var', } allowed_vars = { 'product', 'variation', 'now', 'entries_number', 'entries_today', 'entries_days' } if not rules or not isinstance(rules, dict): return rules if len(rules) > 1: raise ValidationError( f'Rules should not include dictionaries with more than one key, found: "{rules}".' ) operator = list(rules.keys())[0] if operator not in allowed_operators: raise ValidationError( f'Logic operator "{operator}" is currently not allowed.') if depth == 0 and operator not in top_level_operators: raise ValidationError( f'Logic operator "{operator}" is currently not allowed on the first level.' ) values = rules[operator] if not isinstance(values, list) and not isinstance(values, tuple): values = [values] if operator == 'var': if values[0] not in allowed_vars: raise ValidationError( f'Logic variable "{values[0]}" is currently not allowed.') return rules if operator in ('or', 'and') and seen_nonbool: raise ValidationError( f'You cannot use OR/AND logic on a level below a comparison operator.' ) for v in values: cls.validate_rules(v, seen_nonbool=seen_nonbool or operator not in ('or', 'and'), depth=depth + 1) if operator in ('or', 'and') and depth == 0 and not values: return {} return rules
class CheckinList(LoggedModel): event = models.ForeignKey('Event', related_name='checkin_lists', on_delete=models.CASCADE) name = models.CharField(max_length=190) all_products = models.BooleanField( default=True, verbose_name=_("All products (including newly created ones)")) limit_products = models.ManyToManyField( 'Item', verbose_name=_("Limit to products"), blank=True) subevent = models.ForeignKey('SubEvent', null=True, blank=True, verbose_name=pgettext_lazy( 'subevent', 'Date'), on_delete=models.CASCADE) 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 ' 'order have not been paid.')) allow_entry_after_exit = models.BooleanField( verbose_name=_('Allow re-entering after an exit scan'), default=True) allow_multiple_entries = models.BooleanField( verbose_name=_('Allow multiple entries per ticket'), help_text= _('Use this option to turn off warnings if a ticket is scanned a second time.' ), default=False) auto_checkin_sales_channels = MultiStringField( default=[], blank=True, verbose_name=_('Sales channels to automatically check in'), help_text= _('All items on this check-in list will be automatically marked as checked-in when purchased through ' 'any of the selected sales channels. This option can be useful when tickets sold at the box office ' 'are not checked again before entry and should be considered validated directly upon purchase.' )) rules = FallbackJSONField(default=dict, blank=True) objects = ScopedManager(organizer='event__organizer') class Meta: ordering = ('subevent__date_from', 'name') @property def positions(self): from . import Order, OrderPosition qs = OrderPosition.objects.filter( order__event=self.event, order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID], ) if self.subevent_id: qs = qs.filter(subevent_id=self.subevent_id) if not self.all_products: qs = qs.filter( item__in=self.limit_products.values_list('id', flat=True)) return qs @property def inside_count(self): return self.positions.annotate( last_entry=Subquery( Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.pk, type=Checkin.TYPE_ENTRY, ).order_by().values('position_id').annotate( m=Max('datetime')).values('m')), last_exit=Subquery( Checkin.objects.filter( position_id=OuterRef('pk'), list_id=self.pk, type=Checkin.TYPE_EXIT, ).order_by().values('position_id').annotate( m=Max('datetime')).values('m')), ).filter( Q(last_entry__isnull=False) & Q(Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry')))).count() @property @scopes_disabled() # Disable scopes, because this query is safe and the additional organizer filter in the EXISTS() subquery tricks PostgreSQL into a bad # subplan that sequentially scans all events def checkin_count(self): return self.event.cache.get_or_set( 'checkin_list_{}_checkin_count'.format(self.pk), lambda: self.positions.using(settings.DATABASE_REPLICA).annotate( checkedin=Exists( Checkin.objects.filter( list_id=self.pk, position=OuterRef('pk'), type=Checkin.TYPE_ENTRY, ))).filter(checkedin=True).count(), 60) @property def percent(self): pc = self.position_count return round(self.checkin_count * 100 / pc) if pc else 0 @property def position_count(self): return self.event.cache.get_or_set( 'checkin_list_{}_position_count'.format(self.pk), lambda: self.positions.count(), 60) def touch(self): self.event.cache.delete('checkin_list_{}_position_count'.format( self.pk)) self.event.cache.delete('checkin_list_{}_checkin_count'.format( self.pk)) @staticmethod def annotate_with_numbers(qs, event): # This is only kept for backwards-compatibility reasons. This method used to precompute .position_count # and .checkin_count through a huge subquery chain, but was dropped for performance reasons. return qs def __str__(self): return self.name
class CheckinList(LoggedModel): event = models.ForeignKey('Event', related_name='checkin_lists', on_delete=models.CASCADE) name = models.CharField(max_length=190) all_products = models.BooleanField( default=True, verbose_name=_("All products (including newly created ones)")) limit_products = models.ManyToManyField( 'Item', verbose_name=_("Limit to products"), blank=True) subevent = models.ForeignKey('SubEvent', null=True, blank=True, verbose_name=pgettext_lazy( 'subevent', 'Date'), on_delete=models.CASCADE) 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 ' 'order have not been paid.')) auto_checkin_sales_channels = MultiStringField( default=[], blank=True, verbose_name=_('Sales channels to automatically check in'), help_text= _('All items on this check-in list will be automatically marked as checked-in when purchased through ' 'any of the selected sales channels. This option can be useful when tickets sold at the box office ' 'are not checked again before entry and should be considered validated directly upon purchase.' )) objects = ScopedManager(organizer='event__organizer') class Meta: ordering = ('subevent__date_from', 'name') @property def positions(self): from . import OrderPosition, Order qs = OrderPosition.objects.filter( order__event=self.event, order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID], subevent=self.subevent) if not self.all_products: qs = qs.filter( item__in=self.limit_products.values_list('id', flat=True)) return qs @property def checkin_count(self): return self.event.cache.get_or_set( 'checkin_list_{}_checkin_count'.format(self.pk), lambda: self.positions.annotate(checkedin=Exists( Checkin.objects.filter(list_id=self.pk, position=OuterRef('pk')))).filter( checkedin=True).count(), 60) @property def percent(self): pc = self.position_count return round(self.checkin_count * 100 / pc) if pc else 0 @property def position_count(self): return self.event.cache.get_or_set( 'checkin_list_{}_position_count'.format(self.pk), lambda: self.positions.count(), 60) def touch(self): self.event.cache.delete('checkin_list_{}_position_count'.format( self.pk)) self.event.cache.delete('checkin_list_{}_checkin_count'.format( self.pk)) @staticmethod def annotate_with_numbers(qs, event): # This is only kept for backwards-compatibility reasons. This method used to precompute .position_count # and .checkin_count through a huge subquery chain, but was dropped for performance reasons. return qs def __str__(self): return self.name