예제 #1
0
class Event(LoggedModel):
    """
    This model represents an event. An event is anything you can buy
    tickets for.

    :param organizer: The organizer this event belongs to
    :type organizer: Organizer
    :param name: This event's full title
    :type name: str
    :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
                 be unique among the events of the same organizer.
    :type slug: str
    :param live: Whether or not the shop is publicly accessible
    :type live: bool
    :param currency: The currency of all prices and payments of this event
    :type currency: str
    :param date_from: The datetime this event starts
    :type date_from: datetime
    :param date_to: The datetime this event ends
    :type date_to: datetime
    :param presale_start: No tickets will be sold before this date.
    :type presale_start: datetime
    :param presale_end: No tickets will be sold after this date.
    :type presale_end: datetime
    :param plugins: A comma-separated list of plugin names that are active for this
                    event.
    :type plugins: str
    """

    settings_namespace = 'event'
    organizer = models.ForeignKey(Organizer,
                                  related_name="events",
                                  on_delete=models.PROTECT)
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        help_text=_(
            "Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
            "This is being used in addresses and bank transfer references."),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  ),
            ),
            EventSlugBlacklistValidator()
        ],
        verbose_name=_("Slug"),
    )
    live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
    permitted = models.ManyToManyField(
        User,
        through='EventPermission',
        related_name="events",
    )
    currency = models.CharField(max_length=10,
                                verbose_name=_("Default currency"),
                                default=settings.DEFAULT_CURRENCY)
    date_from = models.DateTimeField(verbose_name=_("Event start time"))
    date_to = models.DateTimeField(null=True,
                                   blank=True,
                                   verbose_name=_("Event end time"))
    is_public = models.BooleanField(
        default=False,
        verbose_name=_("Visible in public lists"),
        help_text=_(
            "If selected, this event may show up on the ticket system's start page "
            "or an organization profile."))
    presale_end = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("End of presale"),
        help_text=_("No products will be sold after this date."),
    )
    presale_start = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Start of presale"),
        help_text=_("No products will be sold before this date."),
    )
    plugins = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Plugins"),
    )

    class Meta:
        verbose_name = _("Event")
        verbose_name_plural = _("Events")
        ordering = ("date_from", "name")

    def __str__(self):
        return str(self.name)

    def save(self, *args, **kwargs):
        obj = super().save(*args, **kwargs)
        self.get_cache().clear()
        return obj

    def clean(self):
        if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
            raise ValidationError({
                'presale_end':
                _('The end of the presale period has to be later than its start.'
                  )
            })
        if self.date_from and self.date_to and self.date_from > self.date_to:
            raise ValidationError({
                'date_to':
                _('The end of the event has to be later than its start.')
            })
        super().clean()

    def get_plugins(self) -> "list[str]":
        """
        Returns the names of the plugins activated for this event as a list.
        """
        if self.plugins is None:
            return []
        return self.plugins.split(",")

    def get_date_from_display(self, tz=None) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting.
        """
        tz = tz or pytz.timezone(self.settings.timezone)
        return _date(
            self.date_from.astimezone(tz),
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_date_to_display(self, tz=None) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting. Returns an empty string
        if ``show_date_to`` is ``False``.
        """
        tz = tz or pytz.timezone(self.settings.timezone)
        if not self.settings.show_date_to or not self.date_to:
            return ""
        return _date(
            self.date_to.astimezone(tz),
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.
        """
        from pretix.base.cache import ObjectRelatedCache

        return ObjectRelatedCache(self)

    @cached_property
    def settings(self) -> SettingsProxy:
        """
        Returns an object representing this event's settings.
        """
        try:
            return SettingsProxy(self,
                                 type=EventSetting,
                                 parent=self.organizer)
        except Organizer.DoesNotExist:
            # Should only happen when creating new events
            return SettingsProxy(self, type=EventSetting)

    @property
    def presale_has_ended(self):
        if self.presale_end and now() > self.presale_end:
            return True
        return False

    @property
    def presale_is_running(self):
        if self.presale_start and now() < self.presale_start:
            return False
        if self.presale_end and now() > self.presale_end:
            return False
        return True

    def lock(self):
        """
        Returns a contextmanager that can be used to lock an event for bookings.
        """
        from pretix.base.services import locking

        return locking.LockManager(self)

    def get_mail_backend(self, force_custom=False):
        if self.settings.smtp_use_custom or force_custom:
            return CustomSMTPBackend(host=self.settings.smtp_host,
                                     port=self.settings.smtp_port,
                                     username=self.settings.smtp_username,
                                     password=self.settings.smtp_password,
                                     use_tls=self.settings.smtp_use_tls,
                                     use_ssl=self.settings.smtp_use_ssl,
                                     fail_silently=False)
        else:
            return get_connection(fail_silently=False)
예제 #2
0
파일: event.py 프로젝트: miraai/pretix
class Event(LoggedModel):
    """
    This model represents an event. An event is anything you can buy
    tickets for.

    :param organizer: The organizer this event belongs to
    :type organizer: Organizer
    :param name: This event's full title
    :type name: str
    :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
                 be unique among the events of the same organizer.
    :type slug: str
    :param live: Whether or not the shop is publicly accessible
    :type live: bool
    :param currency: The currency of all prices and payments of this event
    :type currency: str
    :param date_from: The datetime this event starts
    :type date_from: datetime
    :param date_to: The datetime this event ends
    :type date_to: datetime
    :param presale_start: No tickets will be sold before this date.
    :type presale_start: datetime
    :param presale_end: No tickets will be sold after this date.
    :type presale_end: datetime
    :param location: venue
    :type location: str
    :param plugins: A comma-separated list of plugin names that are active for this
                    event.
    :type plugins: str
    """

    settings_namespace = 'event'
    organizer = models.ForeignKey(Organizer,
                                  related_name="events",
                                  on_delete=models.PROTECT)
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        help_text=
        _("Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
          "This will be used in order codes, invoice numbers, links and bank transfer references."
          ),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  ),
            ),
            EventSlugBlacklistValidator()
        ],
        verbose_name=_("Short form"),
    )
    live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
    permitted = models.ManyToManyField(
        User,
        through='EventPermission',
        related_name="events",
    )
    currency = models.CharField(max_length=10,
                                verbose_name=_("Default currency"),
                                default=settings.DEFAULT_CURRENCY)
    date_from = models.DateTimeField(verbose_name=_("Event start time"))
    date_to = models.DateTimeField(null=True,
                                   blank=True,
                                   verbose_name=_("Event end time"))
    is_public = models.BooleanField(
        default=False,
        verbose_name=_("Visible in public lists"),
        help_text=_(
            "If selected, this event may show up on the ticket system's start page "
            "or an organization profile."))
    presale_end = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("End of presale"),
        help_text=_("No products will be sold after this date."),
    )
    presale_start = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Start of presale"),
        help_text=_("No products will be sold before this date."),
    )
    location = I18nCharField(
        null=True,
        blank=True,
        max_length=200,
        verbose_name=_("Location"),
    )
    plugins = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Plugins"),
    )

    class Meta:
        verbose_name = _("Event")
        verbose_name_plural = _("Events")
        ordering = ("date_from", "name")

    def __str__(self):
        return str(self.name)

    def save(self, *args, **kwargs):
        obj = super().save(*args, **kwargs)
        self.get_cache().clear()
        return obj

    def clean(self):
        if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
            raise ValidationError({
                'presale_end':
                _('The end of the presale period has to be later than its start.'
                  )
            })
        if self.date_from and self.date_to and self.date_from > self.date_to:
            raise ValidationError({
                'date_to':
                _('The end of the event has to be later than its start.')
            })
        super().clean()

    def get_plugins(self) -> "list[str]":
        """
        Returns the names of the plugins activated for this event as a list.
        """
        if self.plugins is None:
            return []
        return self.plugins.split(",")

    def get_date_from_display(self, tz=None) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting.
        """
        tz = tz or pytz.timezone(self.settings.timezone)
        return _date(
            self.date_from.astimezone(tz),
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_date_to_display(self, tz=None) -> str:
        """
        Returns a formatted string containing the start date of the event with respect
        to the current locale and to the ``show_times`` setting. Returns an empty string
        if ``show_date_to`` is ``False``.
        """
        tz = tz or pytz.timezone(self.settings.timezone)
        if not self.settings.show_date_to or not self.date_to:
            return ""
        return _date(
            self.date_to.astimezone(tz),
            "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT")

    def get_date_range_display(self, tz=None) -> str:
        tz = tz or pytz.timezone(self.settings.timezone)
        if not self.settings.show_date_to or not self.date_to:
            return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
        return daterange(self.date_from.astimezone(tz),
                         self.date_to.astimezone(tz))

    def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.
        """
        from pretix.base.cache import ObjectRelatedCache

        return ObjectRelatedCache(self)

    @cached_property
    def settings(self) -> SettingsProxy:
        """
        Returns an object representing this event's settings.
        """
        try:
            return SettingsProxy(self,
                                 type=EventSetting,
                                 parent=self.organizer)
        except Organizer.DoesNotExist:
            # Should only happen when creating new events
            return SettingsProxy(self, type=EventSetting)

    @property
    def presale_has_ended(self):
        if self.presale_end and now() > self.presale_end:
            return True
        return False

    @property
    def presale_is_running(self):
        if self.presale_start and now() < self.presale_start:
            return False
        if self.presale_end and now() > self.presale_end:
            return False
        return True

    def lock(self):
        """
        Returns a contextmanager that can be used to lock an event for bookings.
        """
        from pretix.base.services import locking

        return locking.LockManager(self)

    def get_mail_backend(self, force_custom=False):
        if self.settings.smtp_use_custom or force_custom:
            return CustomSMTPBackend(host=self.settings.smtp_host,
                                     port=self.settings.smtp_port,
                                     username=self.settings.smtp_username,
                                     password=self.settings.smtp_password,
                                     use_tls=self.settings.smtp_use_tls,
                                     use_ssl=self.settings.smtp_use_ssl,
                                     fail_silently=False)
        else:
            return get_connection(fail_silently=False)

    @property
    def payment_term_last(self):
        tz = pytz.timezone(self.settings.timezone)
        return make_aware(
            datetime.combine(
                self.settings.get('payment_term_last', as_type=date),
                time(hour=23, minute=59, second=59)), tz)

    def copy_data_from(self, other):
        from . import ItemCategory, Item, Question, Quota
        self.plugins = other.plugins
        self.save()

        category_map = {}
        for c in ItemCategory.objects.filter(event=other):
            category_map[c.pk] = c
            c.pk = None
            c.event = self
            c.save()

        item_map = {}
        variation_map = {}
        for i in Item.objects.filter(
                event=other).prefetch_related('variations'):
            vars = list(i.variations.all())
            item_map[i.pk] = i
            i.pk = None
            i.event = self
            if i.picture:
                i.picture.save(i.picture.name, i.picture)
            if i.category_id:
                i.category = category_map[i.category_id]
            i.save()
            for v in vars:
                variation_map[v.pk] = v
                v.pk = None
                v.item = i
                v.save()

        for q in Quota.objects.filter(event=other).prefetch_related(
                'items', 'variations'):
            items = list(q.items.all())
            vars = list(q.variations.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                q.items.add(item_map[i.pk])
            for v in vars:
                q.variations.add(variation_map[v.pk])

        for q in Question.objects.filter(event=other).prefetch_related(
                'items', 'options'):
            items = list(q.items.all())
            opts = list(q.options.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                q.items.add(item_map[i.pk])
            for o in opts:
                o.pk = None
                o.question = q
                o.save()

        for s in EventSetting.objects.filter(object=other):
            s.object = self
            s.pk = None
            if s.value.startswith('file://'):
                fi = default_storage.open(s.value[7:], 'rb')
                nonce = get_random_string(length=8)
                fname = '%s/%s/%s.%s.%s' % (self.organizer.slug,
                                            self.slug, s.key, nonce,
                                            s.value.split('.')[-1])
                newname = default_storage.save(fname, fi)
                s.value = 'file://' + newname
            s.save()
예제 #3
0
class Event(EventMixin, LoggedModel):
    """
    This model represents an event. An event is anything you can buy
    tickets for.

    :param organizer: The organizer this event belongs to
    :type organizer: Organizer
    :param name: This event's full title
    :type name: str
    :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
                 be unique among the events of the same organizer.
    :type slug: str
    :param live: Whether or not the shop is publicly accessible
    :type live: bool
    :param currency: The currency of all prices and payments of this event
    :type currency: str
    :param date_from: The datetime this event starts
    :type date_from: datetime
    :param date_to: The datetime this event ends
    :type date_to: datetime
    :param presale_start: No tickets will be sold before this date.
    :type presale_start: datetime
    :param presale_end: No tickets will be sold after this date.
    :type presale_end: datetime
    :param location: venue
    :type location: str
    :param plugins: A comma-separated list of plugin names that are active for this
                    event.
    :type plugins: str
    :param has_subevents: Enable event series functionality
    :type has_subevents: bool
    """

    settings_namespace = 'event'
    CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name)
                        for c in settings.CURRENCIES]
    organizer = models.ForeignKey(Organizer,
                                  related_name="events",
                                  on_delete=models.PROTECT)
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Event name"),
    )
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        help_text=
        _("Should be short, only contain lowercase letters, numbers, dots, and dashes, and must be unique among your "
          "events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
          "remembered, but you can also choose to use a random value. "
          "This will be used in URLs, order codes, invoice numbers, and bank transfer references."
          ),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  ),
            ),
            EventSlugBlacklistValidator()
        ],
        verbose_name=_("Short form"),
    )
    live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
    currency = models.CharField(max_length=10,
                                verbose_name=_("Event currency"),
                                choices=CURRENCY_CHOICES,
                                default=settings.DEFAULT_CURRENCY)
    date_from = models.DateTimeField(verbose_name=_("Event start time"))
    date_to = models.DateTimeField(null=True,
                                   blank=True,
                                   verbose_name=_("Event end time"))
    date_admission = models.DateTimeField(null=True,
                                          blank=True,
                                          verbose_name=_("Admission time"))
    is_public = models.BooleanField(
        default=False,
        verbose_name=_("Visible in public lists"),
        help_text=_(
            "If selected, this event may show up on the ticket system's start page "
            "or an organization profile."))
    presale_end = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("End of presale"),
        help_text=_(
            "Optional. No products will be sold after this date. If you do not set this value, the presale "
            "will end after the end date of your event."),
    )
    presale_start = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Start of presale"),
        help_text=_("Optional. No products will be sold before this date."),
    )
    location = I18nTextField(
        null=True,
        blank=True,
        max_length=200,
        verbose_name=_("Location"),
    )
    plugins = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Plugins"),
    )
    comment = models.TextField(verbose_name=_("Internal comment"),
                               null=True,
                               blank=True)
    has_subevents = models.BooleanField(verbose_name=_('Event series'),
                                        default=False)

    class Meta:
        verbose_name = _("Event")
        verbose_name_plural = _("Events")
        ordering = ("date_from", "name")

    def __str__(self):
        return str(self.name)

    @property
    def presale_has_ended(self):
        if self.has_subevents:
            return self.presale_end and now() > self.presale_end
        else:
            return super().presale_has_ended

    def save(self, *args, **kwargs):
        obj = super().save(*args, **kwargs)
        self.cache.clear()
        return obj

    def get_plugins(self) -> "list[str]":
        """
        Returns the names of the plugins activated for this event as a list.
        """
        if self.plugins is None:
            return []
        return self.plugins.split(",")

    def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.

        .. deprecated:: 1.9
           Use the property ``cache`` instead.
        """
        return self.cache

    @cached_property
    def cache(self):
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.
        """
        from pretix.base.cache import ObjectRelatedCache

        return ObjectRelatedCache(self)

    def lock(self):
        """
        Returns a contextmanager that can be used to lock an event for bookings.
        """
        from pretix.base.services import locking

        return locking.LockManager(self)

    def get_mail_backend(self, force_custom=False):
        """
        Returns an email server connection, either by using the system-wide connection
        or by returning a custom one based on the event's settings.
        """
        if self.settings.smtp_use_custom or force_custom:
            return CustomSMTPBackend(host=self.settings.smtp_host,
                                     port=self.settings.smtp_port,
                                     username=self.settings.smtp_username,
                                     password=self.settings.smtp_password,
                                     use_tls=self.settings.smtp_use_tls,
                                     use_ssl=self.settings.smtp_use_ssl,
                                     fail_silently=False)
        else:
            return get_connection(fail_silently=False)

    @property
    def payment_term_last(self):
        """
        The last datetime of payments for this event.
        """
        tz = pytz.timezone(self.settings.timezone)
        return make_aware(
            datetime.combine(
                self.settings.get(
                    'payment_term_last',
                    as_type=RelativeDateWrapper).datetime(self).date(),
                time(hour=23, minute=59, second=59)), tz)

    def copy_data_from(self, other):
        from . import ItemAddOn, ItemCategory, Item, Question, Quota
        from ..signals import event_copy_data

        self.plugins = other.plugins
        self.is_public = other.is_public
        self.save()

        tax_map = {}
        for t in other.tax_rules.all():
            tax_map[t.pk] = t
            t.pk = None
            t.event = self
            t.save()

        category_map = {}
        for c in ItemCategory.objects.filter(event=other):
            category_map[c.pk] = c
            c.pk = None
            c.event = self
            c.save()

        item_map = {}
        variation_map = {}
        for i in Item.objects.filter(
                event=other).prefetch_related('variations'):
            vars = list(i.variations.all())
            item_map[i.pk] = i
            i.pk = None
            i.event = self
            if i.picture:
                i.picture.save(i.picture.name, i.picture)
            if i.category_id:
                i.category = category_map[i.category_id]
            if i.tax_rule_id:
                i.tax_rule = tax_map[i.tax_rule_id]
            i.save()
            for v in vars:
                variation_map[v.pk] = v
                v.pk = None
                v.item = i
                v.save()

        for ia in ItemAddOn.objects.filter(
                base_item__event=other).prefetch_related(
                    'base_item', 'addon_category'):
            ia.pk = None
            ia.base_item = item_map[ia.base_item.pk]
            ia.addon_category = category_map[ia.addon_category.pk]
            ia.save()

        for q in Quota.objects.filter(event=other,
                                      subevent__isnull=True).prefetch_related(
                                          'items', 'variations'):
            items = list(q.items.all())
            vars = list(q.variations.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                if i.pk in item_map:
                    q.items.add(item_map[i.pk])
            for v in vars:
                q.variations.add(variation_map[v.pk])

        question_map = {}
        for q in Question.objects.filter(event=other).prefetch_related(
                'items', 'options'):
            items = list(q.items.all())
            opts = list(q.options.all())
            question_map[q.pk] = q
            q.pk = None
            q.event = self
            q.save()

            for i in items:
                q.items.add(item_map[i.pk])
            for o in opts:
                o.pk = None
                o.question = q
                o.save()

        for cl in other.checkin_lists.filter(
                subevent__isnull=True).prefetch_related('limit_products'):
            items = list(cl.limit_products.all())
            cl.pk = None
            cl.event = self
            cl.save()
            for i in items:
                cl.limit_products.add(item_map[i.pk])

        for s in other.settings._objects.all():
            s.object = self
            s.pk = None
            if s.value.startswith('file://'):
                fi = default_storage.open(s.value[7:], 'rb')
                nonce = get_random_string(length=8)
                # TODO: make sure pub is always correct
                fname = 'pub/%s/%s/%s.%s.%s' % (self.organizer.slug,
                                                self.slug, s.key, nonce,
                                                s.value.split('.')[-1])
                newname = default_storage.save(fname, fi)
                s.value = 'file://' + newname
                s.save()
            elif s.key == 'tax_rate_default':
                try:
                    if int(s.value) in tax_map:
                        s.value = tax_map.get(int(s.value)).pk
                        s.save()
                    else:
                        s.delete()
                except ValueError:
                    s.delete()
            else:
                s.save()

        event_copy_data.send(sender=self,
                             other=other,
                             tax_map=tax_map,
                             category_map=category_map,
                             item_map=item_map,
                             variation_map=variation_map,
                             question_map=question_map)

    def get_payment_providers(self) -> dict:
        """
        Returns a dictionary of initialized payment providers mapped by their identifiers.
        """
        from ..signals import register_payment_providers

        responses = register_payment_providers.send(self)
        providers = {}
        for receiver, response in responses:
            if not isinstance(response, list):
                response = [response]
            for p in response:
                pp = p(self)
                providers[pp.identifier] = pp

        return OrderedDict(
            sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))

    def get_invoice_renderers(self) -> dict:
        """
        Returns a dictionary of initialized invoice renderers mapped by their identifiers.
        """
        from ..signals import register_invoice_renderers

        responses = register_invoice_renderers.send(self)
        renderers = {}
        for receiver, response in responses:
            if not isinstance(response, list):
                response = [response]
            for p in response:
                pp = p(self)
                renderers[pp.identifier] = pp
        return renderers

    def get_data_shredders(self) -> dict:
        """
        Returns a dictionary of initialized data shredders mapped by their identifiers.
        """
        from ..signals import register_data_shredders

        responses = register_data_shredders.send(self)
        renderers = {}
        for receiver, response in responses:
            if not isinstance(response, list):
                response = [response]
            for p in response:
                pp = p(self)
                renderers[pp.identifier] = pp
        return renderers

    @property
    def invoice_renderer(self):
        """
        Returns the currently configured invoice renderer.
        """
        irs = self.get_invoice_renderers()
        return irs[self.settings.invoice_renderer]

    @property
    def active_subevents(self):
        """
        Returns a queryset of active subevents.
        """
        return self.subevents.filter(active=True).order_by(
            '-date_from', 'name')

    @property
    def active_future_subevents(self):
        return self.subevents.filter(
            Q(active=True)
            & (Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
               | Q(date_to__gte=now()))).order_by('date_from', 'name')

    @property
    def subevent_list_subevents(self):
        ordering = self.settings.get('frontpage_subevent_ordering',
                                     default='date_ascending',
                                     as_type=str)
        orderfields = {
            'date_ascending': ('date_from', 'name'),
            'date_descending': ('-date_from', 'name'),
            'name_ascending': ('name', 'date_from'),
            'name_descending': ('-name', 'date_from'),
        }[ordering]
        subevs = self.subevents.filter(
            Q(active=True) &
            (Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
             | Q(date_to__gte=now()))
        )  # order_by doesn't make sense with I18nField
        return sorted(subevs, key=attrgetter(*orderfields))

    @property
    def meta_data(self):
        data = {
            p.name: p.default
            for p in self.organizer.meta_properties.all()
        }
        data.update({
            v.property.name: v.value
            for v in self.meta_values.select_related('property').all()
        })
        return data

    @property
    def has_payment_provider(self):
        result = False
        for provider in self.get_payment_providers().values():
            if provider.is_enabled and provider.identifier not in (
                    'free', 'boxoffice'):
                result = True
                break
        return result

    @property
    def has_paid_things(self):
        from .items import Item, ItemVariation

        return Item.objects.filter(event=self, default_price__gt=0).exists()\
            or ItemVariation.objects.filter(item__event=self, default_price__gt=0).exists()

    @cached_property
    def live_issues(self):
        from pretix.base.signals import event_live_issues
        issues = []

        if self.has_paid_things and not self.has_payment_provider:
            issues.append(
                _('You have configured at least one paid product but have not enabled any payment methods.'
                  ))

        if not self.quotas.exists():
            issues.append(
                _('You need to configure at least one quota to sell anything.')
            )

        responses = event_live_issues.send(self)
        for receiver, response in sorted(responses, key=lambda r: str(r[0])):
            if response:
                issues.append(response)

        return issues

    def get_users_with_any_permission(self):
        """
        Returns a queryset of users who have any permission to this event.

        :return: Iterable of User
        """
        return self.get_users_with_permission(None)

    def get_users_with_permission(self, permission):
        """
        Returns a queryset of users who have a specific permission to this event.

        :return: Iterable of User
        """
        from .auth import User

        if permission:
            kwargs = {permission: True}
        else:
            kwargs = {}

        team_with_perm = Team.objects.filter(
            members__pk=OuterRef('pk'), organizer=self.organizer,
            **kwargs).filter(Q(all_events=True) | Q(limit_events__pk=self.pk))

        return User.objects.annotate(twp=Exists(team_with_perm)).filter(
            twp=True)

    def clean_live(self):
        for issue in self.live_issues:
            if issue:
                raise ValidationError(issue)

    def allow_delete(self):
        return not self.orders.exists() and not self.invoices.exists()

    def delete_sub_objects(self):
        self.items.all().delete()
        self.subevents.all().delete()

    def set_active_plugins(self, modules, allow_restricted=False):
        from pretix.base.plugins import get_all_plugins

        plugins_active = self.get_plugins()
        plugins_available = {
            p.module: p
            for p in get_all_plugins()
            if not p.name.startswith('.') and getattr(p, 'visible', True)
        }

        enable = [
            m for m in modules
            if m not in plugins_active and m in plugins_available
        ]

        for module in enable:
            if getattr(plugins_available[module].app, 'restricted',
                       False) and not allow_restricted:
                modules.remove(module)
            elif hasattr(plugins_available[module].app, 'installed'):
                getattr(plugins_available[module].app, 'installed')(self)

        self.plugins = ",".join(modules)

    def enable_plugin(self, module, allow_restricted=False):
        plugins_active = self.get_plugins()

        if module not in plugins_active:
            plugins_active.append(module)
            self.set_active_plugins(plugins_active,
                                    allow_restricted=allow_restricted)

    def disable_plugin(self, module):
        plugins_active = self.get_plugins()

        if module in plugins_active:
            plugins_active.remove(module)
            self.set_active_plugins(plugins_active)

    @staticmethod
    def clean_has_subevents(event, has_subevents):
        if event is not None and event.has_subevents is not None:
            if event.has_subevents != has_subevents:
                raise ValidationError(
                    _('Once created an event cannot change between an series and a single event.'
                      ))

    @staticmethod
    def clean_slug(organizer, event, slug):
        if event is not None and event.slug is not None:
            if event.slug != slug:
                raise ValidationError(_('The event slug cannot be changed.'))
        else:
            if Event.objects.filter(slug=slug, organizer=organizer).exists():
                raise ValidationError(
                    _('This slug has already been used for a different event.')
                )

    @staticmethod
    def clean_dates(date_from, date_to):
        if date_from is not None and date_to is not None:
            if date_from > date_to:
                raise ValidationError(
                    _('The event cannot end before it starts.'))

    @staticmethod
    def clean_presale(presale_start, presale_end):
        if presale_start is not None and presale_end is not None:
            if presale_start > presale_end:
                raise ValidationError(
                    _('The event\'s presale cannot end before it starts.'))
예제 #4
0
class Event(EventMixin, LoggedModel):
    """
    This model represents an event. An event is anything you can buy
    tickets for.

    :param organizer: The organizer this event belongs to
    :type organizer: Organizer
    :param name: This event's full title
    :type name: str
    :param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
                 be unique among the events of the same organizer.
    :type slug: str
    :param live: Whether or not the shop is publicly accessible
    :type live: bool
    :param currency: The currency of all prices and payments of this event
    :type currency: str
    :param date_from: The datetime this event starts
    :type date_from: datetime
    :param date_to: The datetime this event ends
    :type date_to: datetime
    :param presale_start: No tickets will be sold before this date.
    :type presale_start: datetime
    :param presale_end: No tickets will be sold after this date.
    :type presale_end: datetime
    :param location: venue
    :type location: str
    :param plugins: A comma-separated list of plugin names that are active for this
                    event.
    :type plugins: str
    :param has_subevents: Enable event series functionality
    :type has_subevents: bool
    """

    settings_namespace = 'event'
    CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name)
                        for c in settings.CURRENCIES]
    organizer = models.ForeignKey(Organizer,
                                  related_name="events",
                                  on_delete=models.PROTECT)
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        help_text=
        _("Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
          "We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
          "remembered, but you can also choose to use a random value. "
          "This will be used in URLs, order codes, invoice numbers, and bank transfer references."
          ),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  ),
            ),
            EventSlugBlacklistValidator()
        ],
        verbose_name=_("Short form"),
    )
    live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
    currency = models.CharField(max_length=10,
                                verbose_name=_("Default currency"),
                                choices=CURRENCY_CHOICES,
                                default=settings.DEFAULT_CURRENCY)
    date_from = models.DateTimeField(verbose_name=_("Event start time"))
    date_to = models.DateTimeField(null=True,
                                   blank=True,
                                   verbose_name=_("Event end time"))
    date_admission = models.DateTimeField(null=True,
                                          blank=True,
                                          verbose_name=_("Admission time"))
    is_public = models.BooleanField(
        default=False,
        verbose_name=_("Visible in public lists"),
        help_text=_(
            "If selected, this event may show up on the ticket system's start page "
            "or an organization profile."))
    presale_end = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("End of presale"),
        help_text=_("Optional. No products will be sold after this date."),
    )
    presale_start = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_("Start of presale"),
        help_text=_("Optional. No products will be sold before this date."),
    )
    location = I18nTextField(
        null=True,
        blank=True,
        max_length=200,
        verbose_name=_("Location"),
    )
    plugins = models.TextField(
        null=True,
        blank=True,
        verbose_name=_("Plugins"),
    )
    comment = models.TextField(verbose_name=_("Internal comment"),
                               null=True,
                               blank=True)
    has_subevents = models.BooleanField(verbose_name=_('Event series'),
                                        default=False)

    class Meta:
        verbose_name = _("Event")
        verbose_name_plural = _("Events")
        ordering = ("date_from", "name")

    def __str__(self):
        return str(self.name)

    def save(self, *args, **kwargs):
        obj = super().save(*args, **kwargs)
        self.get_cache().clear()
        return obj

    def get_plugins(self) -> "list[str]":
        """
        Returns the names of the plugins activated for this event as a list.
        """
        if self.plugins is None:
            return []
        return self.plugins.split(",")

    def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
        """
        Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
        Django's built-in cache backends, but puts you into an isolated environment for
        this event, so you don't have to prefix your cache keys. In addition, the cache
        is being cleared every time the event or one of its related objects change.
        """
        from pretix.base.cache import ObjectRelatedCache

        return ObjectRelatedCache(self)

    def lock(self):
        """
        Returns a contextmanager that can be used to lock an event for bookings.
        """
        from pretix.base.services import locking

        return locking.LockManager(self)

    def get_mail_backend(self, force_custom=False):
        """
        Returns an email server connection, either by using the system-wide connection
        or by returning a custom one based on the event's settings.
        """
        if self.settings.smtp_use_custom or force_custom:
            return CustomSMTPBackend(host=self.settings.smtp_host,
                                     port=self.settings.smtp_port,
                                     username=self.settings.smtp_username,
                                     password=self.settings.smtp_password,
                                     use_tls=self.settings.smtp_use_tls,
                                     use_ssl=self.settings.smtp_use_ssl,
                                     fail_silently=False)
        else:
            return get_connection(fail_silently=False)

    @property
    def payment_term_last(self):
        """
        The last datetime of payments for this event.
        """
        tz = pytz.timezone(self.settings.timezone)
        return make_aware(
            datetime.combine(
                self.settings.get(
                    'payment_term_last',
                    as_type=RelativeDateWrapper).datetime(self).date(),
                time(hour=23, minute=59, second=59)), tz)

    def copy_data_from(self, other):
        from . import ItemAddOn, ItemCategory, Item, Question, Quota
        from ..signals import event_copy_data

        self.plugins = other.plugins
        self.is_public = other.is_public
        self.save()

        category_map = {}
        for c in ItemCategory.objects.filter(event=other):
            category_map[c.pk] = c
            c.pk = None
            c.event = self
            c.save()

        item_map = {}
        variation_map = {}
        for i in Item.objects.filter(
                event=other).prefetch_related('variations'):
            vars = list(i.variations.all())
            item_map[i.pk] = i
            i.pk = None
            i.event = self
            if i.picture:
                i.picture.save(i.picture.name, i.picture)
            if i.category_id:
                i.category = category_map[i.category_id]
            i.save()
            for v in vars:
                variation_map[v.pk] = v
                v.pk = None
                v.item = i
                v.save()

        for ia in ItemAddOn.objects.filter(
                base_item__event=other).prefetch_related(
                    'base_item', 'addon_category'):
            ia.pk = None
            ia.base_item = item_map[ia.base_item.pk]
            ia.addon_category = category_map[ia.addon_category.pk]
            ia.save()

        for q in Quota.objects.filter(event=other,
                                      subevent__isnull=True).prefetch_related(
                                          'items', 'variations'):
            items = list(q.items.all())
            vars = list(q.variations.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                if i.pk in item_map:
                    q.items.add(item_map[i.pk])
            for v in vars:
                q.variations.add(variation_map[v.pk])

        for q in Question.objects.filter(event=other).prefetch_related(
                'items', 'options'):
            items = list(q.items.all())
            opts = list(q.options.all())
            q.pk = None
            q.event = self
            q.save()
            for i in items:
                q.items.add(item_map[i.pk])
            for o in opts:
                o.pk = None
                o.question = q
                o.save()

        for s in other.settings._objects.all():
            s.object = self
            s.pk = None
            if s.value.startswith('file://'):
                fi = default_storage.open(s.value[7:], 'rb')
                nonce = get_random_string(length=8)
                fname = '%s/%s/%s.%s.%s' % (self.organizer.slug,
                                            self.slug, s.key, nonce,
                                            s.value.split('.')[-1])
                newname = default_storage.save(fname, fi)
                s.value = 'file://' + newname
            s.save()

        event_copy_data.send(sender=self, other=other)

    def get_payment_providers(self) -> dict:
        """
        Returns a dictionary of initialized payment providers mapped by their identifiers.
        """
        from ..signals import register_payment_providers

        responses = register_payment_providers.send(self)
        providers = {}
        for receiver, response in responses:
            if not isinstance(response, list):
                response = [response]
            for p in response:
                pp = p(self)
                providers[pp.identifier] = pp

        return OrderedDict(
            sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))

    def get_invoice_renderers(self) -> dict:
        """
        Returns a dictionary of initialized invoice renderers mapped by their identifiers.
        """
        from ..signals import register_invoice_renderers

        responses = register_invoice_renderers.send(self)
        renderers = {}
        for receiver, response in responses:
            if not isinstance(response, list):
                response = [response]
            for p in response:
                pp = p(self)
                renderers[pp.identifier] = pp
        return renderers

    @property
    def invoice_renderer(self):
        """
        Returns the currently configured invoice renderer.
        """
        irs = self.get_invoice_renderers()
        return irs[self.settings.invoice_renderer]

    @property
    def active_subevents(self):
        """
        Returns a queryset of active subevents.
        """
        return self.subevents.filter(active=True).order_by(
            '-date_from', 'name')

    @property
    def active_future_subevents(self):
        return self.subevents.filter(
            Q(active=True)
            & (Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
               | Q(date_to__gte=now()))).order_by('date_from', 'name')