forked from CGM_Public/pretix_original
Check-in: Show more information (#3576)
* Check-in: Show more information * Add change notes * Rebase migration * Add "expand" option to checkinrpc * REmove accidental file * Docs fixes * REbase migration * Rebase migration * Fix typo * REbase migration * Make web-checkin look more like new android checkin
This commit is contained in:
@@ -61,7 +61,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'available_from', 'available_until',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -85,7 +85,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
|
||||
fields = ('id', 'value', 'active', 'description',
|
||||
'position', 'default_price', 'price', 'original_price', 'free_price_suggestion', 'require_approval',
|
||||
'require_membership', 'require_membership_types', 'require_membership_hidden',
|
||||
'checkin_attention', 'available_from', 'available_until',
|
||||
'checkin_attention', 'checkin_text', 'available_from', 'available_until',
|
||||
'sales_channels', 'hide_without_voucher', 'meta_data')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -237,7 +237,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'default_price', 'free_price', 'free_price_suggestion', 'tax_rate', 'tax_rule', 'admission',
|
||||
'personalized', 'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
|
||||
'issue_giftcard', 'meta_data',
|
||||
@@ -440,7 +440,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('id', 'question', 'type', 'required', 'items', 'options', 'position',
|
||||
'ask_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_file_portrait')
|
||||
@@ -486,6 +486,9 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
Question.clean_items(event, full_data.get('items'))
|
||||
return data
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.item import (
|
||||
InlineItemVariationSerializer, ItemSerializer,
|
||||
InlineItemVariationSerializer, ItemSerializer, QuestionSerializer,
|
||||
)
|
||||
from pretix.base.channels import get_all_sales_channels
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -585,6 +585,9 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
if 'variation' in self.context['expand']:
|
||||
self.fields['variation'] = InlineItemVariationSerializer(read_only=True)
|
||||
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
|
||||
|
||||
class OrderPaymentTypeField(serializers.Field):
|
||||
# TODO: Remove after pretix 2.2
|
||||
@@ -715,7 +718,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
fields = (
|
||||
'code', 'event', 'status', 'testmode', 'secret', 'email', 'phone', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'custom_followup_at', 'invoice_address', 'positions', 'downloads',
|
||||
'checkin_attention', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'checkin_attention', 'checkin_text', 'last_modified', 'payments', 'refunds', 'require_approval', 'sales_channel',
|
||||
'url', 'customer', 'valid_if_pending'
|
||||
)
|
||||
read_only_fields = (
|
||||
@@ -771,8 +774,8 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
|
||||
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'email', 'locale', 'phone',
|
||||
'valid_if_pending']
|
||||
update_fields = ['comment', 'custom_followup_at', 'checkin_attention', 'checkin_text', 'email', 'locale',
|
||||
'phone', 'valid_if_pending']
|
||||
|
||||
if 'invoice_address' in validated_data:
|
||||
iadata = validated_data.pop('invoice_address')
|
||||
@@ -1036,9 +1039,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'testmode', 'email', 'phone', 'locale', 'payment_provider', 'fees', 'comment', 'sales_channel',
|
||||
'invoice_address', 'positions', 'checkin_attention', 'payment_info', 'payment_date', 'consume_carts',
|
||||
'force', 'send_email', 'simulate', 'customer', 'custom_followup_at', 'require_approval',
|
||||
'valid_if_pending')
|
||||
'invoice_address', 'positions', 'checkin_attention', 'checkin_text', 'payment_info', 'payment_date',
|
||||
'consume_carts', 'force', 'send_email', 'simulate', 'customer', 'custom_followup_at',
|
||||
'require_approval', 'valid_if_pending')
|
||||
|
||||
def validate_payment_provider(self, pp):
|
||||
if pp is None:
|
||||
|
||||
@@ -536,6 +536,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_ALREADY_REDEEMED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'__warning': 'Compatibility hack active due to detected old pretixSCAN version',
|
||||
}, status=400)
|
||||
except: # we don't care e.g. about invalid version numbers
|
||||
@@ -547,6 +548,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_INVALID,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
@@ -576,6 +578,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_REVOKED,
|
||||
'reason_explanation': None,
|
||||
'require_attention': False,
|
||||
'checkin_texts': [],
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, revoked_matches[
|
||||
0].event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
@@ -631,6 +634,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
@@ -679,6 +683,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
return Response({
|
||||
'status': 'incomplete',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'questions': [
|
||||
QuestionSerializer(q).data for q in e.questions
|
||||
@@ -709,6 +714,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'reason': e.code,
|
||||
'reason_explanation': e.reason,
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
@@ -716,6 +722,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
return Response({
|
||||
'status': 'ok',
|
||||
'require_attention': op.require_checkin_attention,
|
||||
'checkin_texts': op.checkin_texts,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=_make_context(context, op.order.event)).data,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=201)
|
||||
|
||||
@@ -827,6 +827,16 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
}
|
||||
)
|
||||
|
||||
if 'checkin_text' in self.request.data and serializer.instance.checkin_text != self.request.data.get('checkin_text'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.checkin_text',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'new_value': self.request.data.get('checkin_text')
|
||||
}
|
||||
)
|
||||
|
||||
if 'valid_if_pending' in self.request.data and serializer.instance.valid_if_pending != self.request.data.get('valid_if_pending'):
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.order.valid_if_pending',
|
||||
|
||||
@@ -88,6 +88,7 @@ class ItemDataExporter(ListExporter):
|
||||
_("Minimum amount per order"),
|
||||
_("Maximum amount per order"),
|
||||
_("Requires special attention"),
|
||||
_("Check-in text"),
|
||||
_("Original price"),
|
||||
_("This product is a gift card"),
|
||||
_("Require a valid membership"),
|
||||
@@ -162,6 +163,7 @@ class ItemDataExporter(ListExporter):
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.checkin_text or "",
|
||||
v.original_price or i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership or v.require_membership else "",
|
||||
@@ -206,6 +208,7 @@ class ItemDataExporter(ListExporter):
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.checkin_text or "",
|
||||
i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership else "",
|
||||
|
||||
@@ -96,6 +96,7 @@ class JSONExporter(BaseExporter):
|
||||
'min_per_order': item.min_per_order,
|
||||
'max_per_order': item.max_per_order,
|
||||
'checkin_attention': item.checkin_attention,
|
||||
'checkin_text': item.checkin_text,
|
||||
'original_price': item.original_price,
|
||||
'issue_giftcard': item.issue_giftcard,
|
||||
'meta_data': item.meta_data,
|
||||
@@ -110,6 +111,7 @@ class JSONExporter(BaseExporter):
|
||||
'description': str(variation.description),
|
||||
'position': variation.position,
|
||||
'checkin_attention': variation.checkin_attention,
|
||||
'checkin_text': variation.checkin_text,
|
||||
'require_approval': variation.require_approval,
|
||||
'require_membership': variation.require_membership,
|
||||
'sales_channels': variation.sales_channels,
|
||||
@@ -164,6 +166,7 @@ class JSONExporter(BaseExporter):
|
||||
'custom_followup_at': order.custom_followup_at,
|
||||
'require_approval': order.require_approval,
|
||||
'checkin_attention': order.checkin_attention,
|
||||
'checkin_text': order.checkin_text,
|
||||
'sales_channel': order.sales_channel,
|
||||
'expires': order.expires,
|
||||
'datetime': order.datetime,
|
||||
|
||||
@@ -275,6 +275,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
headers.append(_('Invoice numbers'))
|
||||
headers.append(_('Sales channel'))
|
||||
headers.append(_('Requires special attention'))
|
||||
headers.append(_('Check-in text'))
|
||||
headers.append(_('Comment'))
|
||||
headers.append(_('Follow-up date'))
|
||||
headers.append(_('Positions'))
|
||||
@@ -384,6 +385,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
row.append(order.invoice_numbers)
|
||||
row.append(order.sales_channel)
|
||||
row.append(_('Yes') if order.checkin_attention else _('No'))
|
||||
row.append(order.checkin_text or "")
|
||||
row.append(order.comment or "")
|
||||
row.append(order.custom_followup_at.strftime("%Y-%m-%d") if order.custom_followup_at else "")
|
||||
row.append(order.pcnt)
|
||||
|
||||
32
src/pretix/base/migrations/0253_checkin_info.py
Normal file
32
src/pretix/base/migrations/0253_checkin_info.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.4 on 2023-09-06 09:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pretixbase", "0252_logentry_organizer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="item",
|
||||
name="checkin_text",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="itemvariation",
|
||||
name="checkin_text",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="order",
|
||||
name="checkin_text",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="question",
|
||||
name="show_during_checkin",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -336,6 +336,8 @@ class Item(LoggedModel):
|
||||
:type min_per_order: int
|
||||
:param checkin_attention: Requires special attention at check-in
|
||||
:type checkin_attention: bool
|
||||
:param checkin_text: Additional text to show at check-in
|
||||
:type checkin_text: bool
|
||||
:param original_price: The item's "original" price. Will not be used for any calculations, will just be shown.
|
||||
:type original_price: decimal.Decimal
|
||||
:param require_approval: If set to ``True``, orders containing this product can only be processed and paid after approved by an administrator
|
||||
@@ -566,6 +568,11 @@ class Item(LoggedModel):
|
||||
'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.')
|
||||
)
|
||||
checkin_text = models.TextField(
|
||||
verbose_name=_('Check-in text'),
|
||||
null=True, blank=True,
|
||||
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
|
||||
)
|
||||
original_price = models.DecimalField(
|
||||
verbose_name=_('Original price'),
|
||||
blank=True, null=True,
|
||||
@@ -1096,6 +1103,11 @@ class ItemVariation(models.Model):
|
||||
'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.')
|
||||
)
|
||||
checkin_text = models.TextField(
|
||||
verbose_name=_('Check-in text'),
|
||||
null=True, blank=True,
|
||||
help_text=_('This text will be shown by the check-in app if a ticket of this type is scanned.')
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='item__event__organizer')
|
||||
|
||||
@@ -1450,6 +1462,8 @@ class Question(LoggedModel):
|
||||
: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 show_during_checkin: Whether to show the answer to this question during check-in.
|
||||
:type show_during_checkin: bool
|
||||
:param hidden: Whether to only show the question in the backend
|
||||
:type hidden: bool
|
||||
:param identifier: An arbitrary, internal identifier
|
||||
@@ -1487,6 +1501,7 @@ class Question(LoggedModel):
|
||||
)
|
||||
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
|
||||
ASK_DURING_CHECKIN_UNSUPPORTED = []
|
||||
SHOW_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE]
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
@@ -1538,6 +1553,11 @@ class Question(LoggedModel):
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
show_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Show answer during check-in'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField(
|
||||
verbose_name=_('Hidden question'),
|
||||
help_text=_('This question will only show up in the backend.'),
|
||||
|
||||
@@ -244,6 +244,11 @@ class Order(LockModel, LoggedModel):
|
||||
'special attention. This will not show any details or custom message, so you need to brief your '
|
||||
'check-in staff how to handle these cases.')
|
||||
)
|
||||
checkin_text = models.TextField(
|
||||
verbose_name=_('Check-in text'),
|
||||
null=True, blank=True,
|
||||
help_text=_('This text will be shown by the check-in app if a ticket of this order is scanned.')
|
||||
)
|
||||
expiry_reminder_sent = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
@@ -2425,6 +2430,17 @@ class OrderPosition(AbstractPosition):
|
||||
return True
|
||||
return False
|
||||
|
||||
@cached_property
|
||||
def checkin_texts(self):
|
||||
texts = []
|
||||
if self.order.checkin_text:
|
||||
texts.append(self.order.checkin_text)
|
||||
if self.variation_id and self.variation.checkin_text:
|
||||
texts.append(self.variation.checkin_text)
|
||||
if self.item.checkin_text:
|
||||
texts.append(self.item.checkin_text)
|
||||
return texts
|
||||
|
||||
@property
|
||||
def checkins(self):
|
||||
"""
|
||||
|
||||
@@ -133,6 +133,14 @@ class QuestionForm(I18nModelForm):
|
||||
|
||||
return val
|
||||
|
||||
def clean_show_during_checkin(self):
|
||||
val = self.cleaned_data.get('show_during_checkin')
|
||||
|
||||
if val and self.cleaned_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
return val
|
||||
|
||||
def clean_identifier(self):
|
||||
val = self.cleaned_data.get('identifier')
|
||||
Question._clean_identifier(self.instance.event, val, self.instance)
|
||||
@@ -155,6 +163,7 @@ class QuestionForm(I18nModelForm):
|
||||
'type',
|
||||
'required',
|
||||
'ask_during_checkin',
|
||||
'show_during_checkin',
|
||||
'hidden',
|
||||
'identifier',
|
||||
'items',
|
||||
@@ -379,6 +388,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
'max_per_order',
|
||||
'generate_tickets',
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'free_price',
|
||||
'original_price',
|
||||
'sales_channels',
|
||||
@@ -703,6 +713,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'max_per_order',
|
||||
'min_per_order',
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'generate_tickets',
|
||||
'original_price',
|
||||
'require_bundling',
|
||||
@@ -751,6 +762,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'show_quota_left': ShowQuotaNullBooleanSelect(),
|
||||
'max_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
|
||||
'min_per_order': forms.widgets.NumberInput(attrs={'min': 0}),
|
||||
'checkin_text': forms.TextInput(),
|
||||
}
|
||||
|
||||
|
||||
@@ -869,6 +881,7 @@ class ItemVariationForm(I18nModelForm):
|
||||
'require_membership_hidden',
|
||||
'require_membership_types',
|
||||
'checkin_attention',
|
||||
'checkin_text',
|
||||
'available_from',
|
||||
'available_until',
|
||||
'sales_channels',
|
||||
@@ -884,6 +897,7 @@ class ItemVariationForm(I18nModelForm):
|
||||
'require_membership_types': forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice'
|
||||
}),
|
||||
'checkin_text': forms.TextInput(),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -265,12 +265,13 @@ class ExporterForm(forms.Form):
|
||||
class CommentForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['comment', 'checkin_attention', 'custom_followup_at']
|
||||
fields = ['comment', 'checkin_attention', 'checkin_text', 'custom_followup_at']
|
||||
widgets = {
|
||||
'comment': forms.Textarea(attrs={
|
||||
'rows': 3,
|
||||
'class': 'helper-width-100',
|
||||
}),
|
||||
'checkin_text': forms.TextInput(),
|
||||
'custom_followup_at': DatePickerWidget(),
|
||||
}
|
||||
|
||||
|
||||
@@ -407,6 +407,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.custom_followup_at': _('The order\'s follow-up date has been updated.'),
|
||||
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
|
||||
'toggled.'),
|
||||
'pretix.event.order.checkin_text': _('The order\'s check-in text has been changed.'),
|
||||
'pretix.event.order.pretix.event.order.valid_if_pending': _('The order\'s flag to be considered valid even if '
|
||||
'unpaid has been toggled.'),
|
||||
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
|
||||
|
||||
@@ -85,11 +85,31 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if result.position %}
|
||||
{% if result.position.require_attention %}
|
||||
{% if result.require_attention %}
|
||||
<p>
|
||||
<span class="fa fa-info-circle fa-fw"></span> {% trans "Special attention required" %}
|
||||
<strong>
|
||||
<span class="fa fa-info-circle text-info fa-fw"></span>
|
||||
{% trans "Special attention required" %}
|
||||
</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% for t in result.checkin_texts %}
|
||||
<p>
|
||||
<span class="fa fa-info-circle text-muted fa-fw"></span>
|
||||
{{ t }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% if result.position_object %}
|
||||
{% for a in result.position_object.answers.all %}
|
||||
{% if a.question.show_during_checkin %}
|
||||
<p>
|
||||
<span class="fa fa-question-circle text-muted fa-fw"></span>
|
||||
<strong>{{ a.question.question }}</strong>
|
||||
{{ a }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<p>
|
||||
<span class="fa fa-ticket fa-fw"></span>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=result.position.order %}">
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field form.checkin_attention layout="control" %}
|
||||
{% bootstrap_field form.checkin_text layout="control" %}
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
@@ -206,6 +207,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_field formset.empty_form.checkin_attention layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.checkin_text layout="control" %}
|
||||
</div>
|
||||
</details>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -202,6 +202,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in & Validity" %}</legend>
|
||||
{% bootstrap_field form.checkin_attention layout="control" %}
|
||||
{% bootstrap_field form.checkin_text layout="control" %}
|
||||
{% bootstrap_field form.validity_mode layout="control" %}
|
||||
<div data-display-dependency="#{{ form.validity_mode.id_for_label }}" data-display-dependency-value="fixed">
|
||||
{% bootstrap_field form.validity_fixed_from layout="control" %}
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
|
||||
|
||||
@@ -997,6 +997,7 @@
|
||||
{% bootstrap_field comment_form.comment show_help=True show_label=False %}
|
||||
{% bootstrap_field comment_form.custom_followup_at %}
|
||||
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
|
||||
{% bootstrap_field comment_form.checkin_text show_help=True show_label=False %}
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
|
||||
@@ -551,9 +551,12 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
|
||||
gate=form.cleaned_data.get("gate"),
|
||||
).data
|
||||
|
||||
if self.result.get("position"):
|
||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||
self.result["position_object"] = op
|
||||
|
||||
if form.cleaned_data["checkin_type"] == Checkin.TYPE_ENTRY and self.list.rules and self.result.get("position")\
|
||||
and (self.result["status"] in ("ok", "incomplete") or self.result["reason"] == "rules"):
|
||||
op = OrderPosition.objects.get(pk=self.result["position"]["id"])
|
||||
rule_data = LazyRuleVars(op, self.list, form.cleaned_data["datetime"], form.cleaned_data.get("gate"))
|
||||
rule_graph = _logic_annotate_for_graphic_explain(self.list.rules, op.subevent or self.list.event, rule_data,
|
||||
form.cleaned_data["datetime"])
|
||||
|
||||
@@ -513,7 +513,8 @@ class OrderDetail(OrderView):
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
'custom_followup_at': self.order.custom_followup_at,
|
||||
'checkin_attention': self.order.checkin_attention
|
||||
'checkin_attention': self.order.checkin_attention,
|
||||
'checkin_text': self.order.checkin_text,
|
||||
})
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
|
||||
|
||||
@@ -747,7 +748,13 @@ class OrderComment(OrderView):
|
||||
self.order.log_action('pretix.event.order.checkin_attention', user=self.request.user, data={
|
||||
'new_value': form.cleaned_data.get('checkin_attention')
|
||||
})
|
||||
self.order.save(update_fields=['checkin_attention', 'comment', 'custom_followup_at'])
|
||||
|
||||
if form.cleaned_data.get('checkin_text') != self.order.checkin_text:
|
||||
self.order.checkin_text = form.cleaned_data.get('checkin_text')
|
||||
self.order.log_action('pretix.event.order.checkin_text', user=self.request.user, data={
|
||||
'new_value': form.cleaned_data.get('checkin_text')
|
||||
})
|
||||
self.order.save(update_fields=['checkin_attention', 'checkin_text', 'comment', 'custom_followup_at'])
|
||||
self.order.refresh_from_db()
|
||||
messages.success(self.request, _('The comment has been updated.'))
|
||||
else:
|
||||
|
||||
@@ -25,22 +25,32 @@
|
||||
{{ checkError }}
|
||||
</div>
|
||||
<div :class="'check-result-status check-result-' + checkResultColor">
|
||||
{{ checkResultText }}
|
||||
</div>
|
||||
<div class="panel-body" v-if="checkResult.position">
|
||||
<div class="details">
|
||||
<h4>{{ checkResult.position.order }}-{{ checkResult.position.positionid }} {{ checkResult.position.attendee_name }}</h4>
|
||||
<strong v-if="checkResult.reason_explanation">{{ checkResult.reason_explanation }}<br></strong>
|
||||
<span>{{ checkResultItemvar }}</span><br>
|
||||
<span v-if="checkResultSubevent">{{ checkResultSubevent }}<br></span>
|
||||
<span class="secret">{{ checkResult.position.secret }}</span>
|
||||
<span v-if="checkResult.position.seat"><br>{{ checkResult.position.seat.name }}</span>
|
||||
</div>
|
||||
<div class="check-result-text">{{ checkResultText }}</div>
|
||||
<div class="check-result-item">{{ checkResultItemvar }}</div>
|
||||
<div class="check-result-reason" v-if="checkResult.reason_explanation">{{ checkResult.reason_explanation }}</div>
|
||||
|
||||
</div>
|
||||
<div class="attention" v-if="checkResult && checkResult.require_attention">
|
||||
<span class="fa fa-warning"></span>
|
||||
{{ $root.strings['check.attention'] }}
|
||||
</div>
|
||||
<div class="panel-body" v-if="checkResult.position">
|
||||
<div class="details">
|
||||
<code>{{ checkResult.position.order }}-{{ checkResult.position.positionid }}</code>
|
||||
<h4>{{ checkResult.position.attendee_name }}</h4>
|
||||
<span v-if="checkResultSubevent">{{ checkResultSubevent }}<br></span>
|
||||
<span class="secret">{{ checkResult.position.secret }}</span>
|
||||
<span v-if="checkResult.position.seat"><br>{{ checkResult.position.seat.name }}</span>
|
||||
<span v-for="a in checkResult.position.answers">
|
||||
<span v-if="a.question.show_during_checkin">
|
||||
<br>
|
||||
<strong>{{ a.question.question | i18nstring_localize }}:</strong>
|
||||
{{ a.answer | answer(a.question, $root.timezone, $root.strings) }}
|
||||
</span>
|
||||
</span>
|
||||
<strong v-for="t in checkResult.checkin_texts"><br>{{ t }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="searchResults !== null" class="panel panel-primary search-results">
|
||||
@@ -296,6 +306,24 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
i18nstring_localize (value) {
|
||||
return i18nstring_localize(value);
|
||||
},
|
||||
answer (value, question, timezone, strings) {
|
||||
if (question.type === "B" && value === "True") {
|
||||
return strings['yes']
|
||||
} else if (question.type === "B" && value === "False") {
|
||||
return strings['no']
|
||||
} else if (question.type === "W" && !!value) {
|
||||
return moment(value).tz(timezone).format("L LT")
|
||||
} else if (question.type === "D" && !!value) {
|
||||
return moment(value).format("L")
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectResult(res) {
|
||||
this.check(res.id, false, false, false, false)
|
||||
@@ -341,7 +369,7 @@ export default {
|
||||
this.$refs.input.blur()
|
||||
})
|
||||
|
||||
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation'
|
||||
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation&expand=answers.question'
|
||||
if (untrusted) {
|
||||
url += '&untrusted_input=true'
|
||||
}
|
||||
@@ -399,7 +427,7 @@ export default {
|
||||
} else if (data.status === 'error' && data.reason === 'invalid' && fallbackToSearch) {
|
||||
this.startSearch(false)
|
||||
} else {
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 20)
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 30)
|
||||
this.fetchStatus()
|
||||
}
|
||||
})
|
||||
@@ -407,7 +435,7 @@ export default {
|
||||
this.checkLoading = false
|
||||
this.checkResult = {}
|
||||
this.checkError = reason.toString()
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 20)
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 30)
|
||||
})
|
||||
},
|
||||
globalKeydown(e) {
|
||||
@@ -487,13 +515,13 @@ export default {
|
||||
} else {
|
||||
this.searchError = data
|
||||
}
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 20)
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 30)
|
||||
})
|
||||
.catch(reason => {
|
||||
this.searchLoading = false
|
||||
this.searchResults = []
|
||||
this.searchError = reason
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 20)
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 30)
|
||||
})
|
||||
},
|
||||
searchNext() {
|
||||
@@ -510,12 +538,12 @@ export default {
|
||||
} else {
|
||||
this.searchError = data
|
||||
}
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 20)
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 30)
|
||||
})
|
||||
.catch(reason => {
|
||||
this.searchLoading = false
|
||||
this.searchError = reason
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 20)
|
||||
this.clearTimeout = window.setTimeout(this.clear, 1000 * 30)
|
||||
})
|
||||
},
|
||||
switchType() {
|
||||
|
||||
@@ -66,6 +66,8 @@ window.vapp = new Vue({
|
||||
'status.checkin': gettext('Checked-in Tickets'),
|
||||
'status.position': gettext('Valid Tickets'),
|
||||
'status.inside': gettext('Currently inside'),
|
||||
'yes': gettext('Yes'),
|
||||
'no': gettext('No'),
|
||||
},
|
||||
event_name: document.querySelector('#app').attributes['data-event-name'].value,
|
||||
timezone: document.body.attributes['data-timezone'].value,
|
||||
|
||||
@@ -96,11 +96,9 @@ a.searchresult, .check-result {
|
||||
.check-result-status {
|
||||
height: 30vh;
|
||||
max-height: 200px;
|
||||
font-size: 35px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
@@ -117,6 +115,21 @@ a.searchresult, .check-result {
|
||||
&.check-result-purple {
|
||||
background: $brand-primary;
|
||||
}
|
||||
.check-result-text {
|
||||
font-size: 35px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.check-result-item {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
.check-result .panel-body code {
|
||||
float: right;
|
||||
background: none;
|
||||
font-size: 18px;
|
||||
line-height: 1.1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.attention {
|
||||
@@ -137,7 +150,7 @@ a.searchresult, .check-result {
|
||||
|
||||
@-webkit-keyframes blinking {
|
||||
0%, 49% {
|
||||
background-color: $brand-primary;
|
||||
background-color: $brand-info;
|
||||
color: white;
|
||||
}
|
||||
50%, 100% {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
data-pretixlocale="{{ request.LANGUAGE_CODE }}" data-timezone="{{ request.event.settings.timezone }}">
|
||||
<div
|
||||
data-api-lists="{% url "api-v1:checkinlist-list" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
data-api-questions="{% url "api-v1:question-list" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
data-event-name="{{ request.event.name }}"
|
||||
id="app"></div>
|
||||
{% compress js %}
|
||||
|
||||
Reference in New Issue
Block a user