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"]
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"
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
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']
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)
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)
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)
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/'
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)
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')
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)
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> ')
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
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
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)}"
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']
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
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)
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"
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')
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'
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.'))
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.'))
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
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.'))
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()
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()
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.'))
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