mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
33
src/pretix/base/migrations/0183_auto_20210423_0829.py
Normal file
33
src/pretix/base/migrations/0183_auto_20210423_0829.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.0.13 on 2021-04-23 08:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0182_question_valid_file_portrait'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subeventitem',
|
||||
name='available_from',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subeventitem',
|
||||
name='available_until',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subeventitemvariation',
|
||||
name='available_from',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subeventitemvariation',
|
||||
name='available_until',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -50,6 +50,7 @@ from django.core.validators import (
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
@@ -280,6 +281,14 @@ class EventMixin:
|
||||
vars_reserved = set()
|
||||
items_gone = set()
|
||||
vars_gone = set()
|
||||
items_disabled = set()
|
||||
vars_disabled = set()
|
||||
|
||||
if hasattr(self, 'disabled_items'): # SubEventItem
|
||||
items_disabled = set(self.disabled_items.split(","))
|
||||
|
||||
if hasattr(self, 'disabled_vars'): # SubEventItemVariation
|
||||
vars_disabled = set(self.disabled_vars.split(","))
|
||||
|
||||
r = getattr(self, '_quota_cache', {})
|
||||
for q in self.active_quotas:
|
||||
@@ -300,8 +309,19 @@ class EventMixin:
|
||||
items_gone.update(q.active_items.split(","))
|
||||
if q.active_variations:
|
||||
vars_gone.update(q.active_variations.split(","))
|
||||
if not self.active_quotas:
|
||||
|
||||
items_available -= items_disabled
|
||||
items_reserved -= items_disabled
|
||||
items_gone -= items_disabled
|
||||
vars_available -= vars_disabled
|
||||
vars_reserved -= vars_disabled
|
||||
vars_gone -= vars_gone
|
||||
|
||||
if not self.active_quotas or (
|
||||
not items_available and not items_reserved and not items_gone and not vars_gone and not vars_available and not vars_reserved
|
||||
):
|
||||
return None
|
||||
|
||||
if items_available - items_reserved - items_gone or vars_available - vars_reserved - vars_gone:
|
||||
return Quota.AVAILABILITY_OK
|
||||
if items_reserved - items_gone or vars_reserved - vars_gone:
|
||||
@@ -1227,6 +1247,36 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
distance_only_within_row=self.settings.seating_distance_within_row)
|
||||
return qs_annotated
|
||||
|
||||
@classmethod
|
||||
def annotated(cls, qs, channel='web'):
|
||||
from .items import SubEventItem, SubEventItemVariation
|
||||
|
||||
qs = super().annotated(qs, channel)
|
||||
qs = qs.annotate(
|
||||
disabled_items=Coalesce(
|
||||
Subquery(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
subevent=OuterRef('pk'),
|
||||
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
|
||||
output_field=models.TextField(),
|
||||
),
|
||||
Value('')
|
||||
),
|
||||
disabled_vars=Coalesce(
|
||||
Subquery(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
subevent=OuterRef('pk'),
|
||||
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
|
||||
output_field=models.TextField(),
|
||||
),
|
||||
Value('')
|
||||
)
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
return self.event.settings
|
||||
|
||||
@@ -151,11 +151,29 @@ class SubEventItem(models.Model):
|
||||
:type item: Item
|
||||
:param price: The modified price (or ``None`` for the original price)
|
||||
:type price: Decimal
|
||||
:param disabled: Disable the product for this subevent
|
||||
:type disabled: bool
|
||||
:param available_until: The date until when the product is on sale
|
||||
:type available_until: datetime
|
||||
: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
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
item = models.ForeignKey('Item', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
|
||||
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.')
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -167,6 +185,16 @@ class SubEventItem(models.Model):
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def is_available(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
if self.disabled:
|
||||
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
|
||||
|
||||
|
||||
class SubEventItemVariation(models.Model):
|
||||
"""
|
||||
@@ -179,11 +207,29 @@ class SubEventItemVariation(models.Model):
|
||||
:type variation: ItemVariation
|
||||
:param price: The modified price (or ``None`` for the original price)
|
||||
:type price: Decimal
|
||||
:param disabled: Disable the product for this subevent
|
||||
:type disabled: bool
|
||||
:param available_until: The date until when the product is on sale
|
||||
:type available_until: datetime
|
||||
: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
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
disabled = models.BooleanField(default=False)
|
||||
disabled = models.BooleanField(default=False, verbose_name=_('Disable product for this date'))
|
||||
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.')
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -195,6 +241,16 @@ class SubEventItemVariation(models.Model):
|
||||
if self.subevent:
|
||||
self.subevent.event.cache.clear()
|
||||
|
||||
def is_available(self, now_dt: datetime=None) -> bool:
|
||||
now_dt = now_dt or now()
|
||||
if self.disabled:
|
||||
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 filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
||||
q = (
|
||||
|
||||
@@ -292,10 +292,11 @@ class CartManager:
|
||||
if self._sales_channel not in op.item.sales_channels:
|
||||
raise CartError(error_messages['unavailable'])
|
||||
|
||||
if op.subevent and op.item.pk in op.subevent.item_overrides and op.subevent.item_overrides[op.item.pk].disabled:
|
||||
if op.subevent and op.item.pk in op.subevent.item_overrides and not op.subevent.item_overrides[op.item.pk].is_available():
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and op.subevent.var_overrides[op.variation.pk].disabled:
|
||||
if op.subevent and op.variation and op.variation.pk in op.subevent.var_overrides and \
|
||||
not op.subevent.var_overrides[op.variation.pk].is_available():
|
||||
raise CartError(error_messages['not_for_sale'])
|
||||
|
||||
if op.item.has_variations and not op.variation:
|
||||
|
||||
@@ -696,12 +696,13 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and cp.subevent.item_overrides[cp.item.pk].disabled:
|
||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(now_dt):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and cp.subevent.var_overrides[cp.variation.pk].disabled:
|
||||
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
|
||||
not cp.subevent.var_overrides[cp.variation.pk].is_available(now_dt):
|
||||
err = err or error_messages['unavailable']
|
||||
delete(cp)
|
||||
continue
|
||||
|
||||
@@ -166,6 +166,56 @@ def timeline_for_event(event, subevent=None):
|
||||
})
|
||||
))
|
||||
|
||||
if subevent:
|
||||
for sei in subevent.item_overrides.values():
|
||||
if sei.available_from:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=sei.available_from,
|
||||
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(name=str(sei.item)),
|
||||
edit_url=reverse('control:event.subevent', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
))
|
||||
if sei.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=sei.available_until,
|
||||
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(name=str(sei.item)),
|
||||
edit_url=reverse('control:event.subevent', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
))
|
||||
for sei in subevent.var_overrides.values():
|
||||
if sei.available_from:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=sei.available_from,
|
||||
description=pgettext_lazy('timeline', 'Product "{name}" becomes available').format(
|
||||
name=str(sei.variation.item) + ' – ' + str(sei.variation)),
|
||||
edit_url=reverse('control:event.subevent', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
))
|
||||
if sei.available_until:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=sei.available_until,
|
||||
description=pgettext_lazy('timeline', 'Product "{name}" becomes unavailable').format(
|
||||
name=str(sei.variation.item) + ' – ' + str(sei.variation)),
|
||||
edit_url=reverse('control:event.subevent', kwargs={
|
||||
'event': event.slug,
|
||||
'organizer': event.organizer.slug,
|
||||
'subevent': subevent.pk,
|
||||
})
|
||||
))
|
||||
|
||||
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
|
||||
if p.available_from:
|
||||
tl.append(TimelineEvent(
|
||||
|
||||
Reference in New Issue
Block a user