diff --git a/doc/api/resources/checkin.rst b/doc/api/resources/checkin.rst
index 89ab1cff11..479fa28a37 100644
--- a/doc/api/resources/checkin.rst
+++ b/doc/api/resources/checkin.rst
@@ -421,3 +421,94 @@ Annulment of a check-in
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
:statuscode 404: The requested nonce does not exist.
+
+
+Check-in history
+----------------
+
+.. rst-class:: rest-resource-table
+
+===================================== ========================== =======================================================
+Field Type Description
+===================================== ========================== =======================================================
+id integer Internal ID of the check-in
+successful boolean Whether the check-in was successful
+error_reason string Category of reason why the check-in was unsuccessful. Currently
+ ``"canceled"``, ``"invalid"``, ``"unpaid"`` ``"product"``,
+ ``"rules"``, ``"revoked"``, ``"incomplete"``, ``"already_redeemed"``,
+ ``"ambiguous"``, ``"error"``, ``"blocked"``, ``"unapproved"``,
+ ``"invalid_time"``, ``"annulled"`` or ``null``
+error_explanation string Additional, human-readable reason for the check-in to be unsuccessful (or ``null``)
+position integer Internal ID of the order position (or ``null`` for unknown scans)
+datetime datetime Logical time when the check-in happened
+created datetime Time when the check-in appeared on the server
+list integer Internal ID of the check-in list
+auto_checked_in boolean Whether the check-in was performed by the system automatically
+gate integer Internal ID of the gate (or ``null``)
+device integer Internal ID of the device (or ``null``)
+device_id integer Organizer-internal ID of the device (or ``null``)
+type string Type of check-in, currently ``"entry"`` or ``"exit"``
+===================================== ========================== =======================================================
+
+.. http:get:: /api/v1/organizers/(organizer)/events/(event)/checkins/
+
+ Returns a list of all check-in events within a given event.
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /api/v1/organizers/bigevents/events/sampleconf/checkins/ HTTP/1.1
+ Host: pretix.eu
+ Accept: application/json, text/javascript
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: application/json
+
+ {
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "successful": true,
+ "error_reason": null,
+ "error_explanation": null,
+ "position": 1234,
+ "datetime": "2017-12-25T12:45:23Z",
+ "created": "2017-12-25T12:45:23Z",
+ "list": 2,
+ "auto_checked_in": false,
+ "gate": null,
+ "device": null,
+ "device_id": null,
+ "type": "entry",
+ }
+ ]
+ }
+
+ :query integer page: The page number in case of a multi-page result set, default is 1
+ :query datetime created_since: Only return check-ins that have been created since the given date (inclusive).
+ :query datetime created_before: Only return check-ins that have been created before the given date (exclusive).
+ :query datetime datetime_since: Only return check-ins that have happened since the given date (inclusive).
+ :query datetime datetime_before: Only return check-ins that have happened before the given date (exclusive).
+ :query boolean successful: Only return check-ins that have (not) been successful.
+ :query boolean error_reason: Only return check-ins with a specific error reason.
+ :query integer list: Only return check-ins from a specific list.
+ :query string type: Only return check-ins of a specific type.
+ :query integer gate: Only return check-ins from a specific gate.
+ :query integer device: Only return check-ins from a specific device.
+ :query boolean auto_checked_in: Only return check-ins that are (not) auto-checked in.
+ :query string ordering: Manually set the ordering of results. Valid fields to be used are ``datetime``, ``created``,
+ and ``id``.
+ :param organizer: The ``slug`` field of the organizer to fetch
+ :param event: The ``slug`` field of the event to fetch
+ :statuscode 200: no error
+ :statuscode 401: Authentication failure
+ :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst
index 5073c3a408..2894d16c6e 100644
--- a/doc/api/resources/orders.rst
+++ b/doc/api/resources/orders.rst
@@ -1030,8 +1030,8 @@ Creating orders
* ``internal_reference``
* ``vat_id``
* ``vat_id_validated`` (optional) – If you need support for reverse charge (rarely the case), you need to check
- yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
- trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
+ yourself if the passed VAT ID is a valid EU VAT ID. In that case, set this to ``true``. Only valid VAT IDs will
+ trigger reverse charge taxation. Don't forget to set ``is_business`` as well!
* ``transmission_type`` (optional, defaults to ``email``)
* ``transmission_info`` (optional, see also :ref:`rest-transmission-types`)
@@ -1058,6 +1058,7 @@ Creating orders
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
+ * ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers``
* ``question``
diff --git a/pyproject.toml b/pyproject.toml
index a5ab8c5461..c1a12ddf82 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,7 +92,7 @@ dependencies = [
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.42.*",
- "sepaxml==2.6.*",
+ "sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2020041600",
diff --git a/src/pretix/__init__.py b/src/pretix/__init__.py
index ab6b82dbd7..7c5f8da56f 100644
--- a/src/pretix/__init__.py
+++ b/src/pretix/__init__.py
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
#
{% blocktrans trimmed %} @@ -17,7 +18,7 @@ {% endif %} - {{ details|linebreaks }} + {{ details | rich_text }} {% if not code %}{% endif %} {% if code %} diff --git a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html index ce8817820c..a6a44d761e 100644 --- a/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html +++ b/src/pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/pending.html @@ -18,20 +18,18 @@
diff --git a/src/pretix/presale/views/user.py b/src/pretix/presale/views/user.py index 4fda0d32ff..a95118408e 100644 --- a/src/pretix/presale/views/user.py +++ b/src/pretix/presale/views/user.py @@ -107,4 +107,10 @@ class UnlockHashView(EventViewMixin, View): hashes = request.session.get('pretix_unlock_hashes', []) hashes.append(kwargs.get('hash')) request.session['pretix_unlock_hashes'] = hashes + + if 'voucher' in request.GET: + return redirect_to_url( + eventreverse(self.request.event, 'presale:event.redeem') + f'?{request.META["QUERY_STRING"]}' + ) + return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index a78a9acfe5..a1928bbcaf 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -348,7 +348,7 @@ var form_handlers = function (el) { dependency.on("change", update); }); - el.find("div[data-display-dependency], textarea[data-display-dependency], input[data-display-dependency], select[data-display-dependency]").each(function () { + el.find("div[data-display-dependency], textarea[data-display-dependency], input[data-display-dependency], select[data-display-dependency], button[data-display-dependency]").each(function () { var dependent = $(this), dependency = findDependency($(this).attr("data-display-dependency"), this), update = function (ev) { @@ -373,10 +373,11 @@ var form_handlers = function (el) { enabled = !enabled; } var $toggling = dependent; - if (dependent.attr("data-disable-dependent")) { + if (dependent.is("[data-disable-dependent]")) { $toggling.attr('disabled', !enabled).trigger("change"); } - if (dependent.get(0).tagName.toLowerCase() !== "div") { + const tagName = dependent.get(0).tagName.toLowerCase() + if (tagName !== "div" && tagName !== "button") { $toggling = dependent.closest('.form-group'); } if (ev) { diff --git a/src/pretix/static/pretixpresale/scss/_forms.scss b/src/pretix/static/pretixpresale/scss/_forms.scss index 0cff5dca40..89f19417cc 100644 --- a/src/pretix/static/pretixpresale/scss/_forms.scss +++ b/src/pretix/static/pretixpresale/scss/_forms.scss @@ -173,7 +173,7 @@ output { } } @media (min-width: $screen-md-min) { - .form-group:not(:has(.checkbox)) { + .form-group:not(:has(.checkbox)), .form-group:has(.checkbox + .checkbox) { .label-required { display: block; } diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index fd18b497dc..1dc696e6f3 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -236,6 +236,21 @@ TEST_LIST_RES = { "rules": {} } +TEST_HISTORY_RES = { + "successful": True, + "error_reason": None, + "error_explanation": None, + "position": 1234, + "datetime": "2017-12-25T12:45:23Z", + "created": "2017-12-25T12:45:23Z", + "list": 2, + "auto_checked_in": False, + "gate": None, + "device": None, + "device_id": None, + "type": "entry", +} + @pytest.fixture def clist(event, item): @@ -1366,3 +1381,57 @@ def test_expand(token_client, organizer, event, clist, clist_all, item, other_it )) assert resp.status_code == 200 assert 'value' in resp.data['results'][0]['variation'] + + +@pytest.mark.django_db +def test_history(token_client, organizer, event, clist, order): + with scopes_disabled(): + ci = order.positions.first().checkins.create(list=clist, type=Checkin.TYPE_ENTRY, datetime=now()) + res = dict(TEST_HISTORY_RES) + res["id"] = ci.pk + res["datetime"] = ci.datetime.isoformat().replace('+00:00', 'Z') + res["created"] = ci.created.isoformat().replace('+00:00', 'Z') + res["list"] = clist.pk + res["position"] = ci.position_id + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/'.format( + organizer.slug, event.slug, + )) + assert resp.status_code == 200 + assert res == resp.data['results'][0] + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?auto_checked_in=false'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?auto_checked_in=true'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?successful=true'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?successful=false'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?type=entry'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?type=exit'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 + + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?created_before=2099-01-01T00:00:00Z'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 1 + resp = token_client.get('/api/v1/organizers/{}/events/{}/checkins/?created_before=2017-01-01T00:00:00Z'.format( + organizer.slug, event.slug, + )) + assert len(resp.data['results']) == 0 diff --git a/src/tests/api/test_order_create.py b/src/tests/api/test_order_create.py index 06083965d6..e954da0477 100644 --- a/src/tests/api/test_order_create.py +++ b/src/tests/api/test_order_create.py @@ -3134,3 +3134,128 @@ def test_order_create_create_medium(token_client, organizer, event, item, quota, m = organizer.reusable_media.get(identifier=i) assert m.linked_orderposition == o.positions.first() assert m.type == "barcode" + + +@pytest.mark.django_db +def test_order_create_auto_pricing_discount(token_client, organizer, event, item, quota, question, taxrule): + with scopes_disabled(): + event.discounts.create( + condition_min_count=2, + benefit_discount_matching_percent=50, + benefit_only_apply_to_cheapest_n_matches=1, + ) + + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + del res['positions'][0]['positionid'] + del res['positions'][0]['price'] + res['positions'].append(dict(res['positions'][0])) + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + p1 = o.positions.first() + p2 = o.positions.last() + assert p1.price == Decimal('23') + assert p2.price == Decimal('11.50') + assert o.total == Decimal('34.75') + + +@pytest.mark.django_db +def test_order_create_auto_pricing_do_not_discount_if_price_explcitly_set(token_client, organizer, event, item, quota, question, taxrule): + with scopes_disabled(): + event.discounts.create( + condition_min_count=2, + benefit_discount_matching_percent=50, + benefit_only_apply_to_cheapest_n_matches=1, + ) + + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + del res['positions'][0]['positionid'] + res['positions'].append(dict(res['positions'][0])) + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + p1 = o.positions.first() + p2 = o.positions.last() + assert p1.price == Decimal('23.00') + assert p2.price == Decimal('23.00') + assert o.total == Decimal('46.25') + + +@pytest.mark.django_db +def test_order_create_auto_pricing_believe_wrong_discounts_by_client(token_client, organizer, event, item, quota, question, taxrule): + with scopes_disabled(): + discount = event.discounts.create( + condition_min_count=2, + benefit_discount_matching_percent=50, + benefit_only_apply_to_cheapest_n_matches=1, + ) + + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + del res['positions'][0]['positionid'] + res['positions'].append(dict(res['positions'][0])) + res['positions'][0]['price'] = Decimal("10.00") + res['positions'][1]['price'] = Decimal("7.00") + res['positions'][1]['discount'] = discount.pk + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 201 + with scopes_disabled(): + o = Order.objects.get(code=resp.data['code']) + p1 = o.positions.first() + p2 = o.positions.last() + assert p1.price == Decimal('10.00') + assert p1.discount is None + assert p2.price == Decimal('7.00') + assert p2.discount == discount + assert o.total == Decimal('17.25') + + +@pytest.mark.django_db +def test_order_create_auto_pricing_explicit_discount_not_allowed(token_client, organizer, event, item, quota, question, taxrule): + with scopes_disabled(): + discount = event.discounts.create( + condition_min_count=2, + benefit_discount_matching_percent=50, + benefit_only_apply_to_cheapest_n_matches=1, + ) + + res = copy.deepcopy(ORDER_CREATE_PAYLOAD) + res['positions'][0]['item'] = item.pk + res['positions'][0]['answers'][0]['question'] = question.pk + del res['positions'][0]['positionid'] + del res['positions'][0]['price'] + res['positions'].append(dict(res['positions'][0])) + res['positions'][1]['discount'] = discount.pk + resp = token_client.post( + '/api/v1/organizers/{}/events/{}/orders/'.format( + organizer.slug, event.slug + ), format='json', data=res + ) + assert resp.status_code == 400 + assert resp.data == { + "positions": [ + {}, + { + "discount": ["You can only specify a discount if you do the price computation, but price is not set."] + } + ] + } diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index 91249dd751..15da07f72e 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -54,6 +54,7 @@ event_urls = [ (None, 'taxrules/'), ('can_view_orders', 'waitinglistentries/'), ('can_view_orders', 'checkinlists/'), + ('can_view_orders', 'checkins/'), (None, 'seats/'), ] @@ -176,6 +177,8 @@ event_permission_sub_urls = [ ('post', 'can_change_orders', 'orders/ABC12/refunds/1/done/', 404), ('get', 'can_view_orders', 'checkinlists/', 200), ('post', 'can_change_orders', 'checkinlists/1/failed_checkins/', 400), + ('get', 'can_view_orders', 'checkins/', 200), + ('get', 'can_view_orders', 'checkins/1/', 404), ('post', 'can_change_event_settings', 'checkinlists/', 400), ('put', 'can_change_event_settings', 'checkinlists/1/', 404), ('patch', 'can_change_event_settings', 'checkinlists/1/', 404), diff --git a/src/tests/base/test_cancelevent.py b/src/tests/base/test_cancelevent.py index 79509aef0f..dbb97c1530 100644 --- a/src/tests/base/test_cancelevent.py +++ b/src/tests/base/test_cancelevent.py @@ -63,6 +63,15 @@ class EventCancelTests(TestCase): generate_invoice(self.order) djmail.outbox = [] + def _cancel_with_dryrun(self, *args, expected_refunds, **kwargs): + dry_run = cancel_event( + *args, **kwargs, dry_run=True + ) + assert dry_run["refund_total"] == expected_refunds + cancel_event( + *args, **kwargs, + ) + @classscope(attr='o') def test_cancel_send_mail(self): gc = self.o.issued_gift_cards.create(currency="EUR") @@ -74,11 +83,11 @@ class EventCancelTests(TestCase): ) self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert len(djmail.outbox) == 1 self.order.refresh_from_db() @@ -114,11 +123,11 @@ class EventCancelTests(TestCase): self.op1.blocked = ["admin"] self.op1.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("23.00") ) self.op1.refresh_from_db() @@ -147,11 +156,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) r = self.order.refunds.get() @@ -175,11 +184,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) self.order.refresh_from_db() @@ -198,11 +207,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("42.00") ) r = self.order.refunds.get() @@ -226,11 +235,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("44.00") ) r = self.order.refunds.get() @@ -252,11 +261,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("44.00") ) r = self.order.refunds.get() @@ -276,11 +285,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("31.40") ) r = self.order.refunds.get() @@ -304,11 +313,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PENDING self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="10.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("12.00") ) assert not self.order.refunds.exists() @@ -335,10 +344,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="", - send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None + send=False, send_subject="Event canceled", send_message="Event canceled :-(", user=None, + expected_refunds=Decimal("36.90") ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE @@ -371,11 +381,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fees=[OrderFee.FEE_TYPE_PAYMENT], keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("39.40") ) r = self.order.refunds.get() assert r.amount == Decimal('39.40') @@ -400,11 +410,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, manual_refund=True, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert self.order.refunds.count() == 2 @@ -436,11 +446,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, manual_refund=False, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert self.order.refunds.count() == 1 @@ -467,11 +477,11 @@ class EventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, manual_refund=True, auto_refund=False, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("46.00") ) assert self.order.refunds.count() == 1 @@ -511,17 +521,26 @@ class SubEventCancelTests(TestCase): generate_invoice(self.order) djmail.outbox = [] + def _cancel_with_dryrun(self, *args, expected_refunds, **kwargs): + dry_run = cancel_event( + *args, **kwargs, dry_run=True + ) + assert dry_run["refund_total"] == expected_refunds + cancel_event( + *args, **kwargs, + ) + @classscope(attr='o') def test_cancel_partially_send_mail_attendees(self): self.op1.attendee_email = 'foo@example.com' self.op1.save() self.op2.attendee_email = 'foo@example.org' self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) assert len(djmail.outbox) == 2 self.order.refresh_from_db() @@ -532,19 +551,19 @@ class SubEventCancelTests(TestCase): def test_cancel_subevent_range(self): self.op2.subevent = self.se1 self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2), auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2), auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_CANCELED @@ -553,11 +572,11 @@ class SubEventCancelTests(TestCase): def test_cancel_simple_order(self): self.op2.subevent = self.se1 self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_CANCELED @@ -567,11 +586,11 @@ class SubEventCancelTests(TestCase): self.op2.subevent = self.se1 self.op2.blocked = ["admin"] self.op2.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING @@ -582,11 +601,11 @@ class SubEventCancelTests(TestCase): @classscope(attr='o') def test_cancel_all_subevents(self): - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_CANCELED @@ -602,11 +621,12 @@ class SubEventCancelTests(TestCase): ) self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self.order.refresh_from_db() + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("23.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PAID @@ -614,20 +634,20 @@ class SubEventCancelTests(TestCase): @classscope(attr='o') def test_cancel_mixed_order_range(self): - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from - timedelta(days=2), auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING assert self.order.positions.count() == 2 - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=None, subevents_from=self.se1.date_from - timedelta(days=3), subevents_to=self.se1.date_from + timedelta(days=2), auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="", send=True, send_subject="Event canceled", send_message="Event canceled :-( {refund_amount}", - user=None + user=None, expected_refunds=Decimal("0.00") ) self.order.refresh_from_db() assert self.order.status == Order.STATUS_PENDING @@ -651,11 +671,11 @@ class SubEventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="10.00", keep_fee_per_ticket="", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("16.20") ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE @@ -682,11 +702,11 @@ class SubEventCancelTests(TestCase): self.order.status = Order.STATUS_PAID self.order.save() - cancel_event( + self._cancel_with_dryrun( self.event.pk, subevent=self.se1.pk, auto_refund=True, keep_fee_fixed="0.00", keep_fee_percentage="0.00", keep_fee_per_ticket="2.00", send=False, send_subject="Event canceled", send_message="Event canceled :-(", - user=None + user=None, expected_refunds=Decimal("21.00") ) r = self.order.refunds.get() assert r.state == OrderRefund.REFUND_STATE_DONE diff --git a/src/tests/base/test_rich_text.py b/src/tests/base/test_rich_text.py index c17fb46546..c893b7d3bb 100644 --- a/src/tests/base/test_rich_text.py +++ b/src/tests/base/test_rich_text.py @@ -73,10 +73,34 @@ from pretix.base.templatetags.rich_text import ( 'goodsite.com', 'https://goodsite.com.evilsite.com', ), + ( + 'evilsite.com/bad/path/', + 'https://evilsite.com/deep/path', + ), + ( + 'evilsite.com/deep', + 'evilsite.com/deep', + ), ( 'evilsite.com', 'evilsite.com', ), + ( + 'evilsite.com', + 'evilsite.com', + ), + ( + 'https://foo:bar@goodsite.com', + 'https://foo:bar@evilsite.com/deep/path', + ), + ( + '@pretix@pretix.social', + '@pretix@pretix.social', + ), + ( + '@pretix@pretix.social', + 'https://evilsite.social/@pretix', + ), ("broken", "broken"), ], )