Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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