mirror of
https://github.com/pretix/pretix.git
synced 2026-01-24 00:32:26 +00:00
Compare commits
7 Commits
fix-progra
...
webcheckin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f79d5032 | ||
|
|
35a7c38b31 | ||
|
|
815e31d9a0 | ||
|
|
ed618f2f32 | ||
|
|
a900e11ce0 | ||
|
|
112d5da792 | ||
|
|
ceb2e13d27 |
@@ -704,6 +704,16 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
||||
if 'answers.question' in self.context['expand']:
|
||||
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
|
||||
|
||||
if 'addons' in self.context['expand']:
|
||||
# Experimental feature, undocumented on purpose for now in case we need to remove it again
|
||||
# for performance reasons
|
||||
subl = CheckinListOrderPositionSerializer(read_only=True, many=True, context={
|
||||
**self.context,
|
||||
'expand': [v for v in self.context['expand'] if v != 'addons'],
|
||||
'pdf_data': False,
|
||||
})
|
||||
self.fields['addons'] = subl
|
||||
|
||||
|
||||
class OrderPaymentTypeField(serializers.Field):
|
||||
# TODO: Remove after pretix 2.2
|
||||
|
||||
@@ -381,15 +381,21 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
|
||||
qs = qs.filter(reduce(operator.or_, lists_qs))
|
||||
|
||||
prefetch_related = [
|
||||
Prefetch(
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
]
|
||||
select_related = [
|
||||
'item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat'
|
||||
]
|
||||
|
||||
if pdf_data:
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
|
||||
# Don't add to list, we don't want to propagate to addons
|
||||
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
|
||||
Prefetch(
|
||||
'event',
|
||||
@@ -404,32 +410,39 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
|
||||
)
|
||||
)
|
||||
))
|
||||
).select_related(
|
||||
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
|
||||
)
|
||||
else:
|
||||
qs = qs.prefetch_related(
|
||||
Prefetch(
|
||||
lookup='checkins',
|
||||
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
|
||||
),
|
||||
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
|
||||
'answers', 'answers__options', 'answers__question',
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
|
||||
|
||||
if expand and 'subevent' in expand:
|
||||
qs = qs.prefetch_related(
|
||||
prefetch_related += [
|
||||
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
|
||||
'subevent__seat_category_mappings', 'subevent__meta_values'
|
||||
)
|
||||
]
|
||||
|
||||
if expand and 'item' in expand:
|
||||
qs = qs.prefetch_related('item', 'item__addons', 'item__bundles', 'item__meta_values',
|
||||
'item__variations').select_related('item__tax_rule')
|
||||
prefetch_related += [
|
||||
'item', 'item__addons', 'item__bundles', 'item__meta_values',
|
||||
'item__variations',
|
||||
]
|
||||
select_related.append('item__tax_rule')
|
||||
|
||||
if expand and 'variation' in expand:
|
||||
qs = qs.prefetch_related('variation', 'variation__meta_values')
|
||||
prefetch_related += [
|
||||
'variation', 'variation__meta_values',
|
||||
]
|
||||
|
||||
if expand and 'addons' in expand:
|
||||
prefetch_related += [
|
||||
Prefetch('addons', OrderPosition.objects.prefetch_related(*prefetch_related).select_related(*select_related)),
|
||||
]
|
||||
else:
|
||||
prefetch_related += [
|
||||
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
|
||||
]
|
||||
|
||||
if pdf_data:
|
||||
select_related.remove("order") # Don't need it twice on this queryset
|
||||
|
||||
qs = qs.prefetch_related(*prefetch_related).select_related(*select_related)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
|
||||
'variations__meta_values', 'variations__meta_values__property',
|
||||
'require_membership_types', 'variations__require_membership_types',
|
||||
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
|
||||
'limit_sales_channels', 'variations__limit_sales_channels',
|
||||
).all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
|
||||
@@ -364,7 +364,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.state,
|
||||
order.invoice_address.state_for_address,
|
||||
order.invoice_address.custom_field,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
@@ -515,7 +515,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.state,
|
||||
order.invoice_address.state_for_address,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
@@ -732,7 +732,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
op.zipcode or '',
|
||||
op.city or '',
|
||||
op.country if op.country else '',
|
||||
op.state or '',
|
||||
op.state_for_address or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
op.secret,
|
||||
@@ -797,7 +797,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country if order.invoice_address.country else
|
||||
order.invoice_address.country_old,
|
||||
order.invoice_address.state,
|
||||
order.invoice_address.state_for_address,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
|
||||
@@ -349,7 +349,7 @@ class AttendeeProfile(models.Model):
|
||||
def state_name(self):
|
||||
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
|
||||
if sd:
|
||||
return sd.name
|
||||
return _(sd.name)
|
||||
return self.state
|
||||
|
||||
@property
|
||||
|
||||
@@ -594,10 +594,11 @@ class Item(LoggedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Only show after sellout of"),
|
||||
help_text=_("If you select a product here, this product will only be shown when that product is "
|
||||
"sold out. If combined with the option to hide sold-out products, this allows you to "
|
||||
"swap out products for more expensive ones once the cheaper option is sold out. There might "
|
||||
"be a short period in which both products are visible while all tickets of the referenced "
|
||||
"product are reserved, but not yet sold.")
|
||||
"no longer available. This will happen either because the other product has sold out or because "
|
||||
"the time is outside of the sales window for the other product. If combined with the option "
|
||||
"to hide sold-out products, this allows you to swap out products for more expensive ones once "
|
||||
"the cheaper option is sold out. There might be a short period in which both products are visible "
|
||||
"while all tickets of the referenced product are reserved, but not yet sold.")
|
||||
)
|
||||
hidden_if_item_available_mode = models.CharField(
|
||||
choices=UNAVAIL_MODES,
|
||||
|
||||
@@ -1675,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
def state_name(self):
|
||||
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
|
||||
if sd:
|
||||
return sd.name
|
||||
return _(sd.name)
|
||||
return self.state
|
||||
|
||||
@property
|
||||
@@ -3480,7 +3480,7 @@ class InvoiceAddress(models.Model):
|
||||
def state_name(self):
|
||||
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
|
||||
if sd:
|
||||
return sd.name
|
||||
return _(sd.name)
|
||||
return self.state
|
||||
|
||||
@property
|
||||
|
||||
@@ -159,6 +159,7 @@ class WaitingListEntry(LoggedModel):
|
||||
if availability[1] is None or availability[1] < 1:
|
||||
raise WaitingListException(_('This product is currently not available.'))
|
||||
|
||||
event = self.event
|
||||
ev = self.subevent or self.event
|
||||
if ev.seat_category_mappings.filter(product=self.item).exists():
|
||||
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
|
||||
@@ -191,6 +192,7 @@ class WaitingListEntry(LoggedModel):
|
||||
|
||||
with transaction.atomic():
|
||||
locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
|
||||
locked_wle.event = event
|
||||
if locked_wle.voucher:
|
||||
raise WaitingListException(_('A voucher has already been sent to this person.'))
|
||||
e = locked_wle.email
|
||||
@@ -227,6 +229,7 @@ class WaitingListEntry(LoggedModel):
|
||||
locked_wle.save()
|
||||
|
||||
self.refresh_from_db()
|
||||
self.event = event
|
||||
|
||||
with language(self.locale, self.event.settings.region):
|
||||
self.send_mail(
|
||||
|
||||
@@ -82,7 +82,7 @@ def _info(cc):
|
||||
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
|
||||
return {
|
||||
'data': [
|
||||
{'name': s.name, 'code': s.code[3:]}
|
||||
{'name': gettext(s.name), 'code': s.code[3:]}
|
||||
for s in sorted(statelist, key=lambda s: s.name)
|
||||
],
|
||||
**info,
|
||||
|
||||
@@ -363,7 +363,7 @@ class Forgot(TemplateView):
|
||||
else:
|
||||
messages.info(request, _('If the address is registered to valid account, then we have sent you an email containing further instructions.'))
|
||||
|
||||
return redirect('control:auth.forgot')
|
||||
return redirect('control:auth.forgot')
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<div class="details">
|
||||
<code>{{ checkResult.position.order }}-{{ checkResult.position.positionid }}</code>
|
||||
<h4>{{ checkResult.position.attendee_name }}</h4>
|
||||
<div v-if="checkResultAddons" class="addons">{{ checkResultAddons }}</div>
|
||||
<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>
|
||||
@@ -265,6 +266,16 @@ export default {
|
||||
const date = moment.utc(this.checkinlist.subevent.date_from).tz(this.$root.timezone).format(this.$root.datetime_format)
|
||||
return `${name} · ${date}`
|
||||
},
|
||||
checkResultAddons() {
|
||||
if (!this.checkResult) return ''
|
||||
if (!this.checkResult.position.addons) return ''
|
||||
return this.checkResult.position.addons.map((addon) => {
|
||||
if (addon.variation) {
|
||||
return `+ ${addon.item.internal_name || i18nstring_localize(addon.item.name)} – ${i18nstring_localize(addon.variation.value)}`
|
||||
}
|
||||
return "+ " + (addon.item.internal_name || i18nstring_localize(addon.item.name));
|
||||
}).join("\n")
|
||||
},
|
||||
checkResultSubevent() {
|
||||
if (!this.checkResult) return ''
|
||||
if (!this.checkResult.position.subevent) return ''
|
||||
@@ -369,7 +380,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&expand=answers.question'
|
||||
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation&expand=answers.question&expand=addons'
|
||||
if (untrusted) {
|
||||
url += '&untrusted_input=true'
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ a.searchresult, .check-result {
|
||||
word-break: break-word;
|
||||
color: $text-muted;
|
||||
}
|
||||
.addons {
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
|
||||
.check-result-status {
|
||||
|
||||
@@ -519,7 +519,7 @@
|
||||
<dialog role="alertdialog" id="cart-extend-confirmation-dialog" class="inline-dialog" aria-labelledby="cart-deadline">
|
||||
<form method="dialog">
|
||||
<p>
|
||||
<button class="btn btn-success" autofocus value="OK">
|
||||
<button class="btn btn-success" value="OK">
|
||||
<span role="img" aria-label="{% trans "OK" %}.">
|
||||
{% icon "check" %}
|
||||
</span>
|
||||
|
||||
@@ -311,7 +311,8 @@ def get_grouped_items(event, *, channel: SalesChannel, subevent=None, voucher=No
|
||||
)
|
||||
else:
|
||||
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
|
||||
item._dependency_available = q[0] == Quota.AVAILABILITY_OK
|
||||
time_available = item.hidden_if_item_available.is_available()
|
||||
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
|
||||
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
|
||||
item._remove = True
|
||||
continue
|
||||
|
||||
@@ -737,6 +737,19 @@ def test_question_expand(token_client, organizer, clist, event, order, question)
|
||||
assert resp.data["position"]["answers"][0]["question"]["question"]["en"] == "Size"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_addons_expand(token_client, organizer, clist, event, order, question, other_item):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].save()
|
||||
p.answers.create(question=question[0], answer="3")
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {"answers": {question[0].pk: ""}}, query="?expand=addons&expand=item")
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
assert resp.data["position"]["addons"][0]["item"]["id"] == other_item.pk
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_store_failed(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -50,7 +50,6 @@ from pretix.base.models import (
|
||||
QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.testutils.queries import assert_num_queries
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -423,13 +422,6 @@ def test_item_list(token_client, organizer, event, team, item):
|
||||
assert [] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_item_list_queries(token_client, organizer, event, team, item, item3):
|
||||
with assert_num_queries(18):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_item_detail(token_client, organizer, event, team, item):
|
||||
res = dict(TEST_ITEM_RES)
|
||||
|
||||
@@ -46,6 +46,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
from freezegun import freeze_time
|
||||
from tests.base import SoupTest
|
||||
from tests.testdummy.signals import FoobarSalesChannel
|
||||
|
||||
@@ -272,6 +273,48 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
|
||||
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertNotIn("Early-bird", resp.rendered_content)
|
||||
|
||||
def tiered_availability_by_date_and_quota(self, q1_size, q2_size, time_offset, expected_phase):
|
||||
current_time = now()
|
||||
|
||||
with scopes_disabled():
|
||||
q1 = Quota.objects.create(event=self.event, name='Phase 1', size=q1_size)
|
||||
item1 = Item.objects.create(
|
||||
event=self.event,
|
||||
name='Phase 1',
|
||||
default_price=0,
|
||||
available_from=current_time,
|
||||
available_until=current_time + datetime.timedelta(days=1),
|
||||
available_from_mode=Item.UNAVAIL_MODE_HIDDEN,
|
||||
available_until_mode=Item.UNAVAIL_MODE_HIDDEN,
|
||||
hidden_if_item_available_mode=Item.UNAVAIL_MODE_HIDDEN,
|
||||
)
|
||||
q1.items.add(item1)
|
||||
q2 = Quota.objects.create(event=self.event, name='Phase 2', size=q2_size)
|
||||
item2 = Item.objects.create(
|
||||
event=self.event,
|
||||
name='Phase 2',
|
||||
default_price=0,
|
||||
available_from=current_time + datetime.timedelta(days=0),
|
||||
available_until=current_time + datetime.timedelta(days=2),
|
||||
available_from_mode=Item.UNAVAIL_MODE_HIDDEN,
|
||||
available_until_mode=Item.UNAVAIL_MODE_HIDDEN,
|
||||
hidden_if_item_available_mode=Item.UNAVAIL_MODE_HIDDEN,
|
||||
hidden_if_item_available=item1
|
||||
)
|
||||
q2.items.add(item2)
|
||||
with freeze_time(current_time + time_offset):
|
||||
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||
self.assertIn(expected_phase, resp.rendered_content)
|
||||
|
||||
def test_tiered_availability_by_date_and_quota_phase1_available(self):
|
||||
self.tiered_availability_by_date_and_quota(1, 1, datetime.timedelta(seconds=1), "Phase 1")
|
||||
|
||||
def test_tiered_availability_by_date_and_quota_phase1_sold_out(self):
|
||||
self.tiered_availability_by_date_and_quota(0, 1, datetime.timedelta(seconds=1), "Phase 2")
|
||||
|
||||
def test_tiered_availability_by_date_and_quota_phase1_timed_out(self):
|
||||
self.tiered_availability_by_date_and_quota(1, 1, datetime.timedelta(days=1, hours=1), "Phase 2")
|
||||
|
||||
def test_subevents_inactive_unknown(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
|
||||
Reference in New Issue
Block a user