mirror of
https://github.com/pretix/pretix.git
synced 2026-08-11 10:57:00 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51f52ffe40 | ||
|
|
d08216d8c5 | ||
|
|
4a28689690 | ||
|
|
958f75b109 |
@@ -71,6 +71,8 @@ Checking a ticket in
|
||||
:>json object questions: List of questions to be answered for check-in, only set on status ``"incomplete"``.
|
||||
:>json object media_policy: Reusable media policy (see documentation on items), only set on status ``"exchange"``.
|
||||
:>json object media_type: Reusable media type (see documentation on items), only set on status ``"exchange"``.
|
||||
:>json boolean simulate: Do not actually perform the check-in, only simulate the response. The ``position`` response
|
||||
object will not reflect the simulated changes.
|
||||
|
||||
**Example request**:
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ class CheckinRPCRedeemInputSerializer(serializers.Serializer):
|
||||
answers = serializers.JSONField(required=False, allow_null=True)
|
||||
exchange_medium_type = serializers.ChoiceField(required=False, choices=MEDIA_TYPES)
|
||||
exchange_medium_identifier = serializers.CharField(required=False)
|
||||
simulate = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -839,6 +839,11 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
)
|
||||
|
||||
if exchange_medium_identifier: # other fields are filled, see CheckinRPCRedeemInputSerializer.validate
|
||||
if simulate:
|
||||
raise CheckInError(
|
||||
gettext('You cannot simulate a medium exchange.'),
|
||||
'error'
|
||||
)
|
||||
with transaction.atomic():
|
||||
# Do exchange and check-in atomically, i.e. both succeed or both fail
|
||||
medium = perform_media_exchange(
|
||||
@@ -1066,6 +1071,7 @@ class CheckinRPCRedeemView(views.APIView):
|
||||
legacy_url_support=False,
|
||||
exchange_medium_type=s.validated_data.get('exchange_medium_type'),
|
||||
exchange_medium_identifier=s.validated_data.get('exchange_medium_identifier'),
|
||||
simulate=s.validated_data.get('simulate'),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ import dateutil
|
||||
import dateutil.parser
|
||||
from dateutil.tz import datetime_exists
|
||||
from django.core.files import File
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import (
|
||||
BooleanField, Case, Count, ExpressionWrapper, F, IntegerField, Max, Min,
|
||||
OuterRef, Q, Subquery, TextField, Value, When,
|
||||
@@ -59,6 +59,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.signals import checkin_created, periodic_task
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.database import conditional_atomic
|
||||
from pretix.helpers.jsonlogic import Logic
|
||||
from pretix.helpers.jsonlogic_boolalg import convert_to_dnf
|
||||
from pretix.helpers.jsonlogic_query import (
|
||||
@@ -1043,10 +1044,10 @@ def perform_checkin(op: OrderPosition, clist: CheckinList, given_answers: dict,
|
||||
if not simulate:
|
||||
_save_answers(op, answers, given_answers)
|
||||
|
||||
with transaction.atomic():
|
||||
with conditional_atomic(not simulate):
|
||||
# Lock order positions, if it is an entry. We don't need it for exits, as a race condition wouldn't be problematic
|
||||
opqs = OrderPosition.all.select_related("order", "item")
|
||||
if type != Checkin.TYPE_EXIT:
|
||||
if type != Checkin.TYPE_EXIT and not simulate:
|
||||
opqs = opqs.select_for_update(of=OF_SELF)
|
||||
op = opqs.get(pk=op.pk)
|
||||
|
||||
|
||||
@@ -3438,7 +3438,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
}
|
||||
)
|
||||
|
||||
new_invoice_created = False
|
||||
new_invoice = None
|
||||
if recreate_invoices:
|
||||
# Lock to prevent duplicate invoice creation
|
||||
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||
@@ -3449,13 +3449,16 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
if has_active_invoice and order.total != oldtotal:
|
||||
try:
|
||||
generate_cancellation(i)
|
||||
generate_invoice(order)
|
||||
new_invoice = generate_invoice(order)
|
||||
except Exception as e:
|
||||
logger.exception("Could not generate invoice.")
|
||||
order.log_action("pretix.event.order.invoice.failed", data={
|
||||
"exception": str(e)
|
||||
})
|
||||
new_invoice_created = True
|
||||
else:
|
||||
order.log_action('pretix.event.order.invoice.generated', data={
|
||||
'invoice': new_invoice.pk
|
||||
})
|
||||
|
||||
elif (not has_active_invoice or order.invoice_dirty) and invoice_qualified(order):
|
||||
if order.event.settings.get('invoice_generate') == 'True' or (
|
||||
@@ -3465,10 +3468,9 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
try:
|
||||
if has_active_invoice:
|
||||
generate_cancellation(i)
|
||||
i = generate_invoice(order)
|
||||
new_invoice_created = True
|
||||
new_invoice = generate_invoice(order)
|
||||
order.log_action('pretix.event.order.invoice.generated', data={
|
||||
'invoice': i.pk
|
||||
'invoice': new_invoice.pk
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("Could not generate invoice.")
|
||||
@@ -3476,8 +3478,11 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
|
||||
"exception": str(e)
|
||||
})
|
||||
|
||||
if new_invoice and invoice_transmission_separately(new_invoice):
|
||||
transmit_invoice.apply_async(args=(order.event_id, new_invoice.pk, False))
|
||||
|
||||
order.create_transactions()
|
||||
return old_fee, new_fee, fee, new_payment, new_invoice_created
|
||||
return old_fee, new_fee, fee, new_payment, bool(new_invoice)
|
||||
|
||||
|
||||
@receiver(order_paid, dispatch_uid="pretixbase_order_paid_giftcards")
|
||||
|
||||
@@ -288,6 +288,15 @@ def get_deterministic_ordering(model, ordering):
|
||||
return ordering
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def conditional_atomic(do_atomic, **kwargs):
|
||||
if do_atomic:
|
||||
with transaction.atomic(**kwargs):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class IgnoreOnSQLiteMixin:
|
||||
# Mixin to allow defining PostgreSQL-specific indexes that will just not be created
|
||||
# on SQLite. SQLite is supported for testing only anyways!
|
||||
|
||||
@@ -23,7 +23,7 @@ import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
@@ -645,7 +645,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
def _execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk)
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
logger.warning('payment is already confirmed; possible return-view/webhook race-condition')
|
||||
# payment is already confirmed; possible return-view/webhook race-condition
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -832,6 +832,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.save(update_fields=['info'])
|
||||
payment.confirm()
|
||||
self.log_payment_duration(payment)
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
# Payment has not any captures yet - so it's probably in created status
|
||||
@@ -841,6 +842,20 @@ class PaypalMethod(BasePaymentProvider):
|
||||
if 'payment_paypal_oid' in request.session:
|
||||
del request.session['payment_paypal_oid']
|
||||
|
||||
@staticmethod
|
||||
def log_payment_duration(payment: OrderPayment):
|
||||
try:
|
||||
capture = payment.info_data["purchase_units"][0]["payments"]["captures"][0]
|
||||
create_time: str | None = capture["create_time"]
|
||||
update_time: str | None = capture["update_time"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
create_time = None
|
||||
update_time = None
|
||||
|
||||
if create_time is not None and update_time is not None:
|
||||
duration = datetime.fromisoformat(update_time) - datetime.fromisoformat(create_time)
|
||||
logger.info('{}: {} - paypal payment processing time'.format(str(payment.global_id), str(duration)))
|
||||
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
retry = True
|
||||
try:
|
||||
|
||||
@@ -490,6 +490,7 @@ def webhook(request, *args, **kwargs):
|
||||
payment.info = json.dumps(sale.dict())
|
||||
payment.save(update_fields=['info'])
|
||||
payment.confirm()
|
||||
prov.log_payment_duration(payment)
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
elif sale['status'] == 'APPROVED':
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
def readonly_db(execute, sql, params, many, context):
|
||||
if not sql.lower().startswith("select"):
|
||||
raise Exception(f"Should not write anything to the database, but detected query: {sql}")
|
||||
return execute(sql, params, many, context)
|
||||
@@ -25,6 +25,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db import connection
|
||||
from django.utils.timezone import now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -36,6 +37,7 @@ from pretix.api.serializers.item import QuestionSerializer
|
||||
from pretix.base.models import (
|
||||
Checkin, InvoiceAddress, Item, Order, OrderPosition, ReusableMedium,
|
||||
)
|
||||
from pretix.testutils.db import readonly_db
|
||||
|
||||
# Lots of this code is overlapping with test_checkin.py, and some of it is arguably redundant since it's triggering
|
||||
# the same backend code paths (for now). However, this is SUCH a critical part of pretix that we don't want to take
|
||||
@@ -1739,3 +1741,41 @@ def test_exchange_create_gift_card(token_client, organizer, clist, event, order,
|
||||
with scopes_disabled():
|
||||
rm = ReusableMedium.objects.get(identifier="0412345")
|
||||
assert rm.linked_giftcard.currency == "EUR"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_simulate(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
with connection.execute_wrapper(readonly_db):
|
||||
resp = _redeem(token_client, organizer, clist, p.secret, {"simulate": True})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert not p.checkins.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_simulate_no_exchange(token_client, organizer, clist, event, order, item):
|
||||
organizer.settings.reusable_media_type_nfc_uid = True
|
||||
item.media_type = "nfc_uid"
|
||||
item.media_policy = Item.MEDIA_POLICY_NEW
|
||||
item.save()
|
||||
with scopes_disabled():
|
||||
rm = ReusableMedium.objects.create(
|
||||
type="nfc_uid",
|
||||
identifier="12345678",
|
||||
organizer=organizer,
|
||||
)
|
||||
with connection.execute_wrapper(readonly_db):
|
||||
resp = _redeem(token_client, organizer, clist, "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", {
|
||||
"source_type": "barcode",
|
||||
"exchange_medium_type": "nfc_uid",
|
||||
"exchange_medium_identifier": "12345678",
|
||||
"simulate": True,
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'error'
|
||||
with scopes_disabled():
|
||||
assert not rm.linked_orderpositions.exists()
|
||||
|
||||
@@ -224,3 +224,28 @@ def test_one_view(logged_in_client, url, expected, event, item, item_category, o
|
||||
)
|
||||
response = logged_in_client.get(url)
|
||||
assert response.status_code == expected
|
||||
|
||||
# Do not reintroduce any CSP nonces into control responses, as discussed in PR #6387
|
||||
if response['Content-Type'] != 'application/json':
|
||||
assert 'script-src' in response['Content-Security-Policy']
|
||||
assert 'nonce-' not in response['Content-Security-Policy']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('url', [
|
||||
'/control/login',
|
||||
'/',
|
||||
'/{orga}/{event}/',
|
||||
])
|
||||
@pytest.mark.django_db
|
||||
def test_csp_header_unauthenticated(client, url, event):
|
||||
# Do not reintroduce any CSP nonces into most presale responses, as discussed in PR #6387
|
||||
with scope(organizer=event.organizer):
|
||||
url = url.format(
|
||||
event=event.slug, orga=event.organizer.slug,
|
||||
)
|
||||
event.live = True
|
||||
event.save()
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert 'script-src' in response['Content-Security-Policy']
|
||||
assert 'nonce-' not in response['Content-Security-Policy']
|
||||
|
||||
@@ -37,18 +37,20 @@ import re
|
||||
from decimal import Decimal
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from django.core import mail as djmail
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer,
|
||||
Question, Quota,
|
||||
Event, Invoice, InvoiceAddress, Item, ItemCategory, ItemVariation, Order,
|
||||
OrderPosition, Organizer, Question, Quota,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment
|
||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||
from pretix.base.services.invoices import generate_invoice
|
||||
from pretix.testutils.scope import classscope
|
||||
|
||||
|
||||
class BaseOrdersTest(TestCase):
|
||||
@@ -1702,6 +1704,37 @@ class OrdersTest(BaseOrdersTest):
|
||||
assert 'Gift card' in response.content.decode()
|
||||
assert '1 available' in response.content.decode()
|
||||
|
||||
@classscope("orga")
|
||||
def test_change_paymentmethod_invoice_separately(self):
|
||||
self.event.settings.payment_banktransfer__enabled = True
|
||||
self.event.settings.payment_banktransfer_invoice_immediately = True
|
||||
self.event.settings.invoice_generate = "paid"
|
||||
InvoiceAddress.objects.create(
|
||||
order=self.order,
|
||||
transmission_type="email",
|
||||
transmission_info={
|
||||
"transmission_email_other": True,
|
||||
"transmission_email_address": "invoice@example.org",
|
||||
}
|
||||
)
|
||||
|
||||
assert self.order.invoices.count() == 0
|
||||
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
self.client.post(
|
||||
'/%s/%s/order/%s/%s/pay/change' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret),
|
||||
{
|
||||
'payment': 'banktransfer'
|
||||
}
|
||||
)
|
||||
|
||||
assert self.order.payments.last().provider == 'banktransfer'
|
||||
assert self.order.invoices.count() == 1
|
||||
i = self.order.invoices.last()
|
||||
assert i.transmission_status == Invoice.TRANSMISSION_STATUS_COMPLETED
|
||||
assert ["invoice@example.org"] == djmail.outbox[0].to
|
||||
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
|
||||
|
||||
def test_answer_download_token(self):
|
||||
with scopes_disabled():
|
||||
q = self.event.questions.create(question="Foo", type="F")
|
||||
|
||||
Reference in New Issue
Block a user