mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Add program times for items (Z#23178639)
* Add program times for items * Fix frontend date validation * Add ical data for program times [wip] * Improve ical data for program times * Remove duplicate code and add comments * Adjust migration * Remove program times form for event series * Add pdf placeholder [wip] * Improve explanation text with suggestion Co-authored-by: Raphael Michel <michel@pretix.eu> * Fix import sorting * Improve ical generation * Improve ical entry description * Fix migration * Add copyability for program times fot items and events * Update migration * Add API endpoints/functions, fix isort * Improve variable name Co-authored-by: Richard Schreiber <schreiber@rami.io> * Remove todo comment * Add documentation, Change endpoint name * Change related name * Remove unnecessary code block * Add program times to item API * Fix imports * Add log text * Use daterange helper * Add and update API tests * Add another API test * Add program times to cloning tests * Update query count because of program times query * Invalidate cached tickets on program time changes * Reduce invalidation calls * Update migration after rebase * Apply improvements to invalidation from review Co-authored-by: Richard Schreiber <schreiber@rami.io> * remove unneccessary attr=item param * remove unnecessary kwargs for formset_factory * fix local var name being overwritten in for-loop * fix empty formset being saved * Use subevent if available * make code less verbose * remove double event-label in ical desc * fix unnecessary var re-assign * fix ev vs p.subevent --------- Co-authored-by: Raphael Michel <michel@pretix.eu> Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -47,8 +47,9 @@ from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
|
||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
SalesChannel,
|
||||
)
|
||||
|
||||
|
||||
@@ -187,6 +188,12 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
'position', 'price_included', 'multi_allowed')
|
||||
|
||||
|
||||
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemProgramTime
|
||||
fields = ('start', 'end')
|
||||
|
||||
|
||||
class ItemBundleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemBundle
|
||||
@@ -212,6 +219,31 @@ class ItemBundleSerializer(serializers.ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class ItemProgramTimeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemProgramTime
|
||||
fields = ('id', 'start', 'end')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
start = full_data.get('start')
|
||||
if not start:
|
||||
raise ValidationError(_("The program start must not be empty."))
|
||||
|
||||
end = full_data.get('end')
|
||||
if not end:
|
||||
raise ValidationError(_("The program end must not be empty."))
|
||||
|
||||
if start > end:
|
||||
raise ValidationError(_("The program end must not be before the program start."))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ItemAddOnSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemAddOn
|
||||
@@ -250,6 +282,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True, required=False)
|
||||
bundles = InlineItemBundleSerializer(many=True, required=False)
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
@@ -271,7 +304,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'addons', 'bundles', 'program_times', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
|
||||
'issue_giftcard', 'meta_data',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
|
||||
@@ -294,9 +327,9 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
|
||||
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
|
||||
'dedicated nested endpoint.'))
|
||||
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data or 'program_times' in data):
|
||||
raise ValidationError(_('Updating add-ons, bundles, program times or variations via PATCH/PUT is not '
|
||||
'supported. Please use the dedicated nested endpoint.'))
|
||||
|
||||
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
|
||||
Item.clean_available(data.get('available_from'), data.get('available_until'))
|
||||
@@ -347,6 +380,13 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
|
||||
return value
|
||||
|
||||
def validate_program_times(self, value):
|
||||
if not self.instance:
|
||||
for program_time_data in value:
|
||||
ItemProgramTime.clean_start_end(self, start=program_time_data.get('start', None),
|
||||
end=program_time_data.get('end', None))
|
||||
return value
|
||||
|
||||
@cached_property
|
||||
def item_meta_properties(self):
|
||||
return {
|
||||
@@ -364,6 +404,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
|
||||
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
|
||||
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
|
||||
program_times_data = validated_data.pop('program_times') if 'program_times' in validated_data else {}
|
||||
meta_data = validated_data.pop('meta_data', None)
|
||||
picture = validated_data.pop('picture', None)
|
||||
require_membership_types = validated_data.pop('require_membership_types', [])
|
||||
@@ -398,6 +439,8 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
ItemAddOn.objects.create(base_item=item, **addon_data)
|
||||
for bundle_data in bundles_data:
|
||||
ItemBundle.objects.create(base_item=item, **bundle_data)
|
||||
for program_time_data in program_times_data:
|
||||
ItemProgramTime.objects.create(item=item, **program_time_data)
|
||||
|
||||
# Meta data
|
||||
if meta_data is not None:
|
||||
|
||||
@@ -112,6 +112,7 @@ item_router = routers.DefaultRouter()
|
||||
item_router.register(r'variations', item.ItemVariationViewSet)
|
||||
item_router.register(r'addons', item.ItemAddOnViewSet)
|
||||
item_router.register(r'bundles', item.ItemBundleViewSet)
|
||||
item_router.register(r'program_times', item.ItemProgramTimeViewSet)
|
||||
|
||||
order_router = routers.DefaultRouter()
|
||||
order_router.register(r'payments', order.PaymentViewSet)
|
||||
|
||||
@@ -46,13 +46,13 @@ from rest_framework.response import Response
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.item import (
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
|
||||
QuestionSerializer, QuotaSerializer,
|
||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
|
||||
Question, QuestionOption, Quota,
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
||||
ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
@@ -279,6 +279,57 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
class ItemProgramTimeViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemProgramTimeSerializer
|
||||
queryset = ItemProgramTime.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
|
||||
ordering_fields = ('id',)
|
||||
ordering = ('id',)
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
@cached_property
|
||||
def item(self):
|
||||
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.item.program_times.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
ctx['item'] = self.item
|
||||
return ctx
|
||||
|
||||
def perform_create(self, serializer):
|
||||
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
|
||||
serializer.save(item=item)
|
||||
item.log_action(
|
||||
'pretix.event.item.program_times.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.item.log_action(
|
||||
'pretix.event.item.program_times.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
super().perform_destroy(instance)
|
||||
instance.item.log_action(
|
||||
'pretix.event.item.program_times.removed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'start': instance.start, 'end': instance.end}
|
||||
)
|
||||
|
||||
|
||||
class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemAddOnSerializer
|
||||
queryset = ItemAddOn.objects.none()
|
||||
|
||||
Reference in New Issue
Block a user