Compare commits

..
Author SHA1 Message Date
Martin Gross 7da5d2e0e7 Vite: Add dev CORS 2026-09-01 10:23:16 +02:00
Martin Gross 4390403b9a Add label-overrides to contact_form_fields_overrides and question_form_fields_overrides (Z#23244154) (#6504) 2026-09-01 10:21:20 +02:00
Lukas Bockstaller 9d53cf840b PayPal: validate that the sale has any captures before marking paid (#6498)
* validate that the sale has any captures before marking paid

* code style
2026-09-01 10:16:51 +02:00
luelista 023f9104ef Fix customer password reset rate limit (#6501) 2026-08-31 16:48:48 +02:00
Lukas Bockstaller e6572344ca CI: add tracing for e2e tests during failure (#6491)
* collect and upload traces on failure

* include deps for pw install
2026-08-31 12:52:06 +02:00
9 changed files with 163 additions and 22 deletions
+19 -2
View File
@@ -123,7 +123,24 @@ jobs:
working-directory: ./src
run: make all compress
- name: Install Playwright browsers
run: playwright install
run: playwright install --with-deps
- name: Run E2E tests
working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10 --tracing=retain-on-failure
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test-results/
- name: Log trace instructions
if: steps.check-traces.outputs.found == 'true'
run: |
{
echo "## 🎭 Playwright traces available"
echo ""
echo "Some tests failed or retried and produced traces."
echo ""
echo "1. Download the **playwright-traces-${{ github.run_id }}** artifact from this run (link in the **Summary** tab, under Artifacts)."
echo "2. Unzip it."
echo "3. Go to https://trace.playwright.dev and drag \`trace.zip\` into the page — or run \`npx playwright show-trace trace.zip\` locally."
} >> "$GITHUB_STEP_SUMMARY"
+2
View File
@@ -135,6 +135,8 @@ class BaseQuestionsViewMixin:
question_field.initial = getattr(question_field, 'initial', None) or src['initial']
if 'validators' in src:
question_field.validators += src['validators']
if 'label' in src:
question_field.label = src['label']
if len(form.fields) > 0:
formlist.append(form)
+3 -1
View File
@@ -494,6 +494,7 @@ def webhook(request, *args, **kwargs):
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED):
if sale['status'] == 'COMPLETED':
any_captures = False
all_captures_completed = True
any_pending_review = False
any_failed = None
@@ -505,6 +506,7 @@ def webhook(request, *args, **kwargs):
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
any_captures = True
if capture['status'] in ('COMPLETED', 'REFUNDED', 'PARTIALLY_REFUNDED'):
pass
elif capture['status'] in ("DECLINED", "FAILED"):
@@ -516,7 +518,7 @@ def webhook(request, *args, **kwargs):
any_pending_review = True
else:
raise ValueError("Unknown paypal capture state: {}".format(capture['status']))
if all_captures_completed:
if any_captures and all_captures_completed:
try:
payment.confirm()
prov.log_payment_duration(payment)
+4
View File
@@ -840,6 +840,8 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
f.fields[fname].disabled = val['disabled']
if 'validators' in val and fname in f.fields:
f.fields[fname].validators += val['validators']
if 'label' in val and fname in f.fields:
f.fields[fname].label = val['label']
return f
@@ -944,6 +946,8 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
f.fields[fname].disabled = val['disabled']
if 'validators' in val and fname in f.fields:
f.fields[fname].validators += val['validators']
if 'label' in val and fname in f.fields:
f.fields[fname].label = val['label']
return f
+1 -1
View File
@@ -334,7 +334,7 @@ class ResetPasswordForm(forms.Form):
def clean_email(self):
if 'email' not in self.cleaned_data:
return
if rate_limit("customer_pwreset_check", max_num=10, expire_time=600):
if rate_limit("customer_pwreset_check", include_ip_from_request=self.request, max_num=10, expire_time=600):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
+4 -4
View File
@@ -233,7 +233,7 @@ Arguments: ``request``, ``order``
This signal allows you to override fields of the contact form that is presented during checkout
and by default only asks for the email address. It is also being used for the invoice address
form. You are supposed to return a dictionary of dictionaries with globally unique keys. The
value-dictionary should contain one or more of the following keys: ``initial``, ``disabled``,
value-dictionary should contain one or more of the following keys: ``label``, ``initial``, ``disabled``,
``validators``. The key of the dictionary should be the name of the form field.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. A ``request``
@@ -264,9 +264,9 @@ Arguments: ``position``, ``request``
This signal allows you to override fields of the questions form that is presented during checkout
and by default only asks for the questions configured in the backend. You are supposed to return a
dictionary of dictionaries with globally unique keys. The value-dictionary should contain one or
more of the following keys: ``initial``, ``disabled``, ``validators``. The key of the dictionary
should be the form field name for system fields (e.g. ``company``), or the question's ``identifier``
for user-defined questions.
more of the following keys: ``label``, ``initial``, ``disabled``, ``validators``. The key of the
dictionary should be the form field name for system fields (e.g. ``company``), or the question's
``identifier`` for user-defined questions.
The ``position`` keyword argument will contain a ``CartPosition`` or ``OrderPosition`` object.
+9 -14
View File
@@ -46,7 +46,7 @@ import isoweek
from django.conf import settings
from django.core.cache import caches
from django.db.models import (
Case, Exists, F, Max, Min, OuterRef, Prefetch, Q, Subquery, Value, When,
Case, Exists, F, Max, Min, OuterRef, Prefetch, Q, Value, When,
)
from django.db.models.functions import Coalesce, Greatest
from django.dispatch.dispatcher import NO_RECEIVERS
@@ -189,27 +189,22 @@ class EventListMixin:
def _get_event_list_queryset(self):
query = Q(is_public=True) & Q(live=True)
qs = self.request.organizer.events.using(settings.DATABASE_REPLICA).filter(query)
qs = qs.filter(Q(all_sales_channels=True) | Q(id__in=self.request.sales_channel.event_set.values_list("pk")))
qs = qs.filter(Q(all_sales_channels=True) | Q(limit_sales_channels=self.request.sales_channel))
show_old = "old" in self.request.GET
subevent_filter = Q(active=True, is_public=True)
subevent_filter = Q(subevents__active=True, subevents__is_public=True)
if not show_old:
subevent_filter &= Q(
Q(date_to__gte=now()) | Q(date_from__gte=now())
Q(subevents__date_to__gte=now()) | Q(subevents__date_from__gte=now())
)
subevent_subquery_qs = SubEvent.objects.with_scopes_disabled().filter(
subevent_filter,
event_id=OuterRef('pk')
).values('event').order_by()
qs = qs.annotate(
min_from=Subquery(subevent_subquery_qs.annotate(m=Min('date_from')).values('m')),
min_to=Subquery(subevent_subquery_qs.annotate(m=Min('date_to')).values('m')),
max_from=Subquery(subevent_subquery_qs.annotate(m=Max('date_from')).values('m')),
max_to=Subquery(subevent_subquery_qs.annotate(m=Max('date_to')).values('m')),
).annotate(
max_fromto=Greatest(F("max_to"), F("max_from")),
min_from=Min('subevents__date_from', filter=subevent_filter),
min_to=Min('subevents__date_to', filter=subevent_filter),
max_from=Max('subevents__date_from', filter=subevent_filter),
max_to=Max('subevents__date_to', filter=subevent_filter),
max_fromto=Greatest(Max('subevents__date_to', filter=subevent_filter), Max('subevents__date_from', filter=subevent_filter)),
)
if show_old:
date_q = Q(date_to__lt=now()) | (Q(date_to__isnull=True) & Q(date_from__lt=now()))
+118
View File
@@ -299,6 +299,30 @@ def get_test_order_review_pending():
'method': 'GET'}]}
def get_test_empty_captures():
return {'id': '806440346Y391300T',
'intent': 'CAPTURE',
'status': 'COMPLETED',
'purchase_units': [{'reference_id': 'default',
'amount': {'currency_code': 'EUR', 'value': '43.59'},
'payee': {'email_address': 'dummy-facilitator@dummy.dummy',
'merchant_id': 'G6R2B9YXADKWW'},
'description': 'Order JWJGC for PayPal v2',
'custom_id': 'Order PAYPALV2-JWJGC',
'soft_descriptor': 'MARTINFACIL',
'payments': {'captures': []}
}],
'payer': {'name': {'given_name': 'test', 'surname': 'buyer'},
'email_address': 'dummy@dummy.dummy',
'payer_id': 'Q739JNKWH67HE',
'address': {'country_code': 'DE'}},
'create_time': '2022-04-28T11:59:59Z',
'update_time': '2022-04-28T12:00:22Z',
'links': [{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
'rel': 'self',
'method': 'GET'}]}
class Object():
pass
@@ -456,6 +480,100 @@ def test_webhook_all_good(env, client, monkeypatch):
assert order.status == Order.STATUS_PAID
@pytest.mark.django_db
def test_webhook_empty_captures(env, client, monkeypatch):
order = env[1]
with scopes_disabled():
p = order.payments.first()
p.state = OrderPayment.PAYMENT_STATE_PENDING
p.save()
order.status = Order.STATUS_PENDING
order.save()
pp_order = Result(get_test_empty_captures())
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
with scopes_disabled():
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
reference="806440346Y391300T")
client.post('/_paypal/webhook/', json.dumps(
{
"id": "WH-4T867178D0574904F-7TT11736YU643990P",
"create_time": "2022-04-28T12:00:37.077Z",
"resource_type": "checkout-order",
"event_type": "CHECKOUT.ORDER.COMPLETED",
"summary": "Checkout Order Completed",
"resource": {
"update_time": "2022-04-28T12:00:22Z",
"create_time": "2022-04-28T11:59:59Z",
"purchase_units": [
{
"reference_id": "default",
"amount": {
"currency_code": "EUR",
"value": "43.59"
},
"payee": {
"email_address": "dummy-facilitator@dummy.dummy",
"merchant_id": "G6R2B9YXADKWW"
},
"description": "Order JWJGC for PayPal v2",
"custom_id": "Order PAYPALV2-JWJGC",
"soft_descriptor": "MARTINFACIL",
"payments": {
"captures": []
}
}
],
"links": [
{
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
"rel": "self",
"method": "GET"
}
],
"id": "806440346Y391300T",
"intent": "CAPTURE",
"payer": {
"name": {
"given_name": "test",
"surname": "buyer"
},
"email_address": "dummy@dummy.dummy",
"payer_id": "Q739JNKWH67HE",
"address": {
"country_code": "DE"
}
},
"status": "COMPLETED"
},
"status": "SUCCESS",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4T867178D0574904F-7TT11736YU643990P",
"rel": "self",
"method": "GET",
"encType": "application/json"
},
{
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4T867178D0574904F-7TT11736YU643990P/resend",
"rel": "resend",
"method": "POST",
"encType": "application/json"
}
],
"event_version": "1.0",
"resource_version": "2.0"
}
), content_type='application_json')
order = env[1]
order.refresh_from_db()
assert order.status == Order.STATUS_PENDING
@pytest.mark.django_db
def test_webhook_mark_paid(env, client, monkeypatch):
order = env[1]
+3
View File
@@ -31,6 +31,9 @@ export default defineConfig({
// Allow serving source files from sibling plugin directories
allow: ['src', ...pluginDirs],
},
cors: {
origin: /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\]|[^:]+\.pretix\.(dev|work))(?::\d+)?$/
},
},
build: {
manifest: true,