Stripe checkout #104411
@ -714,3 +714,19 @@ button,
|
|||||||
&.rounded-lg
|
&.rounded-lg
|
||||||
.plyr
|
.plyr
|
||||||
border-radius: var(--border-radius-lg)
|
border-radius: var(--border-radius-lg)
|
||||||
|
|
||||||
|
|
||||||
|
/* Simple CSS-only tooltips. */
|
||||||
|
[data-tooltip]
|
||||||
|
&:hover
|
||||||
|
&:before, &:after
|
||||||
|
display: block
|
||||||
|
position: absolute
|
||||||
|
color: var(--color-text-primary)
|
||||||
|
&:before
|
||||||
|
border-radius: var(--spacer-1)
|
||||||
|
content: attr(title)
|
||||||
|
background-color: var(--color-bg-primary-subtle)
|
||||||
|
margin-top: var(--spacer)
|
||||||
|
padding: var(--spacer)
|
||||||
|
font-size: var(--fs-sm)
|
||||||
|
@ -155,6 +155,12 @@ class PaymentForm(BillingAddressForm):
|
|||||||
but are still used by the payment flow.
|
but are still used by the payment flow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
gateway = looper.form_fields.GatewayChoiceField(
|
||||||
|
queryset=looper.models.Gateway.objects.filter(name__in={'stripe', 'bank'}).order_by(
|
||||||
|
'-is_default'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# These are used when a payment fails, so that the next attempt to pay can reuse
|
# These are used when a payment fails, so that the next attempt to pay can reuse
|
||||||
# the already-created subscription and order.
|
# the already-created subscription and order.
|
||||||
subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
@ -3,7 +3,6 @@ from typing import Set
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
import alphabetic_timestamp as ats
|
import alphabetic_timestamp as ats
|
||||||
@ -30,43 +29,6 @@ def timebased_order_number():
|
|||||||
return ats.base36.now(time_unit=ats.TimeUnit.milliseconds).upper()
|
return ats.base36.now(time_unit=ats.TimeUnit.milliseconds).upper()
|
||||||
|
|
||||||
|
|
||||||
@receiver(django_signals.post_save, sender=User)
|
|
||||||
def create_customer(sender, instance: User, created, raw, **kwargs):
|
|
||||||
"""Create Customer on User creation."""
|
|
||||||
from looper.models import Customer
|
|
||||||
|
|
||||||
if raw:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not created:
|
|
||||||
return
|
|
||||||
|
|
||||||
my_log = logger.getChild('create_customer')
|
|
||||||
try:
|
|
||||||
customer = instance.customer
|
|
||||||
except Customer.DoesNotExist:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
my_log.debug(
|
|
||||||
'Newly created User %d already has a Customer %d, not creating new one',
|
|
||||||
instance.pk,
|
|
||||||
customer.pk,
|
|
||||||
)
|
|
||||||
billing_address = customer.billing_address
|
|
||||||
my_log.info('Creating new billing address due to creation of user %s', instance.pk)
|
|
||||||
if not billing_address.pk:
|
|
||||||
billing_address.email = instance.email
|
|
||||||
billing_address.save()
|
|
||||||
return
|
|
||||||
|
|
||||||
my_log.info('Creating new Customer due to creation of user %s', instance.pk)
|
|
||||||
with transaction.atomic():
|
|
||||||
customer = Customer.objects.create(user=instance)
|
|
||||||
billing_address = customer.billing_address
|
|
||||||
billing_address.email = instance.email
|
|
||||||
billing_address.save()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(django_signals.pre_save, sender=Order)
|
@receiver(django_signals.pre_save, sender=Order)
|
||||||
def _set_order_number(sender, instance: Order, **kwargs):
|
def _set_order_number(sender, instance: Order, **kwargs):
|
||||||
if instance.pk or instance.number or instance.is_legacy:
|
if instance.pk or instance.number or instance.is_legacy:
|
||||||
|
@ -43,7 +43,12 @@
|
|||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col text-end">
|
<div class="col text-end">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<button class="btn btn-primary w-100" id="submit-button" type="submit">{{ button_text|default:"Continue" }}</button>
|
{% with gw=form.gateway.field.queryset.first %}
|
||||||
|
<button class="btn btn-primary w-100" id="submit-button" type="submit"
|
||||||
|
{% if gw %}name="gateway" value="{{ gw.name }}"{% endif %}>
|
||||||
|
{{ button_text|default:"Continue" }}
|
||||||
|
</button>
|
||||||
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="btn btn-primary w-100 x-sign-in" href="{% url 'oauth:login' %}">Sign in with Blender ID</a>
|
<a class="btn btn-primary w-100 x-sign-in" href="{% url 'oauth:login' %}">Sign in with Blender ID</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -30,7 +30,6 @@
|
|||||||
{% include "subscriptions/components/billing_address_form.html" %}
|
{% include "subscriptions/components/billing_address_form.html" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<p class="mb-0 text-muted x-sm">Required fields are marked with (*).</p>
|
<p class="mb-0 text-muted x-sm">Required fields are marked with (*).</p>
|
||||||
{{ form.price }}
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-bottom border-1 mb-4 pb-2">
|
<div class="border-bottom border-1 mb-4 pb-2">
|
||||||
@ -54,6 +53,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'subscriptions/components/total.html' with button_text="Continue to Payment" %}
|
{% include 'subscriptions/components/total.html' with button_text="Continue to Payment" %}
|
||||||
|
|
||||||
|
{% with gw_bank=form.gateway.field.queryset.last %}
|
||||||
|
{% if current_plan_variation.collection_method == "manual" and gw_bank.name == "bank" %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col text-end">
|
||||||
|
<button class="btn" type="submit" name="gateway" value="bank">Pay via Bank Transfer</button>
|
||||||
|
<span data-tooltip title="{{ gw_bank.form_description|striptags }}" class="text-left"><i class="i-info text-warning"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from typing import Tuple
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
@ -8,6 +9,8 @@ import factory
|
|||||||
import responses
|
import responses
|
||||||
|
|
||||||
from looper.tests.factories import create_customer_with_billing_address
|
from looper.tests.factories import create_customer_with_billing_address
|
||||||
|
import looper.models
|
||||||
|
|
||||||
import users.tests.util as util
|
import users.tests.util as util
|
||||||
|
|
||||||
|
|
||||||
@ -22,6 +25,16 @@ def _write_mail(mail, index=0):
|
|||||||
|
|
||||||
|
|
||||||
class BaseSubscriptionTestCase(TestCase):
|
class BaseSubscriptionTestCase(TestCase):
|
||||||
|
def _get_url_for(self, **filter_params) -> Tuple[str, looper.models.PlanVariation]:
|
||||||
|
plan_variation = looper.models.PlanVariation.objects.active().get(**filter_params)
|
||||||
|
return (
|
||||||
|
reverse(
|
||||||
|
'subscriptions:join-billing-details',
|
||||||
|
kwargs={'plan_variation_id': plan_variation.pk},
|
||||||
|
),
|
||||||
|
plan_variation,
|
||||||
|
)
|
||||||
|
|
||||||
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Allow requests to Braintree Sandbox
|
# Allow requests to Braintree Sandbox
|
||||||
@ -83,12 +96,15 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self._assert_continue_to_payment_displayed(response)
|
self._assert_continue_to_payment_displayed(response)
|
||||||
self.assertContains(response, 'id_street_address')
|
self.assertContains(response, 'id_street_address')
|
||||||
self.assertContains(response, 'id_full_name')
|
self.assertContains(response, 'id_full_name')
|
||||||
|
self.assertContains(response, 'name="gateway" value="stripe"')
|
||||||
|
self.assertNotContains(response, 'name="gateway" value="bank"')
|
||||||
|
|
||||||
def _assert_payment_form_displayed(self, response):
|
def _assert_billing_details_form_with_pay_via_bank_displayed(self, response):
|
||||||
self.assertNotContains(response, 'Pricing has been updated')
|
self._assert_continue_to_payment_displayed(response)
|
||||||
self.assertNotContains(response, 'Continue to Payment')
|
self.assertContains(response, 'id_street_address')
|
||||||
self.assertContains(response, 'payment method')
|
self.assertContains(response, 'id_full_name')
|
||||||
self.assertContains(response, 'Confirm and Pay')
|
self.assertContains(response, 'name="gateway" value="stripe"')
|
||||||
|
self.assertContains(response, 'name="gateway" value="bank"')
|
||||||
|
|
||||||
def _assert_pricing_has_been_updated(self, response):
|
def _assert_pricing_has_been_updated(self, response):
|
||||||
self.assertContains(response, 'Pricing has been updated')
|
self.assertContains(response, 'Pricing has been updated')
|
||||||
|
@ -213,6 +213,7 @@ class TestBillingAddressForm(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
class TestPaymentForm(BaseSubscriptionTestCase):
|
class TestPaymentForm(BaseSubscriptionTestCase):
|
||||||
required_payment_form_data = {
|
required_payment_form_data = {
|
||||||
|
'gateway': 'bank',
|
||||||
'plan_variation_id': 1,
|
'plan_variation_id': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,6 +241,7 @@ class TestPaymentForm(BaseSubscriptionTestCase):
|
|||||||
'country': ['This field is required.'],
|
'country': ['This field is required.'],
|
||||||
'email': ['This field is required.'],
|
'email': ['This field is required.'],
|
||||||
'full_name': ['This field is required.'],
|
'full_name': ['This field is required.'],
|
||||||
|
'gateway': ['This field is required.'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
"""Fill in billing details and initiate Stripe checkout session."""
|
"""Fill in billing details and initiate Stripe checkout session."""
|
||||||
|
|
||||||
# FIXME(anna): this view uses some functionality of CheckoutStripeView,
|
# FIXME(anna): this view uses some functionality of CheckoutStripeView,
|
||||||
# but cannot directly inherit from them, since JoinView supports creating only one subscription.
|
# but cannot directly inherit it, since JoinView supports creating only one subscription.
|
||||||
_fetch_or_create_order = CheckoutStripeView._fetch_or_create_order
|
_fetch_or_create_order = CheckoutStripeView._fetch_or_create_order
|
||||||
|
|
||||||
template_name = 'subscriptions/join/billing_address.html'
|
template_name = 'subscriptions/join/billing_address.html'
|
||||||
@ -75,7 +75,6 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.gateway = looper.models.Gateway.default()
|
|
||||||
self.user = request.user
|
self.user = request.user
|
||||||
self.customer = self.user.customer
|
self.customer = self.user.customer
|
||||||
self.subscription = self._get_existing_subscription()
|
self.subscription = self._get_existing_subscription()
|
||||||
@ -96,13 +95,6 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
)
|
)
|
||||||
return form_kwargs
|
return form_kwargs
|
||||||
|
|
||||||
def get_initial(self) -> dict:
|
|
||||||
"""Prefill default payment gateway, country and selected plan options."""
|
|
||||||
return {
|
|
||||||
**super().get_initial(),
|
|
||||||
'gateway': self.gateway.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs) -> dict:
|
def get_context_data(self, **kwargs) -> dict:
|
||||||
"""Add existing subscription to the view and the context."""
|
"""Add existing subscription to the view and the context."""
|
||||||
return {
|
return {
|
||||||
@ -112,9 +104,8 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def gateway_from_form(self, form) -> looper.models.Gateway:
|
def gateway_from_form(self, form) -> looper.models.Gateway:
|
||||||
"""TODO support the usual bank transfer payments."""
|
"""Use Stripe by default, but allow bank transfer payments for manual plan variations."""
|
||||||
name = 'bank' in form and 'bank' or 'stripe'
|
self.gateway = looper.models.Gateway.objects.get(name=form.cleaned_data['gateway'])
|
||||||
self.gateway = looper.models.Gateway.objects.get(name=name)
|
|
||||||
return self.gateway
|
return self.gateway
|
||||||
|
|
||||||
def _get_or_create_subscription(
|
def _get_or_create_subscription(
|
||||||
@ -125,7 +116,8 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
if not subscription:
|
if not subscription:
|
||||||
subscription = looper.models.Subscription(customer=self.customer)
|
subscription = looper.models.Subscription(customer=self.customer)
|
||||||
is_new = True
|
is_new = True
|
||||||
logger.debug('Creating an new subscription for %s, %s', gateway)
|
args = [self.customer.pk, gateway]
|
||||||
|
logger.debug('Creating a new subscription for customer pk=%s, %s', *args)
|
||||||
collection_method = self.plan_variation.collection_method
|
collection_method = self.plan_variation.collection_method
|
||||||
supported = set(gateway.provider.supported_collection_methods)
|
supported = set(gateway.provider.supported_collection_methods)
|
||||||
if collection_method not in supported:
|
if collection_method not in supported:
|
||||||
@ -163,7 +155,7 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
|
|
||||||
def form_invalid(self, form, *args, **kwargs):
|
def form_invalid(self, form, *args, **kwargs):
|
||||||
"""Temporarily log all validation errors."""
|
"""Temporarily log all validation errors."""
|
||||||
logger.exception('Validation error in ConfirmAndPayView: %s, %s', form.errors, form.data)
|
logger.exception('Validation error in JoinView: %s, %s', form.errors, form.data)
|
||||||
return super().form_invalid(form, *args, **kwargs)
|
return super().form_invalid(form, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
@ -210,6 +202,21 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
messages.warning(self.request, msg)
|
messages.warning(self.request, msg)
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
if not gateway.provider.supports_transactions:
|
||||||
|
logger.info(
|
||||||
|
'Not creating transaction for order pk=%r because gateway %r does '
|
||||||
|
'not support it',
|
||||||
|
order.pk,
|
||||||
|
gateway.name,
|
||||||
|
)
|
||||||
|
url = reverse(
|
||||||
|
'looper:transactionless_checkout_done',
|
||||||
|
kwargs={'pk': order.pk, 'gateway_name': gateway.name},
|
||||||
|
)
|
||||||
|
# Trigger an email with instructions about manual payment:
|
||||||
|
subscription_created_needs_payment.send(sender=subscription)
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
success_url = self.request.build_absolute_uri(
|
success_url = self.request.build_absolute_uri(
|
||||||
reverse(
|
reverse(
|
||||||
'looper:stripe_success',
|
'looper:stripe_success',
|
||||||
@ -221,10 +228,6 @@ class JoinView(LoginRequiredMixin, FormView):
|
|||||||
success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}', 1)
|
success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}', 1)
|
||||||
cancel_url = self.request.build_absolute_uri(self.request.get_full_path())
|
cancel_url = self.request.build_absolute_uri(self.request.get_full_path())
|
||||||
|
|
||||||
if not gateway.provider.supports_transactions:
|
|
||||||
# Trigger an email with instructions about manual payment:
|
|
||||||
subscription_created_needs_payment.send(sender=subscription)
|
|
||||||
|
|
||||||
session = looper.stripe_utils.create_stripe_checkout_session_for_order(
|
session = looper.stripe_utils.create_stripe_checkout_session_for_order(
|
||||||
order,
|
order,
|
||||||
success_url,
|
success_url,
|
||||||
|
@ -39,15 +39,27 @@ full_billing_address_data = {
|
|||||||
# **N.B.**: test cases below require settings.GEOIP2_DB to point to an existing GeoLite2 database.
|
# **N.B.**: test cases below require settings.GEOIP2_DB to point to an existing GeoLite2 database.
|
||||||
|
|
||||||
|
|
||||||
def _get_default_variation(currency='USD'):
|
|
||||||
return looper.models.Plan.objects.first().variation_for_currency(currency)
|
|
||||||
|
|
||||||
|
|
||||||
@freeze_time('2023-05-19 11:41:11')
|
@freeze_time('2023-05-19 11:41:11')
|
||||||
class TestGETJoinView(BaseSubscriptionTestCase):
|
class TestGETJoinView(BaseSubscriptionTestCase):
|
||||||
url_usd = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 1})
|
url_usd = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 1})
|
||||||
url = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 2})
|
url = reverse('subscriptions:join-billing-details', kwargs={'plan_variation_id': 2})
|
||||||
|
|
||||||
|
def test_pay_via_bank_transfer_button_is_shown_for_manual_plan_variation(self):
|
||||||
|
customer = create_customer_with_billing_address(country='NL', full_name='Jane Doe')
|
||||||
|
user = customer.user
|
||||||
|
self.client.force_login(user)
|
||||||
|
|
||||||
|
url, selected_variation = self._get_url_for(
|
||||||
|
currency='EUR',
|
||||||
|
interval_length=1,
|
||||||
|
interval_unit='month',
|
||||||
|
plan__name='Manual renewal',
|
||||||
|
)
|
||||||
|
response = self.client.get(url, REMOTE_ADDR=EURO_IPV4)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self._assert_billing_details_form_with_pay_via_bank_displayed(response)
|
||||||
|
|
||||||
def test_get_prefills_full_name_and_billing_email_from_user(self):
|
def test_get_prefills_full_name_and_billing_email_from_user(self):
|
||||||
user = UserFactory(full_name="Jane До", email='jane.doe@example.com')
|
user = UserFactory(full_name="Jane До", email='jane.doe@example.com')
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
@ -30,7 +30,7 @@ full_billing_address_data = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestSettingsBillingAddress(BaseSubscriptionTestCase):
|
class TestSubscriptionSettingsBillingAddress(BaseSubscriptionTestCase):
|
||||||
url = reverse('subscriptions:billing-address')
|
url = reverse('subscriptions:billing-address')
|
||||||
|
|
||||||
def test_saves_full_billing_address(self):
|
def test_saves_full_billing_address(self):
|
||||||
|
@ -4,6 +4,7 @@ import logging
|
|||||||
from actstream.models import Action
|
from actstream.models import Action
|
||||||
from anymail.signals import tracking
|
from anymail.signals import tracking
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models.signals import pre_save, post_save
|
from django.db.models.signals import pre_save, post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
@ -68,6 +69,44 @@ def _sync_is_subscribed_to_newsletter(sender: object, instance: User, **kwargs):
|
|||||||
tasks.handle_is_subscribed_to_newsletter(pk=instance.pk)
|
tasks.handle_is_subscribed_to_newsletter(pk=instance.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=User)
|
||||||
|
def create_customer(sender, instance: User, created, raw, **kwargs):
|
||||||
|
"""Create Customer on User creation."""
|
||||||
|
from looper.models import Customer
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
customer = instance.customer
|
||||||
|
except Customer.DoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
'Newly created User %d already has a Customer %d, not creating new one',
|
||||||
|
instance.pk,
|
||||||
|
customer.pk,
|
||||||
|
)
|
||||||
|
billing_address = customer.billing_address
|
||||||
|
logger.info('Creating new billing address due to creation of user %s', instance.pk)
|
||||||
|
if not billing_address.pk:
|
||||||
|
billing_address.email = instance.email
|
||||||
|
billing_address.full_name = instance.full_name
|
||||||
|
billing_address.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('Creating new Customer due to creation of user %s', instance.pk)
|
||||||
|
with transaction.atomic():
|
||||||
|
customer = Customer.objects.create(user=instance)
|
||||||
|
billing_address = customer.billing_address
|
||||||
|
billing_address.email = instance.email
|
||||||
|
billing_address.full_name = instance.full_name
|
||||||
|
billing_address.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(tracking)
|
@receiver(tracking)
|
||||||
def _handle_mailgun_tracking_event(sender, event, esp_name, **kwargs):
|
def _handle_mailgun_tracking_event(sender, event, esp_name, **kwargs):
|
||||||
event_type = event.event_type
|
event_type = event.event_type
|
||||||
|
@ -5,8 +5,6 @@ from django.conf import settings
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from common.tests.factories.users import UserFactory
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
@ -145,9 +143,7 @@ def mock_mailgun_responses() -> None:
|
|||||||
|
|
||||||
def create_admin_log_user() -> User:
|
def create_admin_log_user() -> User:
|
||||||
"""Create the admin user used for logging."""
|
"""Create the admin user used for logging."""
|
||||||
admin_user, _ = User.objects.get_or_create(
|
admin_user, _ = User.objects.update_or_create(
|
||||||
id=1, email='admin@blender.studio', is_staff=True, is_superuser=True
|
id=1, defaults={'email': 'admin@blender.studio', 'is_staff': True, 'is_superuser': True}
|
||||||
)
|
)
|
||||||
# Reset ID sequence to avoid clashing with an already used ID 1
|
|
||||||
UserFactory.reset_sequence(100, force=True)
|
|
||||||
return admin_user
|
return admin_user
|
||||||
|
Loading…
Reference in New Issue
Block a user