diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index 5a425c967..73066a9d6 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -61,7 +61,7 @@ Backend item_formsets, order_search_filter_q, order_search_forms .. automodule:: pretix.base.signals - :members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display + :members: logentry_display, logentry_object_link, requiredaction_display, timeline_events, orderposition_blocked_display, customer_created, customer_signed_in Vouchers """""""" diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index 8079ea449..5c064b9c7 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -787,3 +787,23 @@ return a dictionary mapping names of attributes in the settings store to DRF ser As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ + +customer_created = GlobalSignal() +""" +Arguments: ``customer`` + +This signal is sent out every time a customer account is created. The ``customer`` +object is given as the first argument. + +The ``sender`` keyword argument will contain the organizer. +""" + +customer_signed_in = GlobalSignal() +""" +Arguments: ``customer`` + +This signal is sent out every time a customer signs in. The ``customer`` object +is given as the first argument. + +The ``sender`` keyword argument will contain the organizer. +""" diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index b7e5679ef..646ead4f6 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -52,6 +52,7 @@ from pretix.base.customersso.oidc import ( from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition from pretix.base.services.mail import mail from pretix.base.settings import PERSON_NAME_SCHEMES +from pretix.base.signals import customer_created, customer_signed_in from pretix.helpers.compat import CompatDeleteView from pretix.helpers.http import redirect_to_url from pretix.multidomain.models import KnownDomain @@ -151,7 +152,9 @@ class LoginView(RedirectBackMixin, FormView): def form_valid(self, form): """Security check complete. Log the user in.""" - customer_login(self.request, form.get_customer()) + customer = form.get_customer() + customer_login(self.request, customer) + customer_signed_in.send(customer.organizer, customer=customer) return HttpResponseRedirect(self.get_success_url()) @@ -237,7 +240,8 @@ class RegistrationView(RedirectBackMixin, FormView): def form_valid(self, form): with transaction.atomic(): - form.create() + customer = form.create() + customer_created.send(customer.organizer, customer=customer) messages.success( self.request, _('Your account has been created. Please follow the link in the email we sent you to activate your ' @@ -756,6 +760,7 @@ class SSOLoginReturnView(RedirectBackMixin, View): ) try: customer.save(force_insert=True) + customer_created.send(customer.organizer, customer=customer) except IntegrityError: # This might either be a race condition or the email address is taken # by a different customer account @@ -819,6 +824,7 @@ class SSOLoginReturnView(RedirectBackMixin, View): }) else: customer_login(self.request, customer) + customer_signed_in.send(customer.organizer, customer=customer) return redirect_to_url(self.get_success_url(redirect_to)) def _fail(self, message, popup_origin): diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py index c267d18c9..d9ccfade8 100644 --- a/src/tests/presale/test_customer.py +++ b/src/tests/presale/test_customer.py @@ -95,8 +95,12 @@ def test_native_disabled(env, client): @pytest.mark.django_db -def test_org_register(env, client): +def test_org_register(env, client, mocker): + from pretix.base.signals import customer_created + mocker.patch('pretix.base.signals.customer_created.send') + signer = signing.TimestampSigner(salt='customer-registration-captcha-127.0.0.1') + r = client.post('/bigevents/account/register', { 'email': 'john@example.org', 'name_parts_0': 'John Doe', @@ -109,6 +113,7 @@ def test_org_register(env, client): customer = env[0].customers.get(email='john@example.org') assert not customer.is_verified assert customer.is_active + customer_created.send.assert_called_once_with(customer.organizer, customer=customer) r = client.post( f'/bigevents/account/activate?id={customer.identifier}&token={TokenGenerator().make_token(customer)}', { @@ -123,7 +128,10 @@ def test_org_register(env, client): @pytest.mark.django_db -def test_org_register_duplicate_email(env, client): +def test_org_register_duplicate_email(env, client, mocker): + from pretix.base.signals import customer_created + mocker.patch('pretix.base.signals.customer_created.send') + with scopes_disabled(): env[0].customers.create(email='john@example.org') r = client.post('/bigevents/account/register', { @@ -132,6 +140,7 @@ def test_org_register_duplicate_email(env, client): }) assert b'already registered' in r.content assert r.status_code == 200 + customer_created.send.assert_not_called() @pytest.mark.django_db @@ -167,7 +176,11 @@ def test_org_activate_invalid_token(env, client): @pytest.mark.django_db -def test_org_login_logout(env, client): +def test_org_login_logout(env, client, mocker): + from pretix.base.signals import customer_signed_in + mocker.patch('pretix.base.signals.customer_signed_in.send') + + customer = None with scopes_disabled(): customer = env[0].customers.create(email='john@example.org', is_verified=True) customer.set_password('foo') @@ -179,6 +192,8 @@ def test_org_login_logout(env, client): }) assert r.status_code == 302 + customer_signed_in.send.assert_called_once_with(customer.organizer, customer=customer) + r = client.get('/bigevents/account/') assert r.status_code == 200 @@ -190,7 +205,10 @@ def test_org_login_logout(env, client): @pytest.mark.django_db -def test_org_login_invalid_password(env, client): +def test_org_login_invalid_password(env, client, mocker): + from pretix.base.signals import customer_signed_in + mocker.patch('pretix.base.signals.customer_signed_in.send') + with scopes_disabled(): customer = env[0].customers.create(email='john@example.org', is_verified=True) customer.set_password('foo') @@ -202,10 +220,15 @@ def test_org_login_invalid_password(env, client): }) assert r.status_code == 200 assert b'alert-danger' in r.content + customer_signed_in.send.assert_not_called() @pytest.mark.django_db -def test_org_login_not_verified(env, client): +def test_org_login_not_verified(env, client, mocker): + from pretix.base.signals import customer_signed_in + mocker.patch('pretix.base.signals.customer_signed_in.send') + + customer = None with scopes_disabled(): customer = env[0].customers.create(email='john@example.org', is_verified=False) customer.set_password('foo') @@ -217,10 +240,14 @@ def test_org_login_not_verified(env, client): }) assert r.status_code == 200 assert b'alert-danger' in r.content + customer_signed_in.send.assert_not_called() @pytest.mark.django_db -def test_org_login_not_active(env, client): +def test_org_login_not_active(env, client, mocker): + from pretix.base.signals import customer_signed_in + mocker.patch('pretix.base.signals.customer_signed_in.send') + with scopes_disabled(): customer = env[0].customers.create(email='john@example.org', is_verified=True, is_active=False) customer.set_password('foo') @@ -232,6 +259,7 @@ def test_org_login_not_active(env, client): }) assert r.status_code == 200 assert b'alert-danger' in r.content + customer_signed_in.send.assert_not_called() @pytest.fixture @@ -309,12 +337,18 @@ def _sso_login(client, provider, email='test@example.org', popup_origin=None, ex @pytest.mark.django_db -def test_org_sso_login_new_customer(env, client, provider): +def test_org_sso_login_new_customer(env, client, provider, mocker): + from pretix.base.signals import customer_created, customer_signed_in + mocker.patch('pretix.base.signals.customer_created.send') + mocker.patch('pretix.base.signals.customer_signed_in.send') + _sso_login(client, provider) with scopes_disabled(): c = Customer.objects.get(provider=provider) assert c.external_identifier == "abcdf" + customer_created.send.assert_called_once_with(c.organizer, customer=c) + customer_signed_in.send.assert_called_once_with(c.organizer, customer=c) r = client.get('/bigevents/account/') assert r.status_code == 200 @@ -352,10 +386,15 @@ def test_org_sso_login_new_customer_popup_invalid_origin(env, client, provider): @pytest.mark.django_db -def test_org_sso_login_returning_customer_new_email(env, client, provider): +def test_org_sso_login_returning_customer_new_email(env, client, provider, mocker): + from pretix.base.signals import customer_signed_in + mocker.patch('pretix.base.signals.customer_signed_in.send') + _sso_login(client, provider) with scopes_disabled(): c = Customer.objects.get(provider=provider) + customer_signed_in.send.assert_called_once_with(c.organizer, customer=c) + customer_signed_in.send.reset_mock() r = client.get('/bigevents/account/logout') assert r.status_code == 302 @@ -363,6 +402,7 @@ def test_org_sso_login_returning_customer_new_email(env, client, provider): _sso_login(client, provider, 'new@example.net') c.refresh_from_db() assert c.email == "new@example.net" + customer_signed_in.send.assert_called_once_with(c.organizer, customer=c) @pytest.mark.django_db(transaction=True)