Exemple #1
0
class Page(LogMixin, models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="pages")
    slug = models.CharField(
        max_length=150,
        db_index=True,
        verbose_name=_("URL to static page"),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=_(
                    "The slug may only contain letters, numbers, dots and dashes."
                ),
            )
        ],
        help_text=_(
            "This will be used to generate the URL of the page. Please only use latin letters, "
            "numbers, dots and dashes. You cannot change this afterwards."
        ),
    )
    position = models.IntegerField(default=0)
    title = I18nCharField(verbose_name=_("Page title"))
    text = I18nTextField(
        verbose_name=_("Page content"), help_text=phrases.base.use_markdown
    )
    link_in_footer = models.BooleanField(
        default=False, verbose_name=_("Show link in the event footer")
    )

    class Meta:
        ordering = ["position", "title"]
Exemple #2
0
class SpeakerInformation(LogMixin, FileCleanupMixin, models.Model):
    """Represents any information organisers want to show all or some
    submitters or speakers."""

    event = models.ForeignKey(
        to="event.Event", related_name="information", on_delete=models.CASCADE
    )
    include_submitters = models.BooleanField(
        verbose_name=_("Include all submitters"),
        help_text=_("Show to every submitter regardless of their proposals' status"),
        default=False,
    )
    exclude_unconfirmed = models.BooleanField(
        verbose_name=_("Exclude unconfirmed speakers"),
        help_text=_("Show to speakers only once they have confirmed attendance"),
        default=False,
    )
    title = I18nCharField(verbose_name=_("Subject"), max_length=200)
    text = I18nTextField(verbose_name=_("Text"), help_text=phrases.base.use_markdown)
    resource = models.FileField(
        verbose_name=_("file"),
        null=True,
        blank=True,
        help_text=_("Please try to keep your upload small, preferably below 16 MB."),
        upload_to=resource_path,
    )

    objects = ScopedManager(event="event")

    class orga_urls(EventUrls):
        base = edit = "{self.event.orga_urls.information}{self.pk}/"
        delete = "{base}delete"
Exemple #3
0
class ItemCategory(LoggedModel):
    """
    Items can be sorted into these categories.

    :param event: The event this category belongs to
    :type event: Event
    :param name: The name of this category
    :type name: str
    :param position: An integer, used for sorting
    :type position: int
    """
    event = models.ForeignKey(
        Event,
        on_delete=models.CASCADE,
        related_name='categories',
    )
    name = I18nCharField(
        max_length=255,
        verbose_name=_("Category name"),
    )
    description = I18nTextField(
        blank=True, verbose_name=_("Category description")
    )
    position = models.IntegerField(
        default=0
    )
    is_addon = models.BooleanField(
        default=False,
        verbose_name=_('Products in this category are add-on products'),
        help_text=_('If selected, the products belonging to this category are not for sale on their own. They can '
                    'only be bought in combination with a product that has this category configured as a possible '
                    'source for add-ons.')
    )

    class Meta:
        verbose_name = _("Product category")
        verbose_name_plural = _("Product categories")
        ordering = ('position', 'id')

    def __str__(self):
        if self.is_addon:
            return _('{category} (Add-On products)').format(category=str(self.name))
        return str(self.name)

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.event:
            self.event.get_cache().clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.event:
            self.event.get_cache().clear()

    @property
    def sortkey(self):
        return self.position, self.id

    def __lt__(self, other) -> bool:
        return self.sortkey < other.sortkey
Exemple #4
0
class Page(models.Model):
    # TODO: find the table for the foreign key
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    slug = models.CharField(
        max_length=150,
        db_index=True,
        verbose_name=_('URL to static page'),
        validators=[
            RegexValidator(
                regex="^[a-zA-Z0-9.-]+$",
                message=
                _("The slug may only contain letters, numbers, dots and dashes."
                  )),
        ],
        help_text=_(
            "This will be used to generate the URL of the page. Please only use latin letters, "
            "numbers, dots and dashes. You cannot change this afterwards."))
    position = models.IntegerField(default=0)
    title = I18nCharField(verbose_name=_('Page title'))
    text = I18nTextField(verbose_name=_('Page content'))
    link_on_frontpage = models.BooleanField(
        default=False, verbose_name=_('Show link on the event start page'))
    link_in_footer = models.BooleanField(
        default=False, verbose_name=_('Show link in the event footer'))
    require_confirmation = models.BooleanField(
        default=False,
        verbose_name=_('Require the user to acknowledge this page before the '
                       'user action (e.g. for code of conduct).'))

    class Meta:
        ordering = ['position', 'title']
Exemple #5
0
class Book(models.Model):
    title = I18nCharField(verbose_name='Book title', max_length=190)
    abstract = I18nTextField(verbose_name='Abstract')
    author = models.ForeignKey(Author, verbose_name='Author')

    def __str__(self):
        return str(self.title)
Exemple #6
0
class Tag(LogMixin, models.Model):
    created = models.DateTimeField(null=True, auto_now_add=True)
    event = models.ForeignKey(to="event.Event",
                              on_delete=models.PROTECT,
                              related_name="tags")

    tag = models.CharField(max_length=50)
    description = I18nTextField(
        verbose_name=_("Description"),
        blank=True,
    )
    color = models.CharField(
        max_length=7,
        verbose_name=_("Color"),
        validators=[
            RegexValidator(r"#([0-9A-Fa-f]{3}){1,2}"),
        ],
    )
    public = models.BooleanField(default=False,
                                 verbose_name=_("Show tag publicly"))
    objects = ScopedManager(event="event")

    class urls(EventUrls):
        base = edit = "{self.event.orga_urls.tags}{self.pk}/"
        delete = "{base}delete"

    def __str__(self) -> str:
        return str(self.tag)
Exemple #7
0
class CfP(LogMixin, models.Model):
    event = models.OneToOneField(
        to='event.Event',
        on_delete=models.PROTECT,
    )
    headline = I18nCharField(
        max_length=300,
        null=True, blank=True,
    )
    text = I18nTextField(null=True, blank=True)
    default_type = models.ForeignKey(
        to='submission.SubmissionType',
        on_delete=models.PROTECT,
        related_name='+',
    )
    deadline = models.DateTimeField(null=True, blank=True)

    @property
    def is_open(self):
        if self.deadline is not None:
            return now() <= self.deadline
        return True

    def __str__(self) -> str:
        return str(self.headline)
Exemple #8
0
class SpeakerInformation(LogMixin, models.Model):
    event = models.ForeignKey(
        to='event.Event', related_name='information', on_delete=models.CASCADE
    )
    include_submitters = models.BooleanField(
        verbose_name=_('Include all submitters'),
        help_text=_('Show to every submitter regardless of their submissions\' status'),
        default=False,
    )
    exclude_unconfirmed = models.BooleanField(
        verbose_name=_('Exclude unconfirmed speakers'),
        help_text=_('Show to speakers only once they have confirmed attendance'),
        default=False,
    )
    title = I18nCharField(verbose_name=_('Subject'), max_length=200)
    text = I18nTextField(verbose_name=_('Text'), help_text=phrases.base.use_markdown)
    resource = models.FileField(
        verbose_name=_('file'),
        null=True,
        blank=True,
        help_text=_('Please try to keep your upload small, preferably below 16 MB.'),
    )

    class orga_urls(EventUrls):
        base = edit = '{self.event.orga_urls.information}/{self.pk}'
        delete = '{base}/delete/'
Exemple #9
0
class CfP(LogMixin, models.Model):
    event = models.OneToOneField(
        to='event.Event',
        on_delete=models.PROTECT,
    )
    headline = I18nCharField(
        max_length=300,
        null=True,
        blank=True,
    )
    text = I18nTextField(null=True, blank=True)
    default_type = models.ForeignKey(
        to='submission.SubmissionType',
        on_delete=models.PROTECT,
        related_name='+',
    )
    deadline = models.DateTimeField(null=True, blank=True)

    class urls(Urls):
        base = '{self.event.orga_urls.cfp}'
        questions = '{base}/questions'
        new_question = '{questions}/new'
        text = '{base}/text'
        edit_text = '{text}/edit'
        types = '{base}/types'
        new_type = '{types}/new'

    @property
    def is_open(self):
        if self.deadline is not None:
            return now() <= self.deadline
        return True

    def __str__(self) -> str:
        return str(self.headline)
Exemple #10
0
class FAQ(models.Model):
    category = models.ForeignKey(
        to=FAQCategory,
        on_delete=models.CASCADE,
        related_name='questions',
        verbose_name=_('Category'),
    )
    question = I18nCharField(verbose_name=_('Question'))
    answer = I18nTextField(verbose_name=_('Answer'))
    tags = models.CharField(
        null=True,
        blank=True,
        max_length=180,
        verbose_name=_('Tags'),
        help_text=(
            'Tags can help people find related questions. Please enter the tags separated by commas.'
        ),
    )
    position = models.PositiveIntegerField(verbose_name=_('Position'))

    objects = ScopedManager(event='category__event')

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

    class Meta:
        ordering = ('category__position', 'position', 'id')
Exemple #11
0
class CfP(LogMixin, models.Model):
    event = models.OneToOneField(
        to='event.Event',
        on_delete=models.PROTECT,
    )
    headline = I18nCharField(
        max_length=300,
        null=True,
        blank=True,
        verbose_name=_('headline'),
    )
    text = I18nTextField(
        null=True,
        blank=True,
        verbose_name=_('text'),
        help_text=_('You can use markdown here.'),
    )
    default_type = models.ForeignKey(
        to='submission.SubmissionType',
        on_delete=models.PROTECT,
        related_name='+',
        verbose_name=_('Default submission type'),
    )
    deadline = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_('deadline'),
        help_text=
        _('Please put in the last date you want to accept submissions from users.'
          ),
    )

    class urls(EventUrls):
        base = '{self.event.orga_urls.cfp}'
        questions = '{base}/questions'
        new_question = '{questions}/new'
        remind_questions = '{questions}/remind'
        text = edit_text = '{base}/text'
        types = '{base}/types'
        new_type = '{types}/new'
        public = '{self.event.urls.base}/cfp'

    def __str__(self) -> str:
        return f'CfP(event={self.event.slug})'

    @cached_property
    def is_open(self):
        if self.deadline is None:
            return True
        return self.max_deadline >= now()

    @cached_property
    def max_deadline(self):
        deadlines = list(
            self.event.submission_types.filter(
                deadline__isnull=False).values_list('deadline', flat=True))
        if self.deadline:
            deadlines += [self.deadline]
        return max(deadlines)
Exemple #12
0
class MailTemplate(Auditable, models.Model):

    subject = I18nCharField(
        max_length=200,
        verbose_name=_('Subject'),
    )
    text = I18nTextField(
        verbose_name=_('Text'),
    )
    reply_to = models.EmailField(
        max_length=200,
        blank=True, null=True,
        verbose_name=_('Reply-To'),
        help_text=_('Change the Reply-To address if you do not want to use the default orga address'),
    )
    bcc = models.CharField(
        max_length=1000,
        blank=True, null=True,
        verbose_name=_('BCC'),
        help_text=_('Enter comma separated addresses. Will receive a blind copy of every mail sent from this template. This may be a LOT!'),
    )

    def __str__(self):
        return '{self.subject}'.format(self=self)

    def to_mail(self, email, locale=None, context=None, skip_queue=False, attachments=None, save=True):
        from byro.common.models import Configuration
        config = Configuration.get_solo()
        locale = locale or config.language
        with override(locale):
            context = context or dict()
            try:
                subject = str(self.subject).format(**context)
                text = str(self.text).format(**context)
            except KeyError as e:
                raise SendMailException('Experienced KeyError when rendering Text: {e}'.format(e=e))

            mail = EMail(
                to=email,
                reply_to=self.reply_to,
                bcc=self.bcc,
                subject=subject,
                text=text,
                template=self,
            )
            if save:
                mail.save()
                if attachments:
                    for a in attachments:
                        mail.attachments.add(a)
                if skip_queue:
                    mail.send()
        return mail

    def get_absolute_url(self):
        return reverse('office:mails.templates.view', kwargs={'pk': self.pk})

    def get_object_icon(self):
        return mark_safe('<i class="fa fa-envelope-o"></i> ')
Exemple #13
0
class CfP(LogMixin, models.Model):
    event = models.OneToOneField(to='event.Event', on_delete=models.PROTECT)
    headline = I18nCharField(max_length=300,
                             null=True,
                             blank=True,
                             verbose_name=_('headline'))
    text = I18nTextField(
        null=True,
        blank=True,
        verbose_name=_('text'),
        help_text=phrases.base.use_markdown,
    )
    default_type = models.ForeignKey(
        to='submission.SubmissionType',
        on_delete=models.PROTECT,
        related_name='+',
        verbose_name=_('Default submission type'),
    )
    deadline = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_('deadline'),
        help_text=
        _('Please put in the last date you want to accept submissions from users.'
          ),
    )

    class urls(EventUrls):
        base = '{self.event.orga_urls.cfp}'
        questions = '{base}questions/'
        new_question = '{questions}new'
        remind_questions = '{questions}remind'
        text = edit_text = '{base}text'
        types = '{base}types/'
        new_type = '{types}new'
        tracks = '{base}tracks/'
        new_track = '{tracks}new'
        public = '{self.event.urls.base}cfp'
        submit = '{self.event.urls.base}submit/'

    def __str__(self) -> str:
        """Help with debugging."""
        return f'CfP(event={self.event.slug})'

    @cached_property
    def is_open(self):
        if self.deadline is None:
            return True
        return self.max_deadline >= now() if self.max_deadline else True

    @cached_property
    def max_deadline(self):
        deadlines = list(
            self.event.submission_types.filter(
                deadline__isnull=False).values_list('deadline', flat=True))
        if self.deadline:
            deadlines.append(self.deadline)
        return max(deadlines) if deadlines else None
Exemple #14
0
class MailTemplate(LogMixin, models.Model):
    event = models.ForeignKey(
        to='event.Event',
        on_delete=models.PROTECT,
        related_name='mail_templates',
    )
    subject = I18nCharField(max_length=200)
    text = I18nTextField()
    reply_to = models.EmailField(
        max_length=200,
        blank=True,
        null=True,
        verbose_name=_('Reply-To'),
        help_text=
        _('Change the Reply-To address if you do not want to use the default orga address'
          ),
    )
    bcc = models.CharField(
        max_length=1000,
        blank=True,
        null=True,
        verbose_name=_('BCC'),
        help_text=
        _('Enter comma separated addresses. Will receive a blind copy of every mail sent from this template. This may be a LOT!'
          ))

    class urls(Urls):
        base = '{self.event.orga_urls.mail_templates}/{self.pk}'
        edit = '{base}/edit'
        delete = '{base}/delete'

    def bulk_mail(self):
        # TODO: call to_mail
        pass

    def to_mail(self,
                user,
                event,
                locale=None,
                context=None,
                skip_queue=False):
        with override(locale):
            context = TolerantDict(context or dict())
            mail = QueuedMail(event=self.event,
                              to=user.email,
                              reply_to=self.reply_to or event.email,
                              bcc=self.bcc,
                              subject=str(self.subject).format(**context),
                              text=str(self.text).format(**context))
            if skip_queue:
                mail.send()
            else:
                mail.save()
        return mail
Exemple #15
0
class Track(LogMixin, models.Model):
    """A track groups :class:`~pretalx.submission.models.submission.Submission`
    objects within an :class:`~pretalx.event.models.event.Event`, e.g. by
    topic.

    :param color: The track colour, in the format #012345.
    """

    event = models.ForeignKey(
        to="event.Event", on_delete=models.PROTECT, related_name="tracks"
    )
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    description = I18nTextField(
        verbose_name=_("Description"),
        blank=True,
    )
    color = models.CharField(
        max_length=7,
        verbose_name=_("Color"),
        validators=[
            RegexValidator(r"#([0-9A-Fa-f]{3}){1,2}"),
        ],
    )
    requires_access_code = models.BooleanField(
        verbose_name=_("Requires access code"),
        help_text=_(
            "This track will only be shown to submitters with a matching access code."
        ),
        default=False,
    )

    objects = ScopedManager(event="event")

    class urls(EventUrls):
        base = edit = "{self.event.cfp.urls.tracks}{self.pk}/"
        delete = "{base}delete"
        prefilled_cfp = "{self.event.cfp.urls.public}?track={self.slug}"

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

    @property
    def slug(self) -> str:
        """The slug makes tracks more readable in URLs.

        It consists of the ID, followed by a slugified (and, in lookups,
        optional) form of the track name.
        """
        return f"{self.id}-{slugify(self.name)}"
Exemple #16
0
class LatexTemplate(LoggedModel):
    event = models.ForeignKey('pretixbase.Event', on_delete=models.CASCADE)
    position = models.IntegerField(default=0)
    title = I18nCharField(verbose_name=_('Template name'))
    text = I18nTextField(verbose_name=_('Template code'))
    all_products = models.BooleanField(blank=True)
    limit_products = models.ManyToManyField(
        'pretixbase.Item', verbose_name=_("Limit to products"), blank=True)

    objects = ScopedManager(organizer='event__organizer')

    class Meta:
        ordering = ['position', 'title']
Exemple #17
0
class MailTemplate(Auditable, models.Model):

    subject = I18nCharField(
        max_length=200,
        verbose_name=_('Subject'),
    )
    text = I18nTextField(verbose_name=_('Text'), )
    reply_to = models.EmailField(
        max_length=200,
        blank=True,
        null=True,
        verbose_name=_('Reply-To'),
        help_text=
        _('Change the Reply-To address if you do not want to use the default orga address'
          ),
    )
    bcc = models.CharField(
        max_length=1000,
        blank=True,
        null=True,
        verbose_name=_('BCC'),
        help_text=
        _('Enter comma separated addresses. Will receive a blind copy of every mail sent from this template. This may be a LOT!'
          ),
    )

    def __str__(self):
        return f'{self.subject}'

    def to_mail(self, email, locale=None, context=None, skip_queue=False):
        with override(locale):
            context = context or dict()
            try:
                subject = str(self.subject).format(**context)
                text = str(self.text).format(**context)
            except KeyError as e:
                raise SendMailException(
                    f'Experienced KeyError when rendering Text: {str(e)}')

            mail = EMail(
                to=email,
                reply_to=self.reply_to,
                bcc=self.bcc,
                subject=subject,
                text=text,
            )
            if skip_queue:
                mail.send()
            else:
                mail.save()
        return mail
Exemple #18
0
class CfP(LogMixin, models.Model):
    event = models.OneToOneField(
        to='event.Event',
        on_delete=models.PROTECT,
    )
    headline = I18nCharField(
        max_length=300,
        null=True,
        blank=True,
        verbose_name=_('headline'),
    )
    text = I18nTextField(
        null=True,
        blank=True,
        verbose_name=_('text'),
        help_text=_('You can use markdown here.'),
    )
    default_type = models.ForeignKey(
        to='submission.SubmissionType',
        on_delete=models.PROTECT,
        related_name='+',
        verbose_name=_('Default submission type'),
    )
    deadline = models.DateTimeField(
        null=True,
        blank=True,
        verbose_name=_('deadline'),
        help_text=
        _('Please put in the last date you want to accept submissions from users.'
          ),
    )

    class urls(Urls):
        base = '{self.event.orga_urls.cfp}'
        questions = '{base}/questions'
        new_question = '{questions}/new'
        text = '{base}/text'
        edit_text = '{text}/edit'
        types = '{base}/types'
        new_type = '{types}/new'

    @property
    def is_open(self):
        if self.deadline is not None:
            return now() <= self.deadline
        return True

    def __str__(self) -> str:
        return str(self.headline)
Exemple #19
0
class ItemCategory(LoggedModel):
    """
    Items can be sorted into these categories.

    :param event: The event this category belongs to
    :type event: Event
    :param name: The name of this category
    :type name: str
    :param position: An integer, used for sorting
    :type position: int
    """
    event = models.ForeignKey(
        Event,
        on_delete=models.CASCADE,
        related_name='categories',
    )
    name = I18nCharField(
        max_length=255,
        verbose_name=_("Category name"),
    )
    description = I18nTextField(blank=True,
                                verbose_name=_("Category description"))
    position = models.IntegerField(default=0)

    class Meta:
        verbose_name = _("Product category")
        verbose_name_plural = _("Product categories")
        ordering = ('position', 'id')

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

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.event:
            self.event.get_cache().clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.event:
            self.event.get_cache().clear()

    @property
    def sortkey(self):
        return self.position, self.id

    def __lt__(self, other) -> bool:
        return self.sortkey < other.sortkey
class SpeakerInformation(LogMixin, FileCleanupMixin, models.Model):
    """Represents any information organisers want to show all or some
    submitters or speakers."""

    event = models.ForeignKey(to="event.Event",
                              related_name="information",
                              on_delete=models.CASCADE)
    target_group = models.CharField(
        choices=(
            ("submitters", _("All submitters")),
            ("accepted", _("All accepted speakers")),
            ("confirmed", _("Only confirmed speakers")),
        ),
        default="accepted",
        max_length=11,
    )
    limit_tracks = models.ManyToManyField(
        to="submission.Track",
        verbose_name=_("Limit to tracks"),
        blank=True,
        help_text=_("Leave empty to show this information to all tracks."),
    )
    limit_types = models.ManyToManyField(
        to="submission.SubmissionType",
        verbose_name=_("Limit to proposal types"),
        blank=True,
        help_text=_(
            "Leave empty to show this information for all proposal types."),
    )
    title = I18nCharField(verbose_name=_("Subject"), max_length=200)
    text = I18nTextField(verbose_name=_("Text"),
                         help_text=phrases.base.use_markdown)
    resource = models.FileField(
        verbose_name=_("file"),
        null=True,
        blank=True,
        help_text=_(
            "Please try to keep your upload small, preferably below 16 MB."),
        upload_to=resource_path,
    )

    objects = ScopedManager(event="event")

    class orga_urls(EventUrls):
        base = edit = "{self.event.orga_urls.information}{self.pk}/"
        delete = "{base}delete"
Exemple #21
0
class EmailTemplate(models.Model):
    title = I18nCharField(max_length=255)
    subject = I18nCharField(max_length=255)
    plain_text = I18nTextField()
    html_body = I18nRichTextField(blank=True)

    panels = [
        FieldPanel('title', heading=_('Title')),
        FieldPanel('subject', heading=_('Subject')),
        FieldPanel('plain_text', heading=_('Plain Text Body')),
        FieldPanel('html_body', heading=_('HTML Body')),
    ]

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

    class Meta:
        verbose_name = _('Email Template')
        verbose_name_plural = _('Email Templates')
Exemple #22
0
class Dormitory(django_models.Model):
    name = django_models.CharField(max_length=60)
    about = I18nTextField()

    geo_longitude = django_models.CharField(max_length=20)
    geo_latitude = django_models.CharField(max_length=20)
    address = django_models.CharField(max_length=150)

    contact_name = django_models.CharField(max_length=60)
    contact_email = django_models.CharField(max_length=60)
    contact_number = django_models.CharField(max_length=60)
    contact_fax = django_models.CharField(max_length=60)

    cover = django_models.ImageField()

    category = django_models.ForeignKey(DormitoryCategory,
                                        related_name='dormitories',
                                        on_delete=django_models.CASCADE)

    features = django_models.ManyToManyField(FeatureFilter,
                                             related_name='dormitories')

    manager = django_models.ForeignKey(User,
                                       related_name='dormitories',
                                       on_delete=django_models.CASCADE)

    objects = DormitoryQuerySet.as_manager()

    def is_owner(self, manager):
        return self.manager == manager

    def __str__(self):
        return f'{self.name}'

    class Meta:
        verbose_name_plural = 'Dormitories'
Exemple #23
0
class Item(LoggedModel):
    """
    An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
    Items are often also called 'products' but are named 'items' internally due to historic reasons.

    :param event: The event this item belongs to
    :type event: Event
    :param category: The category this belongs to. May be null.
    :type category: ItemCategory
    :param name: The name of this item
    :type name: str
    :param active: Whether this item is being sold.
    :type active: bool
    :param description: A short description
    :type description: str
    :param default_price: The item's default price
    :type default_price: decimal.Decimal
    :param tax_rate: The VAT tax that is included in this item's price (in %)
    :type tax_rate: decimal.Decimal
    :param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise)
    :type admission: bool
    :param picture: A product picture to be shown next to the product description
    :type picture: File
    :param available_from: The date this product goes on sale
    :type available_from: datetime
    :param available_until: The date until when the product is on sale
    :type available_until: datetime
    :param require_voucher: If set to ``True``, this item can only be bought using a voucher.
    :type require_voucher: bool
    :param hide_without_voucher: If set to ``True``, this item is only visible and available when a voucher is used.
    :type hide_without_voucher: bool
    :param allow_cancel: If set to ``False``, an order with this product can not be canceled by the user.
    :type allow_cancel: bool
    :param max_per_order: Maximum number of times this item can be in an order. None for unlimited.
    :type max_per_order: int
    :param min_per_order: Minimum number of times this item needs to be in an order if bought at all. None for unlimited.
    :type min_per_order: int
    :param checkin_attention: Requires special attention at check-in
    :type checkin_attention: bool
    """

    event = models.ForeignKey(
        Event,
        on_delete=models.PROTECT,
        related_name="items",
        verbose_name=_("Event"),
    )
    category = models.ForeignKey(
        ItemCategory,
        on_delete=models.PROTECT,
        related_name="items",
        blank=True,
        null=True,
        verbose_name=_("Category"),
        help_text=
        _("If you have many products, you can optionally sort them into categories to keep things organized."
          ))
    name = I18nCharField(
        max_length=255,
        verbose_name=_("Item name"),
    )
    active = models.BooleanField(
        default=True,
        verbose_name=_("Active"),
    )
    description = I18nTextField(
        verbose_name=_("Description"),
        help_text=_("This is shown below the product name in lists."),
        null=True,
        blank=True,
    )
    default_price = models.DecimalField(
        verbose_name=_("Default price"),
        help_text=
        _("If this product has multiple variations, you can set different prices for each of the "
          "variations. If a variation does not have a special price or if you do not have variations, "
          "this price will be used."),
        max_digits=7,
        decimal_places=2,
        null=True)
    free_price = models.BooleanField(
        default=False,
        verbose_name=_("Free price input"),
        help_text=
        _("If this option is active, your users can choose the price themselves. The price configured above "
          "is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect "
          "additional donations for your event. This is currently not supported for products that are "
          "bought as an add-on to other products."))
    tax_rule = models.ForeignKey('TaxRule',
                                 verbose_name=_('Sales tax'),
                                 on_delete=models.PROTECT,
                                 null=True,
                                 blank=True)
    admission = models.BooleanField(
        verbose_name=_("Is an admission ticket"),
        help_text=_(
            'Whether or not buying this product allows a person to enter '
            'your event'),
        default=False)
    position = models.IntegerField(default=0)
    picture = models.ImageField(verbose_name=_("Product picture"),
                                null=True,
                                blank=True,
                                max_length=255,
                                upload_to=itempicture_upload_to)
    available_from = models.DateTimeField(
        verbose_name=_("Available from"),
        null=True,
        blank=True,
        help_text=_('This product will not be sold before the given date.'))
    available_until = models.DateTimeField(
        verbose_name=_("Available until"),
        null=True,
        blank=True,
        help_text=_('This product will not be sold after the given date.'))
    require_voucher = models.BooleanField(
        verbose_name=_('This product can only be bought using a voucher.'),
        default=False,
        help_text=_(
            'To buy this product, the user needs a voucher that applies to this product '
            'either directly or via a quota.'))
    hide_without_voucher = models.BooleanField(
        verbose_name=
        _('This product will only be shown if a voucher matching the product is redeemed.'
          ),
        default=False,
        help_text=
        _('This product will be hidden from the event page until the user enters a voucher '
          'code that is specifically tied to this product (and not via a quota).'
          ))
    allow_cancel = models.BooleanField(
        verbose_name=_('Allow product to be canceled'),
        default=True,
        help_text=
        _('If this is active and the general event settings allow it, orders containing this product can be '
          'canceled by the user until the order is paid for. Users cannot cancel paid orders on their own '
          'and you can cancel orders at all times, regardless of this setting'
          ))
    min_per_order = models.IntegerField(
        verbose_name=_('Minimum amount per order'),
        null=True,
        blank=True,
        help_text=
        _('This product can only be bought if it is added to the cart at least this many times. If you keep '
          'the field empty or set it to 0, there is no special limit for this product.'
          ))
    max_per_order = models.IntegerField(
        verbose_name=_('Maximum amount per order'),
        null=True,
        blank=True,
        help_text=
        _('This product can only be bought at most this many times within one order. If you keep the field '
          'empty or set it to 0, there is no special limit for this product. The limit for the maximum '
          'number of items in the whole order applies regardless.'))
    checkin_attention = models.BooleanField(
        verbose_name=_('Requires special attention'),
        default=False,
        help_text=
        _('If you set this, the check-in app will show a visible warning that this ticket requires special '
          'attention. You can use this for example for student tickets to indicate to the person at '
          'check-in that the student ID card still needs to be checked.'))

    # !!! Attention: If you add new fields here, also add them to the copying code in
    # pretix/control/views/item.py if applicable.

    class Meta:
        verbose_name = _("Product")
        verbose_name_plural = _("Products")
        ordering = ("category__position", "category", "position")

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

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    def tax(self, price=None, base_price_is='auto'):
        price = price if price is not None else self.default_price
        if not self.tax_rule:
            return TaxedPrice(gross=price,
                              net=price,
                              tax=Decimal('0.00'),
                              rate=Decimal('0.00'),
                              name='')
        return self.tax_rule.tax(price, base_price_is=base_price_is)

    def is_available(self, now_dt: datetime = None) -> bool:
        """
        Returns whether this item is available according to its ``active`` flag
        and its ``available_from`` and ``available_until`` fields
        """
        now_dt = now_dt or now()
        if not self.active:
            return False
        if self.available_from and self.available_from > now_dt:
            return False
        if self.available_until and self.available_until < now_dt:
            return False
        return True

    def check_quotas(self,
                     ignored_quotas=None,
                     count_waitinglist=True,
                     subevent=None,
                     _cache=None):
        """
        This method is used to determine whether this Item is currently available
        for sale.

        :param ignored_quotas: If a collection if quota objects is given here, those
                               quotas will be ignored in the calculation. If this leads
                               to no quotas being checked at all, this method will return
                               unlimited availability.
        :returns: any of the return codes of :py:meth:`Quota.availability()`.

        :raises ValueError: if you call this on an item which has variations associated with it.
                            Please use the method on the ItemVariation object you are interested in.
        """
        check_quotas = set(
            getattr(
                self,
                '_subevent_quotas',  # Utilize cache in product list
                self.quotas.select_related('subevent').filter(
                    subevent=subevent) if subevent else self.quotas.all()))
        if not subevent and self.event.has_subevents:
            raise TypeError('You need to supply a subevent.')
        if ignored_quotas:
            check_quotas -= set(ignored_quotas)
        if not check_quotas:
            return Quota.AVAILABILITY_OK, sys.maxsize
        if self.has_variations:  # NOQA
            raise ValueError(
                'Do not call this directly on items which have variations '
                'but call this on their ItemVariation objects')
        return min([
            q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
            for q in check_quotas
        ],
                   key=lambda s: (s[0], s[1]
                                  if s[1] is not None else sys.maxsize))

    def allow_delete(self):
        from pretix.base.models.orders import CartPosition, OrderPosition

        return (not OrderPosition.objects.filter(item=self).exists()
                and not CartPosition.objects.filter(item=self).exists())

    @cached_property
    def has_variations(self):
        return self.variations.exists()

    @staticmethod
    def clean_per_order(min_per_order, max_per_order):
        if min_per_order is not None and max_per_order is not None:
            if min_per_order > max_per_order:
                raise ValidationError(
                    _('The maximum number per order can not be lower than the minimum number per '
                      'order.'))

    @staticmethod
    def clean_category(category, event):
        if category is not None and category.event is not None and category.event != event:
            raise ValidationError(
                _('The item\'s category must belong to the same event as the item.'
                  ))

    @staticmethod
    def clean_tax_rule(tax_rule, event):
        if tax_rule is not None and tax_rule.event is not None and tax_rule.event != event:
            raise ValidationError(
                _('The item\'s tax rule must belong to the same event as the item.'
                  ))

    @staticmethod
    def clean_available(from_date, until_date):
        if from_date is not None and until_date is not None:
            if from_date > until_date:
                raise ValidationError(
                    _('The item\'s availability cannot end before it starts.'))
Exemple #24
0
class SubEvent(EventMixin, LoggedModel):
    """
    This model represents a date within an event series.

    :param event: The event this belongs to
    :type event: Event
    :param active: Whether to show the subevent
    :type active: bool
    :param is_public: Whether to show the subevent in lists
    :type is_public: bool
    :param name: This event's full title
    :type name: 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
    """

    event = models.ForeignKey(Event,
                              related_name="subevents",
                              on_delete=models.PROTECT)
    active = models.BooleanField(
        default=False,
        verbose_name=_("Active"),
        help_text=_(
            "Only with this checkbox enabled, this date is visible in the "
            "frontend to users."))
    is_public = models.BooleanField(
        default=True,
        verbose_name=_("Show in lists"),
        help_text=_(
            "If selected, this event will show up publicly on the list of dates "
            "for your event."))
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    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"))
    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"),
    )
    geo_lat = models.FloatField(
        verbose_name=_("Latitude"),
        null=True,
        blank=True,
    )
    geo_lon = models.FloatField(verbose_name=_("Longitude"),
                                null=True,
                                blank=True)
    frontpage_text = I18nTextField(null=True,
                                   blank=True,
                                   verbose_name=_("Frontpage text"))
    seating_plan = models.ForeignKey('SeatingPlan',
                                     on_delete=models.PROTECT,
                                     null=True,
                                     blank=True,
                                     related_name='subevents')

    items = models.ManyToManyField('Item', through='SubEventItem')
    variations = models.ManyToManyField('ItemVariation',
                                        through='SubEventItemVariation')

    objects = ScopedManager(organizer='event__organizer')

    class Meta:
        verbose_name = _("Date in event series")
        verbose_name_plural = _("Dates in event series")
        ordering = ("date_from", "name")

    def __str__(self):
        return '{} - {}'.format(self.name, self.get_date_range_display())

    @property
    def free_seats(self):
        from .orders import CartPosition, Order, OrderPosition
        return self.seats.annotate(has_order=Exists(
            OrderPosition.objects.filter(
                order__event_id=self.event_id,
                subevent=self,
                seat_id=OuterRef('pk'),
                order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])),
                                   has_cart=Exists(
                                       CartPosition.objects.filter(
                                           event_id=self.event_id,
                                           subevent=self,
                                           seat_id=OuterRef('pk'),
                                           expires__gte=now()))).filter(
                                               has_order=False,
                                               has_cart=False,
                                               blocked=False)

    @cached_property
    def settings(self):
        return self.event.settings

    @cached_property
    def item_price_overrides(self):
        from .items import SubEventItem

        return {
            si.item_id: si.price
            for si in SubEventItem.objects.filter(subevent=self,
                                                  price__isnull=False)
        }

    @cached_property
    def var_price_overrides(self):
        from .items import SubEventItemVariation

        return {
            si.variation_id: si.price
            for si in SubEventItemVariation.objects.filter(subevent=self,
                                                           price__isnull=False)
        }

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

    @property
    def currency(self):
        return self.event.currency

    def allow_delete(self):
        return not self.orderposition_set.exists()

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    @staticmethod
    def clean_items(event, items):
        for item in items:
            if event != item.event:
                raise ValidationError(
                    _('One or more items do not belong to this event.'))

    @staticmethod
    def clean_variations(event, variations):
        for variation in variations:
            if event != variation.item.event:
                raise ValidationError(
                    _('One or more variations do not belong to this event.'))
Exemple #25
0
class Event(LogMixin, FileCleanupMixin, models.Model):
    """The Event class has direct or indirect relations to all other models.

    Since most models depend on the Event model in some way, they should
    preferably be accessed via the reverse relation on the event model to
    prevent data leaks.

    :param is_public: Is this event public yet? Should only be set via the
        ``pretalx.orga.views.EventLive`` view after the warnings have been
        acknowledged.
    :param locale_array: Contains the event's active locales as a comma
        separated string. Please use the ``locales`` property to interact
        with this information.
    :param accept_template: Templates for emails sent when accepting a talk.
    :param reject_template: Templates for emails sent when rejecting a talk.
    :param ack_template: Templates for emails sent when acknowledging that
        a submission was sent in.
    :param update_template: Templates for emails sent when a talk scheduling
        was modified.
    :param question_template: Templates for emails sent when a speaker has not
        yet answered a question, and organisers send out reminders.
    :param primary_color: Main event color. Accepts hex values like
        ``#00ff00``.
    :param custom_css: Custom event CSS. Has to pass fairly restrictive
        validation for security considerations.
    :param logo: Replaces the event name in the public header. Will be
        displayed at up to full header height and up to full content width.
    :param header_image: Replaces the header pattern and/or background
        color. Center-aligned, so when the window shrinks, the center will
        continue to be displayed.
    :param plugins: A list of active plugins as a comma-separated string.
        Please use the ``plugin_list`` property for interaction.
    """

    name = I18nCharField(max_length=200, verbose_name=_("Name"))
    slug = models.SlugField(
        max_length=50,
        db_index=True,
        unique=True,
        validators=[
            RegexValidator(
                regex=f"^[{SLUG_CHARS}]+$",
                message=_(
                    "The slug may only contain letters, numbers, dots and dashes."
                ),
            ),
            validate_event_slug_permitted,
        ],
        verbose_name=_("Short form"),
        help_text=_("The slug may only contain letters, numbers, dots and dashes."),
    )
    organiser = models.ForeignKey(
        to="Organiser",
        null=True,  # backwards compatibility, won't ever be empty
        related_name="events",
        on_delete=models.PROTECT,
    )
    is_public = models.BooleanField(default=False, verbose_name=_("Event is public"))
    date_from = models.DateField(verbose_name=_("Event start date"))
    date_to = models.DateField(verbose_name=_("Event end date"))
    timezone = models.CharField(
        choices=[(tz, tz) for tz in pytz.common_timezones],
        max_length=30,
        default="UTC",
        help_text=_(
            "All event dates will be localized and interpreted to be in this timezone."
        ),
    )
    email = models.EmailField(
        verbose_name=_("Organiser email address"),
        help_text=_("Will be used as Reply-To in emails."),
    )
    primary_color = models.CharField(
        max_length=7,
        null=True,
        blank=True,
        validators=[RegexValidator(r"#([0-9A-Fa-f]{3}){1,2}"),],
        verbose_name=_("Main event colour"),
        help_text=_(
            "Provide a hex value like #00ff00 if you want to style pretalx in your event's colour scheme."
        ),
    )
    custom_css = models.FileField(
        upload_to=event_css_path,
        null=True,
        blank=True,
        verbose_name=_("Custom Event CSS"),
        help_text=_(
            "Upload a custom CSS file if changing the primary colour is not sufficient for you."
        ),
    )
    logo = models.FileField(
        upload_to=event_logo_path,
        null=True,
        blank=True,
        verbose_name=_("Logo"),
        help_text=_(
            "If you provide a logo image, your event's name will not be shown in the event header. "
            "The logo will be displayed left-aligned, and be allowed to grow up to the width of the"
            "event content, if it is larger than that."
        ),
    )
    header_image = models.FileField(
        upload_to=event_logo_path,
        null=True,
        blank=True,
        verbose_name=_("Header image"),
        help_text=_(
            "If you provide a header image, it will be displayed instead of your event's color and/or header pattern "
            "at the top of all event pages. It will be center-aligned, so when the window shrinks, the center parts will "
            "continue to be displayed, and not stretched."
        ),
    )
    locale_array = models.TextField(default=settings.LANGUAGE_CODE)
    locale = models.CharField(
        max_length=32,
        default=settings.LANGUAGE_CODE,
        choices=settings.LANGUAGES,
        verbose_name=_("Default language"),
    )
    accept_template = models.ForeignKey(
        to="mail.MailTemplate",
        on_delete=models.CASCADE,
        related_name="+",
        null=True,
        blank=True,
    )
    ack_template = models.ForeignKey(
        to="mail.MailTemplate",
        on_delete=models.CASCADE,
        related_name="+",
        null=True,
        blank=True,
    )
    reject_template = models.ForeignKey(
        to="mail.MailTemplate",
        on_delete=models.CASCADE,
        related_name="+",
        null=True,
        blank=True,
    )
    update_template = models.ForeignKey(
        to="mail.MailTemplate",
        on_delete=models.CASCADE,
        related_name="+",
        null=True,
        blank=True,
    )
    question_template = models.ForeignKey(
        to="mail.MailTemplate",
        on_delete=models.CASCADE,
        related_name="+",
        null=True,
        blank=True,
    )
    landing_page_text = I18nTextField(
        verbose_name=_("Landing page text"),
        help_text=_(
            "This text will be shown on the landing page, alongside with links to the CfP and schedule, if appropriate."
        )
        + " "
        + phrases.base.use_markdown,
        null=True,
        blank=True,
    )
    plugins = models.TextField(null=True, blank=True, verbose_name=_("Plugins"))

    template_names = [
        f"{t}_template" for t in ("accept", "ack", "reject", "update", "question")
    ]

    class urls(EventUrls):
        base = "/{self.slug}/"
        login = "******"
        logout = "{base}logout"
        auth = "{base}auth/"
        logo = "{self.logo.url}"
        reset = "{base}reset"
        submit = "{base}submit/"
        user = "******"
        user_delete = "{base}me/delete"
        user_submissions = "{user}submissions/"
        user_mails = "{user}mails/"
        schedule = "{base}schedule/"
        sneakpeek = "{base}sneak/"
        talks = "{base}talk/"
        speakers = "{base}speaker/"
        changelog = "{schedule}changelog/"
        feed = "{schedule}feed.xml"
        export = "{schedule}export/"
        frab_xml = "{export}schedule.xml"
        frab_json = "{export}schedule.json"
        frab_xcal = "{export}schedule.xcal"
        ical = "{export}schedule.ics"
        widget_data_source = "{schedule}widget/v1.json"

    class orga_urls(EventUrls):
        create = "/orga/event/new"
        base = "/orga/event/{self.slug}/"
        live = "{base}live"
        delete = "{base}delete"
        cfp = "{base}cfp/"
        users = "{base}api/users"
        mail = "{base}mails/"
        compose_mails = "{mail}compose"
        mail_templates = "{mail}templates/"
        new_template = "{mail_templates}new"
        outbox = "{mail}outbox/"
        sent_mails = "{mail}sent"
        send_outbox = "{outbox}send"
        purge_outbox = "{outbox}purge"
        submissions = "{base}submissions/"
        submission_cards = "{base}submissions/cards/"
        stats = "{base}submissions/statistics/"
        submission_feed = "{base}submissions/feed/"
        new_submission = "{submissions}new"
        feedback = "{submissions}feedback/"
        speakers = "{base}speakers/"
        settings = edit_settings = "{base}settings/"
        review_settings = "{settings}review/"
        mail_settings = edit_mail_settings = "{settings}mail"
        widget_settings = "{settings}widget"
        team_settings = "{settings}team/"
        new_team = "{settings}team/new"
        room_settings = "{schedule}rooms/"
        new_room = "{room_settings}new"
        schedule = "{base}schedule/"
        schedule_export = "{schedule}export/"
        schedule_export_trigger = "{schedule_export}trigger"
        schedule_export_download = "{schedule_export}download"
        release_schedule = "{schedule}release"
        reset_schedule = "{schedule}reset"
        toggle_schedule = "{schedule}toggle"
        reviews = "{base}reviews/"
        schedule_api = "{base}schedule/api/"
        talks_api = "{schedule_api}talks/"
        plugins = "{settings}plugins"
        information = "{base}info/"
        new_information = "{base}info/new"

    class api_urls(EventUrls):
        base = "/api/events/{self.slug}/"
        submissions = "{base}submissions/"
        talks = "{base}talks/"
        schedules = "{base}schedules/"
        speakers = "{base}speakers/"
        reviews = "{base}reviews/"
        rooms = "{base}rooms/"

    class Meta:
        ordering = ("date_from",)

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

    @cached_property
    def locales(self) -> list:
        """Is a list of active event locales."""
        return self.locale_array.split(",")

    @cached_property
    def is_multilingual(self) -> bool:
        """Is ``True`` if the event supports more than one locale."""
        return len(self.locales) > 1

    @cached_property
    def named_locales(self) -> list:
        """Is a list of tuples of locale codes and natural names for this
        event."""
        enabled = set(self.locale_array.split(","))
        return [
            (language["code"], language["natural_name"])
            for language in settings.LANGUAGES_INFORMATION.values()
            if language["code"] in enabled
        ]

    @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.
        """
        return ObjectRelatedCache(self, field="slug")

    def save(self, *args, **kwargs):
        was_created = not bool(self.pk)
        super().save(*args, **kwargs)

        if was_created:
            self.build_initial_data()

    @property
    def plugin_list(self) -> list:
        """Provides a list of active plugins as strings, and is also an
        attribute setter."""
        if not self.plugins:
            return []
        return self.plugins.split(",")

    @plugin_list.setter
    def plugin_list(self, modules: list) -> None:
        from pretalx.common.plugins import get_all_plugins

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

        enable = set(modules) & (set(plugins_available) - plugins_active)
        disable = plugins_active - set(modules)

        for module in enable:
            with suppress(Exception):
                plugins_available[module].app.installed(self)
        for module in disable:
            with suppress(Exception):
                plugins_available[module].app.uninstalled(self)

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

    def enable_plugin(self, module: str) -> None:
        """Enables a named plugin.

        Caution, no validation is performed at this point. No exception is
        raised if the module is unknown. An already active module will not
        be added to the plugin list again.

        :param module: The module to be activated.
        """
        plugins_active = self.plugin_list

        if module not in plugins_active:
            plugins_active.append(module)
            self.plugin_list = plugins_active

    def disable_plugin(self, module: str) -> None:
        """Disbles a named plugin.

        Caution, no validation is performed at this point. No exception is
        raised if the module was not part of the active plugins.

        :param module: The module to be deactivated.
        """
        plugins_active = self.plugin_list

        if module in plugins_active:
            plugins_active.remove(module)
            self.plugin_list = plugins_active

    def _get_default_submission_type(self):
        from pretalx.submission.models import SubmissionType

        sub_type = SubmissionType.objects.filter(event=self).first()
        if not sub_type:
            sub_type = SubmissionType.objects.create(event=self, name="Talk")
        return sub_type

    @cached_property
    def fixed_templates(self) -> list:
        return [
            self.accept_template,
            self.ack_template,
            self.reject_template,
            self.update_template,
        ]

    def build_initial_data(self):
        from pretalx.mail.default_templates import (
            ACCEPT_TEXT,
            ACK_TEXT,
            GENERIC_SUBJECT,
            QUESTION_SUBJECT,
            QUESTION_TEXT,
            REJECT_TEXT,
            UPDATE_SUBJECT,
            UPDATE_TEXT,
        )
        from pretalx.mail.models import MailTemplate
        from pretalx.submission.models import CfP

        if not hasattr(self, "cfp"):
            CfP.objects.create(
                event=self, default_type=self._get_default_submission_type()
            )

        if not self.schedules.filter(version__isnull=True).exists():
            from pretalx.schedule.models import Schedule

            Schedule.objects.create(event=self)

        self.accept_template = self.accept_template or MailTemplate.objects.create(
            event=self, subject=GENERIC_SUBJECT, text=ACCEPT_TEXT
        )
        self.ack_template = self.ack_template or MailTemplate.objects.create(
            event=self, subject=GENERIC_SUBJECT, text=ACK_TEXT
        )
        self.reject_template = self.reject_template or MailTemplate.objects.create(
            event=self, subject=GENERIC_SUBJECT, text=REJECT_TEXT
        )
        self.update_template = self.update_template or MailTemplate.objects.create(
            event=self, subject=UPDATE_SUBJECT, text=UPDATE_TEXT
        )
        self.question_template = self.question_template or MailTemplate.objects.create(
            event=self, subject=QUESTION_SUBJECT, text=QUESTION_TEXT
        )

        if not self.review_phases.all().exists():
            from pretalx.submission.models import ReviewPhase

            cfp_deadline = self.cfp.deadline
            r = ReviewPhase.objects.create(
                event=self,
                name=_("Review"),
                start=cfp_deadline,
                end=self.datetime_from - relativedelta(months=-3),
                is_active=bool(not cfp_deadline or cfp_deadline < now()),
                position=0,
            )
            ReviewPhase.objects.create(
                event=self,
                name=_("Selection"),
                start=r.end,
                is_active=False,
                position=1,
                can_review=False,
                can_see_other_reviews="always",
                can_change_submission_state=True,
            )
        self.save()

    build_initial_data.alters_data = True

    def _delete_mail_templates(self):
        for template in self.template_names:
            setattr(self, template, None)
        self.save()
        self.mail_templates.all().delete()

    _delete_mail_templates.alters_data = True

    @scopes_disabled()
    def copy_data_from(self, other_event):
        from pretalx.orga.signals import event_copy_data

        protected_settings = ["custom_domain", "display_header_data"]
        self._delete_mail_templates()
        self.submission_types.exclude(pk=self.cfp.default_type_id).delete()
        for template in self.template_names:
            new_template = getattr(other_event, template)
            new_template.pk = None
            new_template.event = self
            new_template.save()
            setattr(self, template, new_template)
        submission_type_map = {}
        for submission_type in other_event.submission_types.all():
            submission_type_map[submission_type.pk] = submission_type
            is_default = submission_type == other_event.cfp.default_type
            submission_type.pk = None
            submission_type.event = self
            submission_type.save()
            if is_default:
                old_default = self.cfp.default_type
                self.cfp.default_type = submission_type
                self.cfp.save()
                old_default.delete()
        track_map = {}
        for track in other_event.tracks.all():
            track_map[track.pk] = track
            track.pk = None
            track.event = self
            track.save()
        question_map = {}
        for question in other_event.questions.all():
            question_map[question.pk] = question
            options = question.options.all()
            tracks = question.tracks.all().values_list("pk", flat=True)
            question.pk = None
            question.event = self
            question.save()
            question.tracks.set([])
            for option in options:
                option.pk = None
                option.question = question
                option.save()
            for track in tracks:
                question.tracks.add(track_map.get(track))

        for s in other_event.settings._objects.all():
            if s.value.startswith("file://") or s.key in protected_settings:
                continue
            s.object = self
            s.pk = None
            s.save()
        self.settings.flush()
        event_copy_data.send(
            sender=self,
            other=other_event.slug,
            question_map=question_map,
            track_map=track_map,
            submission_type_map=submission_type_map,
        )
        self.build_initial_data()  # make sure we get a functioning event

    copy_data_from.alters_data = True

    @cached_property
    def pending_mails(self) -> int:
        """The amount of currently unsent.

        :class:`~pretalx.mail.models.QueuedMail` objects.
        """
        return self.queued_mails.filter(sent__isnull=True).count()

    @cached_property
    def wip_schedule(self):
        """Returns the latest unreleased.

        :class:`~pretalx.schedule.models.schedule.Schedule`.

        :retval: :class:`~pretalx.schedule.models.schedule.Schedule`
        """
        schedule, _ = self.schedules.get_or_create(version__isnull=True)
        return schedule

    @cached_property
    def current_schedule(self):
        """Returns the latest released.

        :class:`~pretalx.schedule.models.schedule.Schedule`, or ``None`` before
        the first release.
        """
        return (
            self.schedules.order_by("-published")
            .filter(published__isnull=False)
            .first()
        )

    @cached_property
    def duration(self):
        return (self.date_to - self.date_from).days + 1

    def get_mail_backend(self, force_custom: bool = False) -> BaseEmailBackend:
        from pretalx.common.mail import CustomSMTPBackend

        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,
            )
        return get_connection(fail_silently=False)

    @cached_property
    def event(self):
        return self

    @cached_property
    def teams(self):
        """Returns all :class:`~pretalx.event.models.organiser.Team` objects
        that concern this event."""
        from .organiser import Team

        return Team.objects.filter(
            models.Q(limit_events__in=[self]) | models.Q(all_events=True),
            organiser=self.organiser,
        )

    @cached_property
    def datetime_from(self) -> dt.datetime:
        """The localised datetime of the event start date.

        :rtype: datetime
        """
        return make_aware(
            dt.datetime.combine(self.date_from, dt.time(hour=0, minute=0, second=0)),
            self.tz,
        )

    @cached_property
    def datetime_to(self) -> dt.datetime:
        """The localised datetime of the event end date.

        :rtype: datetime
        """
        return make_aware(
            dt.datetime.combine(self.date_to, dt.time(hour=23, minute=59, second=59)),
            self.tz,
        )

    @cached_property
    def tz(self):
        return pytz.timezone(self.timezone)

    @cached_property
    def reviews(self):
        from pretalx.submission.models import Review

        return Review.objects.filter(submission__event=self)

    @cached_property
    def active_review_phase(self):
        phase = self.review_phases.filter(is_active=True).first()
        if not phase and not self.review_phases.all().exists():
            from pretalx.submission.models import ReviewPhase

            cfp_deadline = self.cfp.deadline
            phase = ReviewPhase.objects.create(
                event=self,
                name=_("Review"),
                start=cfp_deadline,
                end=self.datetime_from - relativedelta(months=-3),
                is_active=bool(cfp_deadline),
                can_see_other_reviews="after_review",
                can_see_speaker_names=True,
            )
        return phase

    def update_review_phase(self):
        """This method activates the next review phase if the current one is
        over.

        If no review phase is active and if there is a new one to
        activate.
        """
        _now = now()
        future_phases = self.review_phases.all()
        old_phase = self.active_review_phase
        if old_phase and old_phase.end and old_phase.end > _now:
            return old_phase
        future_phases = future_phases.filter(position__gt=old_phase.position)
        next_phase = future_phases.order_by("position").first()
        if not (
            next_phase
            and (
                (next_phase.start and next_phase.start <= _now) or not next_phase.start
            )
        ):
            return old_phase
        old_phase.is_active = False
        old_phase.save()
        next_phase.activate()
        return next_phase

    update_review_phase.alters_data = True

    @cached_property
    def talks(self):
        """Returns a queryset of all.

        :class:`~pretalx.submission.models.submission.Submission` object in the
        current released schedule.
        """
        from pretalx.submission.models.submission import Submission

        if self.current_schedule:
            return (
                self.submissions.filter(
                    slots__in=self.current_schedule.talks.filter(is_visible=True)
                )
                .select_related("submission_type")
                .prefetch_related("speakers")
            )
        return Submission.objects.none()

    @cached_property
    def speakers(self):
        """Returns a queryset of all speakers (of type.

        :class:`~pretalx.person.models.user.User`) visible in the current
        released schedule.
        """
        from pretalx.person.models import User

        return User.objects.filter(submissions__in=self.talks).order_by("id").distinct()

    @cached_property
    def submitters(self):
        """Returns a queryset of all :class:`~pretalx.person.models.user.User`
        objects who have submitted to this event.

        Ignores users who have deleted all of their submissions.
        """
        from pretalx.person.models import User

        return (
            User.objects.filter(submissions__in=self.submissions.all())
            .prefetch_related("submissions")
            .order_by("id")
            .distinct()
        )

    @cached_property
    def cfp_flow(self):
        from pretalx.cfp.flow import CfPFlow

        return CfPFlow(self)

    def get_date_range_display(self) -> str:
        """Returns the localised, prettily formatted date range for this event.

        E.g. as long as the event takes place within the same month, the
        month is only named once.
        """
        return daterange(self.date_from, self.date_to)

    def release_schedule(
        self, name: str, user=None, notify_speakers: bool = False, comment: str = None
    ):
        """Releases a new :class:`~pretalx.schedule.models.schedule.Schedule`
        by finalizing the current WIP schedule.

        :param name: The new version name
        :param user: The :class:`~pretalx.person.models.user.User` executing the release
        :param notify_speakers: Generate emails for all speakers with changed slots.
        :param comment: Public comment for the release
        :type user: :class:`~pretalx.person.models.user.User`
        """
        self.wip_schedule.freeze(
            name=name, user=user, notify_speakers=notify_speakers, comment=comment
        )

    release_schedule.alters_data = True

    def send_orga_mail(self, text, stats=False):
        from django.utils.translation import override

        from pretalx.mail.models import QueuedMail

        context = {
            "event_dashboard": self.orga_urls.base.full(),
            "event_review": self.orga_urls.reviews.full(),
            "event_schedule": self.orga_urls.schedule.full(),
            "event_submissions": self.orga_urls.submissions.full(),
            "event_team": self.orga_urls.team_settings.full(),
            "submission_count": self.submissions.all().count(),
        }
        if stats:
            context.update(
                {
                    "talk_count": self.current_schedule.talks.filter(
                        is_visible=True
                    ).count(),
                    "review_count": self.reviews.count(),
                    "schedule_count": self.schedules.count() - 1,
                    "mail_count": self.queued_mails.filter(sent__isnull=False).count(),
                }
            )
        with override(self.locale):
            QueuedMail(
                subject=_("News from your content system"),
                text=str(text).format(**context),
                to=self.email,
            ).send()

    @transaction.atomic
    def shred(self):
        """Irrevocably deletes an event and all related data."""
        from pretalx.common.models import ActivityLog
        from pretalx.person.models import SpeakerProfile
        from pretalx.schedule.models import TalkSlot
        from pretalx.submission.models import (
            Answer,
            AnswerOption,
            Feedback,
            Question,
            Resource,
            Submission,
        )

        deletion_order = [
            (self.logged_actions(), False),
            (self.queued_mails.all(), False),
            (self.cfp, False),
            (self.mail_templates.all(), False),
            (self.information.all(), True),
            (TalkSlot.objects.filter(schedule__event=self), False),
            (Feedback.objects.filter(talk__event=self), False),
            (Resource.objects.filter(submission__event=self), True),
            (Answer.objects.filter(question__event=self), True),
            (AnswerOption.objects.filter(question__event=self), False),
            (Question.all_objects.filter(event=self), False),
            (Submission.all_objects.filter(event=self), True),
            (self.tracks.all(), False),
            (self.submission_types.all(), False),
            (self.schedules.all(), False),
            (SpeakerProfile.objects.filter(event=self), False),
            (self.rooms.all(), False),
            (ActivityLog.objects.filter(event=self), False),
            (self, False),
        ]

        self._delete_mail_templates()
        for entry, detail in deletion_order:
            if detail:
                for obj in entry:
                    obj.delete()
            else:
                entry.delete()

    shred.alters_data = True
Exemple #26
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 testmode: This event is in test mode
    :type testmode: bool
    :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)
    testmode = models.BooleanField(default=False)
    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."
                  ),
            ),
            EventSlugBanlistValidator()
        ],
        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=True,
        verbose_name=_("Show in lists"),
        help_text=
        _("If selected, this event will show up publicly on the list of events for your organizer account."
          ))
    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"),
    )
    geo_lat = models.FloatField(
        verbose_name=_("Latitude"),
        null=True,
        blank=True,
    )
    geo_lon = models.FloatField(
        verbose_name=_("Longitude"),
        null=True,
        blank=True,
    )
    plugins = models.TextField(
        null=False,
        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)
    seating_plan = models.ForeignKey('SeatingPlan',
                                     on_delete=models.PROTECT,
                                     null=True,
                                     blank=True,
                                     related_name='events')

    objects = ScopedManager(organizer='organizer')

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

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

    @property
    def free_seats(self):
        from .orders import CartPosition, Order, OrderPosition
        return self.seats.annotate(has_order=Exists(
            OrderPosition.objects.filter(
                order__event=self,
                seat_id=OuterRef('pk'),
                order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID])),
                                   has_cart=Exists(
                                       CartPosition.objects.filter(
                                           event=self,
                                           seat_id=OuterRef('pk'),
                                           expires__gte=now()))).filter(
                                               has_order=False,
                                               has_cart=False,
                                               blocked=False)

    @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 delete_all_orders(self, really=False):
        from .orders import OrderRefund, OrderPayment, OrderPosition, OrderFee

        if not really:
            raise TypeError("Pass really=True as a parameter.")

        OrderPosition.all.filter(order__event=self,
                                 addon_to__isnull=False).delete()
        OrderPosition.all.filter(order__event=self).delete()
        OrderFee.objects.filter(order__event=self).delete()
        OrderRefund.objects.filter(order__event=self).delete()
        OrderPayment.objects.filter(order__event=self).delete()
        self.orders.all().delete()

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

    def get_plugins(self):
        """
        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):
        """
        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.
        """
        from pretix.base.email import CustomSMTPBackend

        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.testmode = other.testmode
        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())
            oldid = q.pk
            q.pk = None
            q.event = self
            q.cached_availability_state = None
            q.cached_availability_number = None
            q.cached_availability_paid_orders = None
            q.cached_availability_time = None
            q.closed = False
            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])
            self.items.filter(hidden_if_available_id=oldid).update(
                hidden_if_available=q)

        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 q in self.questions.filter(dependency_question__isnull=False):
            q.dependency_question = question_map[q.dependency_question_id]
            q.save(update_fields=['dependency_question'])

        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])

        if other.seating_plan:
            if other.seating_plan.organizer_id == self.organizer_id:
                self.seating_plan = other.seating_plan
            else:
                self.organizer.seating_plans.create(
                    name=other.seating_plan.name,
                    layout=other.seating_plan.layout)
            self.save()

        for m in other.seat_category_mappings.filter(subevent__isnull=True):
            m.pk = None
            m.event = self
            m.product = item_map[m.product_id]
            m.save()

        for s in other.seats.filter(subevent__isnull=True):
            s.pk = None
            s.event = self
            s.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)
                # 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()
                except ValueError:
                    pass
            else:
                s.save()

        self.settings.flush()
        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, cached=False) -> dict:
        """
        Returns a dictionary of initialized payment providers mapped by their identifiers.
        """
        from ..signals import register_payment_providers

        if not cached or not hasattr(self, '_cached_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

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

    def get_html_mail_renderer(self):
        """
        Returns the currently selected HTML email renderer
        """
        return self.get_html_mail_renderers()[self.settings.mail_html_renderer]

    def get_html_mail_renderers(self) -> dict:
        """
        Returns a dictionary of initialized HTML email renderers mapped by their identifiers.
        """
        from ..signals import register_html_mail_renderers

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

    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]

    def subevents_annotated(self, channel):
        return SubEvent.annotated(self.subevents, channel)

    def subevents_sorted(self, queryset):
        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 = queryset.filter(
            Q(active=True) & Q(is_public=True) &
            (Q(
                Q(date_to__isnull=True)
                & Q(date_from__gte=now() - timedelta(hours=24)))
             | Q(date_to__gte=now() - timedelta(hours=24)))
        )  # order_by doesn't make sense with I18nField
        for f in reversed(orderfields):
            if f.startswith('-'):
                subevs = sorted(subevs, key=attrgetter(f[1:]), reverse=True)
            else:
                subevs = sorted(subevs, key=attrgetter(f))
        return subevs

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

        return OrderedDict(
            (k, v) for k, v in sorted(data.items(), key=lambda k: k[0]))

    @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', 'offsetting', 'giftcard'):
                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.cartposition_set.filter(addon_to__isnull=False).delete()
        self.cartposition_set.all().delete()
        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(self)
            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()
        from pretix.presale.style import regenerate_css

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

        regenerate_css.apply_async(args=(self.pk, ))

    def disable_plugin(self, module):
        plugins_active = self.get_plugins()
        from pretix.presale.style import regenerate_css

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

        regenerate_css.apply_async(args=(self.pk, ))

    @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.'))
Exemple #27
0
class SubEvent(EventMixin, LoggedModel):
    """
    This model represents a date within an event series.

    :param event: The event this belongs to
    :type event: Event
    :param active: Whether to show the subevent
    :type active: bool
    :param name: This event's full title
    :type name: 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
    """

    event = models.ForeignKey(Event,
                              related_name="subevents",
                              on_delete=models.PROTECT)
    active = models.BooleanField(
        default=False,
        verbose_name=_("Active"),
        help_text=_(
            "Only with this checkbox enabled, this date is visible in the "
            "frontend to users."))
    name = I18nCharField(
        max_length=200,
        verbose_name=_("Name"),
    )
    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"))
    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"),
    )
    frontpage_text = I18nTextField(null=True,
                                   blank=True,
                                   verbose_name=_("Frontpage text"))

    items = models.ManyToManyField('Item', through='SubEventItem')
    variations = models.ManyToManyField('ItemVariation',
                                        through='SubEventItemVariation')

    class Meta:
        verbose_name = _("Date in event series")
        verbose_name_plural = _("Dates in event series")
        ordering = ("date_from", "name")

    def __str__(self):
        return '{} - {}'.format(self.name, self.get_date_range_display())

    @cached_property
    def settings(self):
        return self.event.settings

    @cached_property
    def item_price_overrides(self):
        from .items import SubEventItem

        return {
            si.item_id: si.price
            for si in SubEventItem.objects.filter(subevent=self,
                                                  price__isnull=False)
        }

    @cached_property
    def var_price_overrides(self):
        from .items import SubEventItemVariation

        return {
            si.variation_id: si.price
            for si in SubEventItemVariation.objects.filter(subevent=self,
                                                           price__isnull=False)
        }

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

    def allow_delete(self):
        return self.event.subevents.count() > 1

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.event:
            self.event.cache.clear()
Exemple #28
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)
                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()
            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

    @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 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

    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(
            Q(is_superuser=True) | Q(twp=True))

    def allow_delete(self):
        return not self.orders.exists() and not self.invoices.exists()
Exemple #29
0
class Question(LoggedModel):
    """
    A question is an input field that can be used to extend a ticket by custom information,
    e.g. "Attendee age". The answers are found next to the position. The answers may be found
    in QuestionAnswers, attached to OrderPositions/CartPositions. A question can allow one of
    several input types, currently:

    * a number (``TYPE_NUMBER``)
    * a one-line string (``TYPE_STRING``)
    * a multi-line string (``TYPE_TEXT``)
    * a boolean (``TYPE_BOOLEAN``)
    * a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
    * a file upload (``TYPE_FILE``)
    * a date (``TYPE_DATE``)
    * a time (``TYPE_TIME``)
    * a date and a time (``TYPE_DATETIME``)

    :param event: The event this question belongs to
    :type event: Event
    :param question: The question text. This will be displayed next to the input field.
    :type question: str
    :param type: One of the above types
    :param required: Whether answering this question is required for submitting an order including
                     items associated with this question.
    :type required: bool
    :param items: A set of ``Items`` objects that this question should be applied to
    :param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
    :type ask_during_checkin: bool
    :param identifier: An arbitrary, internal identifier
    :type identifier: str
    """
    TYPE_NUMBER = "N"
    TYPE_STRING = "S"
    TYPE_TEXT = "T"
    TYPE_BOOLEAN = "B"
    TYPE_CHOICE = "C"
    TYPE_CHOICE_MULTIPLE = "M"
    TYPE_FILE = "F"
    TYPE_DATE = "D"
    TYPE_TIME = "H"
    TYPE_DATETIME = "W"
    TYPE_CHOICES = (
        (TYPE_NUMBER, _("Number")),
        (TYPE_STRING, _("Text (one line)")),
        (TYPE_TEXT, _("Multiline text")),
        (TYPE_BOOLEAN, _("Yes/No")),
        (TYPE_CHOICE, _("Choose one from a list")),
        (TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
        (TYPE_FILE, _("File upload")),
        (TYPE_DATE, _("Date")),
        (TYPE_TIME, _("Time")),
        (TYPE_DATETIME, _("Date and time")),
    )

    event = models.ForeignKey(Event, related_name="questions")
    question = I18nTextField(verbose_name=_("Question"))
    identifier = models.CharField(
        max_length=190,
        verbose_name=_("Internal identifier"),
        help_text=_(
            'You can enter any value here to make it easier to match the data with other sources. If you do '
            'not input one, we will generate one automatically.'))
    help_text = I18nTextField(
        verbose_name=_("Help text"),
        help_text=_(
            "If the question needs to be explained or clarified, do it here!"),
        null=True,
        blank=True,
    )
    type = models.CharField(max_length=5,
                            choices=TYPE_CHOICES,
                            verbose_name=_("Question type"))
    required = models.BooleanField(default=False,
                                   verbose_name=_("Required question"))
    items = models.ManyToManyField(
        Item,
        related_name='questions',
        verbose_name=_("Products"),
        blank=True,
        help_text=_(
            'This question will be asked to buyers of the selected products'))
    position = models.PositiveIntegerField(default=0,
                                           verbose_name=_("Position"))
    ask_during_checkin = models.BooleanField(
        verbose_name=_(
            'Ask during check-in instead of in the ticket buying process'),
        help_text=_(
            'This will only work if you handle your check-in with pretixdroid 1.8 or newer or '
            'pretixdesk 0.2 or newer.'),
        default=False)

    class Meta:
        verbose_name = _("Question")
        verbose_name_plural = _("Questions")
        ordering = ('position', 'id')

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

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    def clean_identifier(self, code):
        Question._clean_identifier(self.event, code, self)

    @staticmethod
    def _clean_identifier(event, code, instance=None):
        qs = Question.objects.filter(event=event, identifier=code)
        if instance:
            qs = qs.exclude(pk=instance.pk)
        if qs.exists():
            raise ValidationError(
                _('This identifier is already used for a different question.'))

    def save(self, *args, **kwargs):
        if not self.identifier:
            charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789')
            while True:
                code = get_random_string(length=8, allowed_chars=charset)
                if not Question.objects.filter(event=self.event,
                                               identifier=code).exists():
                    self.identifier = code
                    break
        super().save(*args, **kwargs)
        if self.event:
            self.event.cache.clear()

    @property
    def sortkey(self):
        return self.position, self.id

    def __lt__(self, other) -> bool:
        return self.sortkey < other.sortkey

    def clean_answer(self, answer):
        if self.required:
            if not answer or (self.type == Question.TYPE_BOOLEAN
                              and answer not in ("true", "True", True)):
                raise ValidationError(
                    _('An answer to this question is required to proceed.'))
        if not answer:
            if self.type == Question.TYPE_BOOLEAN:
                return False
            return None

        if self.type == Question.TYPE_CHOICE:
            try:
                return self.options.get(pk=answer)
            except:
                raise ValidationError(_('Invalid option selected.'))
        elif self.type == Question.TYPE_CHOICE_MULTIPLE:
            try:
                if isinstance(answer, str):
                    return list(self.options.filter(pk__in=answer.split(",")))
                else:
                    return list(self.options.filter(pk__in=answer))
            except:
                raise ValidationError(_('Invalid option selected.'))
        elif self.type == Question.TYPE_BOOLEAN:
            return answer in ('true', 'True', True)
        elif self.type == Question.TYPE_NUMBER:
            answer = formats.sanitize_separators(answer)
            answer = str(answer).strip()
            try:
                return Decimal(answer)
            except DecimalException:
                raise ValidationError(_('Invalid number input.'))
        elif self.type == Question.TYPE_DATE:
            if isinstance(answer, date):
                return answer
            try:
                return dateutil.parser.parse(answer).date()
            except:
                raise ValidationError(_('Invalid date input.'))
        elif self.type == Question.TYPE_TIME:
            if isinstance(answer, time):
                return answer
            try:
                return dateutil.parser.parse(answer).time()
            except:
                raise ValidationError(_('Invalid time input.'))
        elif self.type == Question.TYPE_DATETIME and answer:
            if isinstance(answer, datetime):
                return answer
            try:
                dt = dateutil.parser.parse(answer)
                if is_naive(dt):
                    dt = make_aware(
                        dt, pytz.timezone(self.event.settings.timezone))
                return dt
            except:
                raise ValidationError(_('Invalid datetime input.'))

        return answer

    @staticmethod
    def clean_items(event, items):
        for item in items:
            if event != item.event:
                raise ValidationError(
                    _('One or more items do not belong to this event.'))
Exemple #30
0
class ItemVariation(models.Model):
    """
    A variation of a product. For example, if your item is 'T-Shirt'
    then an example for a variation would be 'T-Shirt XL'.

    :param item: The item this variation belongs to
    :type item: Item
    :param value: A string defining this variation
    :type value: str
    :param description: A short description
    :type description: str
    :param active: Whether this variation is being sold.
    :type active: bool
    :param default_price: This variation's default price
    :type default_price: decimal.Decimal
    """
    item = models.ForeignKey(Item, related_name='variations')
    value = I18nCharField(max_length=255, verbose_name=_('Description'))
    active = models.BooleanField(
        default=True,
        verbose_name=_("Active"),
    )
    description = I18nTextField(
        verbose_name=_("Description"),
        help_text=_("This is shown below the variation name in lists."),
        null=True,
        blank=True,
    )
    position = models.PositiveIntegerField(default=0,
                                           verbose_name=_("Position"))
    default_price = models.DecimalField(
        decimal_places=2,
        max_digits=7,
        null=True,
        blank=True,
        verbose_name=_("Default price"),
    )

    class Meta:
        verbose_name = _("Product variation")
        verbose_name_plural = _("Product variations")
        ordering = ("position", "id")

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

    @property
    def price(self):
        return self.default_price if self.default_price is not None else self.item.default_price

    def tax(self, price=None):
        price = price or self.price
        if not self.item.tax_rule:
            return TaxedPrice(gross=price,
                              net=price,
                              tax=Decimal('0.00'),
                              rate=Decimal('0.00'),
                              name='')
        return self.item.tax_rule.tax(price)

    def delete(self, *args, **kwargs):
        super().delete(*args, **kwargs)
        if self.item:
            self.item.event.cache.clear()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.item:
            self.item.event.cache.clear()

    def check_quotas(self,
                     ignored_quotas=None,
                     count_waitinglist=True,
                     subevent=None,
                     _cache=None) -> Tuple[int, int]:
        """
        This method is used to determine whether this ItemVariation is currently
        available for sale in terms of quotas.

        :param ignored_quotas: If a collection if quota objects is given here, those
                               quotas will be ignored in the calculation. If this leads
                               to no quotas being checked at all, this method will return
                               unlimited availability.
        :param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
        :returns: any of the return codes of :py:meth:`Quota.availability()`.
        """
        check_quotas = set(
            getattr(
                self,
                '_subevent_quotas',  # Utilize cache in product list
                self.quotas.filter(
                    subevent=subevent).select_related('subevent')
                if subevent else self.quotas.all()))
        if ignored_quotas:
            check_quotas -= set(ignored_quotas)
        if not subevent and self.item.event.has_subevents:  # NOQA
            raise TypeError('You need to supply a subevent.')
        if not check_quotas:
            return Quota.AVAILABILITY_OK, sys.maxsize
        return min([
            q.availability(count_waitinglist=count_waitinglist, _cache=_cache)
            for q in check_quotas
        ],
                   key=lambda s: (s[0], s[1]
                                  if s[1] is not None else sys.maxsize))

    def __lt__(self, other):
        if self.position == other.position:
            return self.id < other.id
        return self.position < other.position

    def allow_delete(self):
        from pretix.base.models.orders import CartPosition, OrderPosition

        return (not OrderPosition.objects.filter(variation=self).exists()
                and not CartPosition.objects.filter(variation=self).exists())

    def is_only_variation(self):
        return ItemVariation.objects.filter(item=self.item).count() == 1