mirror of
https://github.com/pretix/pretix.git
synced 2026-05-20 17:44:02 +00:00
Compare commits
2 Commits
order-chan
...
apidoc-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93eef9ce7e | ||
|
|
803d0b1570 |
@@ -208,20 +208,6 @@ Additionally, when creating a device through the user interface or API, a user c
|
||||
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
||||
policies for official pretix apps like pretixSCAN and pretixPOS.
|
||||
|
||||
Removing a device
|
||||
-----------------
|
||||
|
||||
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
|
||||
invalidate your API key. There is no way to reverse this operation.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/device/revoke HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||
|
||||
This can also be done by the user through the web interface.
|
||||
|
||||
Event selection
|
||||
---------------
|
||||
|
||||
|
||||
@@ -2094,43 +2094,6 @@ class OrderChangeManager:
|
||||
)
|
||||
item_counts[item] += 1
|
||||
|
||||
# Detect removed add-ons and create RemoveOperations
|
||||
for cp, al in list(current_addons.items()):
|
||||
for k, v in al.items():
|
||||
input_num = input_addons[cp.id].get(k, 0)
|
||||
current_num = len(current_addons[cp].get(k, []))
|
||||
if input_num < current_num:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
is_unavailable = (
|
||||
# If an item is no longer available due to time, it should usually also be no longer
|
||||
# user-removable, because e.g. the stock has already been ordered.
|
||||
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
|
||||
# not mean it should be unremovable for others.
|
||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||
# be part of the input.
|
||||
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
||||
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
|
||||
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
||||
or (
|
||||
not a.item.all_sales_channels and
|
||||
not a.item.limit_sales_channels.contains(self.order.sales_channel)
|
||||
)
|
||||
)
|
||||
if is_unavailable:
|
||||
# "Re-select" add-on
|
||||
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
|
||||
continue
|
||||
if a.checkins.filter(list__consider_tickets_used=True).exists():
|
||||
raise OrderError(
|
||||
error_messages['addon_already_checked_in'] % {
|
||||
'addon': str(a.item.name),
|
||||
}
|
||||
)
|
||||
self.cancel(a)
|
||||
item_counts[a.item] -= 1
|
||||
|
||||
# Check constraints on the add-on combinations
|
||||
for op in toplevel_op:
|
||||
item = op.item
|
||||
@@ -2163,6 +2126,41 @@ class OrderChangeManager:
|
||||
}
|
||||
)
|
||||
|
||||
# Detect removed add-ons and create RemoveOperations
|
||||
for cp, al in list(current_addons.items()):
|
||||
for k, v in al.items():
|
||||
input_num = input_addons[cp.id].get(k, 0)
|
||||
current_num = len(current_addons[cp].get(k, []))
|
||||
if input_num < current_num:
|
||||
for a in current_addons[cp][k][:current_num - input_num]:
|
||||
if a.canceled:
|
||||
continue
|
||||
is_unavailable = (
|
||||
# If an item is no longer available due to time, it should usually also be no longer
|
||||
# user-removable, because e.g. the stock has already been ordered.
|
||||
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
|
||||
# not mean it should be unremovable for others.
|
||||
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||
# be part of the input.
|
||||
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
||||
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
|
||||
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
||||
or (
|
||||
not item.all_sales_channels and
|
||||
not item.limit_sales_channels.contains(self.order.sales_channel)
|
||||
)
|
||||
)
|
||||
if is_unavailable:
|
||||
continue
|
||||
if a.checkins.filter(list__consider_tickets_used=True).exists():
|
||||
raise OrderError(
|
||||
error_messages['addon_already_checked_in'] % {
|
||||
'addon': str(a.item.name),
|
||||
}
|
||||
)
|
||||
self.cancel(a)
|
||||
item_counts[a.item] -= 1
|
||||
|
||||
for item, count in item_counts.items():
|
||||
if count == 0:
|
||||
continue
|
||||
|
||||
@@ -372,19 +372,6 @@
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if c.items_missing %}
|
||||
<div class="product-appendix-row">
|
||||
<p class="text-muted">
|
||||
{% trans "There are products selected in this add-on category that currently cannot be changed because they are not on sale:" %}
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
{% for itemvar, count in c.items_missing.items %}
|
||||
<li>{{ count }}x {{ itemvar.0 }}{% if itemvar.1 %} – {{ itemvar.1 }}{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
|
||||
@@ -40,7 +40,7 @@ import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from collections import OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote
|
||||
|
||||
@@ -1431,13 +1431,11 @@ class OrderChangeMixin:
|
||||
'categories': []
|
||||
}
|
||||
current_addon_products = defaultdict(list)
|
||||
current_addon_products_missing = Counter()
|
||||
for a in p.addons.all():
|
||||
if a.canceled:
|
||||
continue
|
||||
if not a.is_bundled:
|
||||
current_addon_products[a.item_id, a.variation_id].append(a)
|
||||
current_addon_products_missing[a.item, a.variation] += 1
|
||||
|
||||
for iao in p.item.addons.all():
|
||||
ckey = '{}-{}'.format(p.subevent.pk if p.subevent else 0, iao.addon_category.pk)
|
||||
@@ -1475,7 +1473,6 @@ class OrderChangeMixin:
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
v.initial = len(current_addon_products[i.pk, v.pk])
|
||||
current_addon_products_missing[i, v] = 0
|
||||
if v.initial and i.free_price:
|
||||
a = current_addon_products[i.pk, v.pk][0]
|
||||
v.initial_price = TaxedPrice(
|
||||
@@ -1491,7 +1488,6 @@ class OrderChangeMixin:
|
||||
i.expand = any(v.initial for v in i.available_variations)
|
||||
else:
|
||||
i.initial = len(current_addon_products[i.pk, None])
|
||||
current_addon_products_missing[i, None] = 0
|
||||
if i.initial and i.free_price:
|
||||
a = current_addon_products[i.pk, None][0]
|
||||
i.initial_price = TaxedPrice(
|
||||
@@ -1513,8 +1509,7 @@ class OrderChangeMixin:
|
||||
'min_count': iao.min_count,
|
||||
'max_count': iao.max_count,
|
||||
'iao': iao,
|
||||
'items': [i for i in items if not i.require_voucher],
|
||||
'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
|
||||
'items': [i for i in items if not i.require_voucher]
|
||||
})
|
||||
|
||||
return positions
|
||||
|
||||
@@ -301,15 +301,14 @@ Vue.component('availbox', {
|
||||
return this.avail[0] < 100 && this.$root.waiting_list_enabled && this.item.allow_waitinglist;
|
||||
},
|
||||
waiting_list_url: function () {
|
||||
var u
|
||||
var u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?locale=' + lang + '&item=' + this.item.id
|
||||
if (this.item.has_variations) {
|
||||
u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter;
|
||||
} else {
|
||||
u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter;
|
||||
u += '&var=' + this.variation.id
|
||||
}
|
||||
if (this.$root.subevent) {
|
||||
u += '&subevent=' + this.$root.subevent
|
||||
}
|
||||
u += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter
|
||||
return u
|
||||
}
|
||||
},
|
||||
|
||||
@@ -99,15 +99,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.product-appendix-row {
|
||||
border-top: 1px solid $table-border-color;
|
||||
border-bottom: 1px solid $table-border-color;
|
||||
padding: 1.25*$line-height-computed 0;
|
||||
& > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
article.item-with-variations {
|
||||
margin: 0 -15px;
|
||||
padding: 0 15px;
|
||||
|
||||
@@ -1015,8 +1015,7 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert '<li>1x Workshop 1</li>' in response.content.decode()
|
||||
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
|
||||
assert 'Workshop 1' not in response.content.decode()
|
||||
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
@@ -1036,39 +1035,6 @@ class OrderChangeAddonsTest(BaseOrdersTest):
|
||||
with scopes_disabled():
|
||||
assert self.ticket_pos.addons.count() == 2
|
||||
|
||||
def test_do_not_overbook_unavailable_on_adding(self):
|
||||
self.iao.max_count = 1
|
||||
self.iao.save()
|
||||
self.workshop1.available_until = now() - datetime.timedelta(days=1)
|
||||
self.workshop1.save()
|
||||
with scopes_disabled():
|
||||
OrderPosition.objects.create(
|
||||
order=self.order,
|
||||
item=self.workshop1,
|
||||
variation=None,
|
||||
price=Decimal("12"),
|
||||
addon_to=self.ticket_pos,
|
||||
attendee_name_parts={'full_name': "Peter"}
|
||||
)
|
||||
self.order.total += Decimal("12")
|
||||
self.order.save()
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert '<li>1x Workshop 1</li>' in response.content.decode()
|
||||
assert f'cp_{self.ticket_pos.pk}_item_{self.workshop1.pk}' not in response.content.decode()
|
||||
|
||||
response = self.client.post(
|
||||
'/%s/%s/order/%s/%s/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
{
|
||||
f'cp_{self.ticket_pos.pk}_variation_{self.workshop2.pk}_{self.workshop2a.pk}': '1'
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert 'alert-danger' in response.content.decode()
|
||||
|
||||
def test_remove_addon_checked_in(self):
|
||||
with scopes_disabled():
|
||||
self.event.settings.change_allow_user_if_checked_in = True
|
||||
|
||||
Reference in New Issue
Block a user