mirror of
https://github.com/pretix/pretix.git
synced 2026-05-07 15:34:02 +00:00
@@ -423,13 +423,13 @@ class CloneEventSerializer(EventSerializer):
|
||||
class SubEventItemSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ('item', 'price', 'disabled')
|
||||
fields = ('item', 'price', 'disabled', 'available_from', 'available_until')
|
||||
|
||||
|
||||
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEventItemVariation
|
||||
fields = ('variation', 'price', 'disabled')
|
||||
fields = ('variation', 'price', 'disabled', 'available_from', 'available_until')
|
||||
|
||||
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
|
||||
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(
|
||||
|
||||
@@ -35,8 +35,8 @@ from i18nfield.forms import I18nInlineFormSet
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.forms.widgets import DatePickerWidget, TimePickerWidget
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import SubEventItem
|
||||
from pretix.base.reldate import RelativeDateTimeField
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
@@ -263,10 +263,16 @@ class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ['price', 'disabled']
|
||||
fields = ['price', 'disabled', 'available_from', 'available_until']
|
||||
widgets = {
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
'available_until': SplitDateTimePickerWidget(),
|
||||
'price': forms.TextInput
|
||||
}
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
'available_until': SplitDateTimeField,
|
||||
}
|
||||
|
||||
|
||||
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
@@ -276,11 +282,61 @@ class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelFor
|
||||
self.fields['price'].label = '{} – {}'.format(str(self.item), self.variation.value)
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ['price', 'disabled']
|
||||
model = SubEventItemVariation
|
||||
fields = ['price', 'disabled', 'available_from', 'available_until']
|
||||
widgets = {
|
||||
'available_from': SplitDateTimePickerWidget(),
|
||||
'available_until': SplitDateTimePickerWidget(),
|
||||
'price': forms.TextInput
|
||||
}
|
||||
field_classes = {
|
||||
'available_from': SplitDateTimeField,
|
||||
'available_until': SplitDateTimeField,
|
||||
}
|
||||
|
||||
|
||||
class BulkSubEventItemForm(SubEventItemForm):
|
||||
rel_available_from = RelativeDateTimeField(
|
||||
label=_('Available from'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
)
|
||||
rel_available_until = RelativeDateTimeField(
|
||||
label=_('Available_until'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
del self.fields['available_from']
|
||||
del self.fields['available_until']
|
||||
if self.instance and self.instance.available_from and 'rel_available_from' not in self.initial:
|
||||
self.initial['rel_available_from'] = RelativeDateWrapper(self.instance.available_from)
|
||||
if self.instance and self.instance.available_until and 'rel_available_until' not in self.initial:
|
||||
self.initial['rel_available_until'] = RelativeDateWrapper(self.instance.available_until)
|
||||
|
||||
|
||||
class BulkSubEventItemVariationForm(SubEventItemVariationForm):
|
||||
rel_available_from = RelativeDateTimeField(
|
||||
label=_('Available from'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
)
|
||||
rel_available_until = RelativeDateTimeField(
|
||||
label=_('Available_until'),
|
||||
required=False,
|
||||
limit_choices=('date_from', 'date_to'),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
del self.fields['available_from']
|
||||
del self.fields['available_until']
|
||||
if self.instance and self.instance.available_from and 'rel_available_from' not in self.initial:
|
||||
self.initial['rel_available_from'] = RelativeDateWrapper(self.instance.available_from)
|
||||
if self.instance and self.instance.available_until and 'rel_available_until' not in self.initial:
|
||||
self.initial['rel_available_until'] = RelativeDateWrapper(self.instance.available_until)
|
||||
|
||||
|
||||
class QuotaFormSet(I18nInlineFormSet):
|
||||
|
||||
@@ -480,21 +480,34 @@
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Item prices" %}</legend>
|
||||
<legend>{% trans "Product settings" %}</legend>
|
||||
<p class="text-muted">
|
||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||
</p>
|
||||
{% for f in itemvar_forms %}
|
||||
<div class="form-group">
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.rel_available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
|
||||
{% bootstrap_field f.rel_available_from form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.rel_available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
|
||||
{% bootstrap_field f.rel_available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -151,17 +151,29 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Item prices" %}</legend>
|
||||
{% for f in itemvar_forms %}
|
||||
<div class="form-group">
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="bulkedit_inline" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-5">
|
||||
<label class="text-muted"> </label><br>
|
||||
{% bootstrap_field f.disabled layout="bulkedit_inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
|
||||
{% bootstrap_field f.available_from form_group_class="" layout="bulkedit_inline" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="bulkedit_inline" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -122,21 +122,34 @@
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Item prices" %}</legend>
|
||||
<legend>{% trans "Product settings" %}</legend>
|
||||
<p class="text-muted">
|
||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||
</p>
|
||||
{% for f in itemvar_forms %}
|
||||
<div class="form-group">
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
<br>
|
||||
<span class="optional">{% trans "Optional" %}</span>
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label><br>
|
||||
{% bootstrap_field f.available_from form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label><br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
||||
@@ -68,9 +68,10 @@ from pretix.control.forms.checkin import SimpleCheckinListForm
|
||||
from pretix.control.forms.filter import SubEventFilterForm
|
||||
from pretix.control.forms.item import QuotaForm
|
||||
from pretix.control.forms.subevents import (
|
||||
CheckinListFormSet, QuotaFormSet, RRuleFormSet, SubEventBulkEditForm,
|
||||
SubEventBulkForm, SubEventForm, SubEventItemForm,
|
||||
SubEventItemVariationForm, SubEventMetaValueForm, TimeFormSet,
|
||||
BulkSubEventItemForm, BulkSubEventItemVariationForm, CheckinListFormSet,
|
||||
QuotaFormSet, RRuleFormSet, SubEventBulkEditForm, SubEventBulkForm,
|
||||
SubEventForm, SubEventItemForm, SubEventItemVariationForm,
|
||||
SubEventMetaValueForm, TimeFormSet,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import subevent_forms
|
||||
@@ -193,6 +194,8 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
meta_form = SubEventMetaValueForm
|
||||
meta_model = SubEventMetaValue
|
||||
itemformclass = SubEventItemForm
|
||||
itemvarformclass = SubEventItemVariationForm
|
||||
|
||||
@cached_property
|
||||
def plugin_forms(self):
|
||||
@@ -391,11 +394,17 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
|
||||
if self.copy_from:
|
||||
se_item_instances = {
|
||||
sei.item_id: SubEventItem(item=sei.item, price=sei.price, disabled=sei.disabled)
|
||||
sei.item_id: SubEventItem(
|
||||
item=sei.item, price=sei.price, disabled=sei.disabled,
|
||||
available_from=sei.available_from, available_until=sei.available_until
|
||||
)
|
||||
for sei in SubEventItem.objects.filter(subevent=self.copy_from).select_related('item')
|
||||
}
|
||||
se_var_instances = {
|
||||
sei.variation_id: SubEventItemVariation(variation=sei.variation, price=sei.price, disabled=sei.disabled)
|
||||
sei.variation_id: SubEventItemVariation(
|
||||
variation=sei.variation, price=sei.price, disabled=sei.disabled,
|
||||
available_from=sei.available_from, available_until=sei.available_until
|
||||
)
|
||||
for sei in SubEventItemVariation.objects.filter(subevent=self.copy_from).select_related('variation')
|
||||
}
|
||||
|
||||
@@ -404,7 +413,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
if i.has_variations:
|
||||
for v in i.variations.all():
|
||||
inst = se_var_instances.get(v.pk) or SubEventItemVariation(subevent=self.object, variation=v)
|
||||
formlist.append(SubEventItemVariationForm(
|
||||
formlist.append(self.itemvarformclass(
|
||||
prefix='itemvar-{}'.format(v.pk),
|
||||
item=i, variation=v,
|
||||
instance=inst,
|
||||
@@ -412,7 +421,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
))
|
||||
else:
|
||||
inst = se_item_instances.get(i.pk) or SubEventItem(subevent=self.object, item=i)
|
||||
formlist.append(SubEventItemForm(
|
||||
formlist.append(self.itemformclass(
|
||||
prefix='item-{}'.format(i.pk),
|
||||
item=i,
|
||||
instance=inst,
|
||||
@@ -641,6 +650,8 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
permission = 'can_change_settings'
|
||||
context_object_name = 'subevent'
|
||||
form_class = SubEventBulkForm
|
||||
itemformclass = BulkSubEventItemForm
|
||||
itemvarformclass = BulkSubEventItemVariationForm
|
||||
|
||||
def is_valid(self, form):
|
||||
return self.rrule_formset.is_valid() and self.time_formset.is_valid() and super().is_valid(form)
|
||||
@@ -846,6 +857,18 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Crea
|
||||
i = copy.copy(f.instance)
|
||||
i.pk = None
|
||||
i.subevent = se
|
||||
|
||||
i.available_from = (
|
||||
f.cleaned_data['rel_available_from'].datetime(se)
|
||||
if f.cleaned_data.get('rel_available_from')
|
||||
else None
|
||||
)
|
||||
i.available_until = (
|
||||
f.cleaned_data['rel_available_until'].datetime(se)
|
||||
if f.cleaned_data.get('rel_available_until')
|
||||
else None
|
||||
)
|
||||
|
||||
if isinstance(i, SubEventItem):
|
||||
to_save_items.append(i)
|
||||
else:
|
||||
@@ -962,16 +985,19 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
|
||||
def cached_num(self):
|
||||
return self.get_queryset().count()
|
||||
|
||||
itemformclass = SubEventItemForm
|
||||
itemvarformclass = SubEventItemVariationForm
|
||||
|
||||
@cached_property
|
||||
def itemvar_forms(self):
|
||||
matches = defaultdict(list)
|
||||
for sei in SubEventItem.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).order_by().values('item', 'price', 'disabled').annotate(c=Count('*')):
|
||||
).order_by().values('item', 'price', 'disabled', 'available_from', 'available_until').annotate(c=Count('*')):
|
||||
matches['item', sei['item']].append(sei)
|
||||
for sei in SubEventItemVariation.objects.filter(
|
||||
subevent__in=self.get_queryset()
|
||||
).order_by().values('variation', 'price', 'disabled').annotate(c=Count('*')):
|
||||
).order_by().values('variation', 'price', 'disabled', 'available_from', 'available_until').annotate(c=Count('*')):
|
||||
matches['variation', sei['variation']].append(sei)
|
||||
total = self.cached_num
|
||||
|
||||
@@ -981,10 +1007,13 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
|
||||
for v in i.variations.all():
|
||||
m = matches['variation', v.pk]
|
||||
if m and len(m) == 1 and m[0]['c'] == total:
|
||||
inst = SubEventItemVariation(variation=v, disabled=m[0]['disabled'], price=m[0]['price'])
|
||||
inst = SubEventItemVariation(
|
||||
variation=v, disabled=m[0]['disabled'], price=m[0]['price'],
|
||||
available_from=m[0]['available_from'], available_until=m[0]['available_until']
|
||||
)
|
||||
else:
|
||||
inst = SubEventItemVariation(variation=v)
|
||||
formlist.append(SubEventItemVariationForm(
|
||||
formlist.append(self.itemvarformclass(
|
||||
prefix='itemvar-{}'.format(v.pk),
|
||||
item=i, variation=v,
|
||||
instance=inst,
|
||||
@@ -993,10 +1022,13 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
|
||||
else:
|
||||
m = matches['item', i.pk]
|
||||
if m and len(m) == 1 and m[0]['c'] == total:
|
||||
inst = SubEventItem(item=i, disabled=m[0]['disabled'], price=m[0]['price'])
|
||||
inst = SubEventItem(
|
||||
item=i, disabled=m[0]['disabled'], price=m[0]['price'],
|
||||
available_from=m[0]['available_from'], available_until=m[0]['available_until']
|
||||
)
|
||||
else:
|
||||
inst = SubEventItem(item=i)
|
||||
formlist.append(SubEventItemForm(
|
||||
formlist.append(self.itemformclass(
|
||||
prefix='item-{}'.format(i.pk),
|
||||
item=i,
|
||||
instance=inst,
|
||||
@@ -1405,12 +1437,16 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
|
||||
u['price'] = f.cleaned_data.get('price')
|
||||
if f.prefix + 'disabled' in self.request.POST.getlist('_bulk'):
|
||||
u['disabled'] = f.cleaned_data.get('disabled')
|
||||
if f.prefix + 'available_from' in self.request.POST.getlist('_bulk'):
|
||||
u['available_from'] = f.cleaned_data.get('available_from')
|
||||
if f.prefix + 'available_until' in self.request.POST.getlist('_bulk'):
|
||||
u['available_until'] = f.cleaned_data.get('available_until')
|
||||
|
||||
if not u:
|
||||
continue
|
||||
|
||||
if isinstance(f, SubEventItemForm):
|
||||
if u.get('price') is None and not u.get('disabled'):
|
||||
if u.get('price') is None and not u.get('disabled') and not u.get('available_from') and not u.get('available_until'):
|
||||
SubEventItem.objects.filter(
|
||||
subevent__in=self.get_queryset(),
|
||||
item=f.instance.item,
|
||||
@@ -1423,7 +1459,7 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
|
||||
defaults=u
|
||||
)
|
||||
elif isinstance(f, SubEventItemVariationForm):
|
||||
if u.get('price') is None and not u.get('disabled'):
|
||||
if u.get('price') is None and not u.get('disabled') and not u.get('available_from') and not u.get('available_until'):
|
||||
SubEventItemVariation.objects.filter(
|
||||
subevent__in=self.get_queryset(),
|
||||
variation=f.instance.variation,
|
||||
|
||||
@@ -44,7 +44,7 @@ import pytz
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import (
|
||||
Count, Exists, IntegerField, OuterRef, Prefetch, Value,
|
||||
Count, Exists, IntegerField, OuterRef, Prefetch, Q, Value,
|
||||
)
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
@@ -139,9 +139,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
|
||||
subevent_disabled=Exists(
|
||||
SubEventItemVariation.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
variation_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
disabled=True,
|
||||
)
|
||||
),
|
||||
).filter(
|
||||
@@ -156,9 +156,9 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
|
||||
has_variations=Count('variations'),
|
||||
subevent_disabled=Exists(
|
||||
SubEventItem.objects.filter(
|
||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
||||
item_id=OuterRef('pk'),
|
||||
subevent=subevent,
|
||||
disabled=True,
|
||||
)
|
||||
),
|
||||
requires_seat=requires_seat,
|
||||
|
||||
@@ -718,3 +718,17 @@ table td > .checkbox input[type="checkbox"] {
|
||||
.pos-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#subevent-bulk-create-form {
|
||||
fieldset {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
.subevent-itemvar-group {
|
||||
label.control-label {
|
||||
padding-top: 0;
|
||||
}
|
||||
label:not(.control-label) {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user