forked from CGM_Public/pretix_original
Allow to enter gift cards into the voucher input (Z#23171961) (#4670)
This commit is contained in:
@@ -1419,50 +1419,51 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
def payment_refund_supported(self, payment: OrderPayment) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
def _add_giftcard_to_cart(self, cs, gc):
|
||||||
from pretix.base.services.cart import add_payment_to_cart
|
from pretix.base.services.cart import add_payment_to_cart_session
|
||||||
|
|
||||||
|
if gc.currency != self.event.currency:
|
||||||
|
raise ValidationError(_("This gift card does not support this currency."))
|
||||||
|
if gc.testmode and not self.event.testmode:
|
||||||
|
raise ValidationError(_("This gift card can only be used in test mode."))
|
||||||
|
if not gc.testmode and self.event.testmode:
|
||||||
|
raise ValidationError(_("Only test gift cards can be used in test mode."))
|
||||||
|
if gc.expires and gc.expires < time_machine_now():
|
||||||
|
raise ValidationError(_("This gift card is no longer valid."))
|
||||||
|
if gc.value <= Decimal("0.00"):
|
||||||
|
raise ValidationError(_("All credit on this gift card has been used."))
|
||||||
|
|
||||||
|
for p in cs.get('payments', []):
|
||||||
|
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
||||||
|
raise ValidationError(_("This gift card is already used for your payment."))
|
||||||
|
|
||||||
|
add_payment_to_cart_session(
|
||||||
|
cs,
|
||||||
|
self,
|
||||||
|
max_value=gc.value,
|
||||||
|
info_data={
|
||||||
|
'gift_card': gc.pk,
|
||||||
|
'gift_card_secret': gc.secret,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def checkout_prepare(self, request: HttpRequest, cart: Dict[str, Any]) -> Union[bool, str, None]:
|
||||||
for p in get_cart(request):
|
for p in get_cart(request):
|
||||||
if p.item.issue_giftcard:
|
if p.item.issue_giftcard:
|
||||||
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
messages.error(request, _("You cannot pay with gift cards when buying a gift card."))
|
||||||
return
|
return
|
||||||
|
|
||||||
cs = cart_session(request)
|
|
||||||
try:
|
try:
|
||||||
gc = self.event.organizer.accepted_gift_cards.get(
|
gc = self.event.organizer.accepted_gift_cards.get(
|
||||||
secret=request.POST.get("giftcard").strip()
|
secret=request.POST.get("giftcard").strip()
|
||||||
)
|
)
|
||||||
if gc.currency != self.event.currency:
|
cs = cart_session(request)
|
||||||
messages.error(request, _("This gift card does not support this currency."))
|
try:
|
||||||
|
self._add_giftcard_to_cart(cs, gc)
|
||||||
|
return True
|
||||||
|
except ValidationError as e:
|
||||||
|
messages.error(request, str(e.message))
|
||||||
return
|
return
|
||||||
if gc.testmode and not self.event.testmode:
|
|
||||||
messages.error(request, _("This gift card can only be used in test mode."))
|
|
||||||
return
|
|
||||||
if not gc.testmode and self.event.testmode:
|
|
||||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
|
||||||
return
|
|
||||||
if gc.expires and gc.expires < time_machine_now():
|
|
||||||
messages.error(request, _("This gift card is no longer valid."))
|
|
||||||
return
|
|
||||||
if gc.value <= Decimal("0.00"):
|
|
||||||
messages.error(request, _("All credit on this gift card has been used."))
|
|
||||||
return
|
|
||||||
|
|
||||||
for p in cs.get('payments', []):
|
|
||||||
if p['provider'] == self.identifier and p['info_data']['gift_card'] == gc.pk:
|
|
||||||
messages.error(request, _("This gift card is already used for your payment."))
|
|
||||||
return
|
|
||||||
|
|
||||||
add_payment_to_cart(
|
|
||||||
request,
|
|
||||||
self,
|
|
||||||
max_value=gc.value,
|
|
||||||
info_data={
|
|
||||||
'gift_card': gc.pk,
|
|
||||||
'gift_card_secret': gc.secret,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
except GiftCard.DoesNotExist:
|
except GiftCard.DoesNotExist:
|
||||||
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
if self.event.vouchers.filter(code__iexact=request.POST.get("giftcard")).exists():
|
||||||
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
messages.warning(request, _("You entered a voucher instead of a gift card. Vouchers can only be entered on the first page of the shop below "
|
||||||
|
|||||||
@@ -1426,6 +1426,28 @@ class CartManager:
|
|||||||
raise CartError(err)
|
raise CartError(err)
|
||||||
|
|
||||||
|
|
||||||
|
def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||||
|
"""
|
||||||
|
:param cart_session: The current cart session.
|
||||||
|
:param provider: The instance of your payment provider.
|
||||||
|
:param min_value: The minimum value this payment instrument supports, or ``None`` for unlimited.
|
||||||
|
:param max_value: The maximum value this payment instrument supports, or ``None`` for unlimited. Highly discouraged
|
||||||
|
to use for payment providers which charge a payment fee, as this can be very user-unfriendly if
|
||||||
|
users need a second payment method just for the payment fee of the first method.
|
||||||
|
:param info_data: A dictionary of information that will be passed through to the ``OrderPayment.info_data`` attribute.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
cart_session.setdefault('payments', [])
|
||||||
|
cart_session['payments'].append({
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'provider': provider.identifier,
|
||||||
|
'multi_use_supported': provider.multi_use_supported,
|
||||||
|
'min_value': str(min_value) if min_value is not None else None,
|
||||||
|
'max_value': str(max_value) if max_value is not None else None,
|
||||||
|
'info_data': info_data or {},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||||
"""
|
"""
|
||||||
:param request: The current HTTP request context.
|
:param request: The current HTTP request context.
|
||||||
@@ -1440,16 +1462,7 @@ def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: D
|
|||||||
from pretix.presale.views.cart import cart_session
|
from pretix.presale.views.cart import cart_session
|
||||||
|
|
||||||
cs = cart_session(request)
|
cs = cart_session(request)
|
||||||
cs.setdefault('payments', [])
|
add_payment_to_cart_session(cs, provider, min_value, max_value, info_data)
|
||||||
|
|
||||||
cs['payments'].append({
|
|
||||||
'id': str(uuid.uuid4()),
|
|
||||||
'provider': provider.identifier,
|
|
||||||
'multi_use_supported': provider.multi_use_supported,
|
|
||||||
'min_value': str(min_value) if min_value is not None else None,
|
|
||||||
'max_value': str(max_value) if max_value is not None else None,
|
|
||||||
'info_data': info_data or {},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def get_fees(event, request, total, invoice_address, payments, positions):
|
def get_fees(event, request, total, invoice_address, payments, positions):
|
||||||
|
|||||||
@@ -10,18 +10,41 @@
|
|||||||
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
|
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
|
||||||
<strong>{% trans "Your cart" %}</strong>
|
<strong>{% trans "Your cart" %}</strong>
|
||||||
</span>
|
</span>
|
||||||
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" aria-hidden="true">
|
{% if cart.positions %}
|
||||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
<strong id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" aria-hidden="true">
|
||||||
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||||
{% else %}
|
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
||||||
{% trans "Cart expired" %}
|
{% else %}
|
||||||
{% endif %}
|
{% trans "Cart expired" %}
|
||||||
</strong>
|
{% endif %}
|
||||||
|
</strong>
|
||||||
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
</summary>
|
</summary>
|
||||||
<div>
|
<div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
|
{% if cart.positions %}
|
||||||
|
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=True %}
|
||||||
|
{% endif %}
|
||||||
|
{% if cart.current_selected_payments %}
|
||||||
|
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||||
|
<div class="list-group">
|
||||||
|
{% for p in cart.current_selected_payments %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-9">
|
||||||
|
{{ p.provider_name }}
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-3 text-right">
|
||||||
|
{% if p.payment_amount %}
|
||||||
|
{{ p.payment_amount|money:request.event.currency }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="checkout-button-row">
|
<div class="checkout-button-row">
|
||||||
<form class="checkout-button-primary" method="get" action="{% eventurl request.event "presale:event.checkout.start" cart_namespace=cart_namespace %}">
|
<form class="checkout-button-primary" method="get" action="{% eventurl request.event "presale:event.checkout.start" cart_namespace=cart_namespace %}">
|
||||||
<p><button class="btn btn-primary btn-lg" type="submit"{% if has_addon_choices or cart.total == 0 %} aria-label="{% trans "Continue with order process" %}"{% endif %}>
|
<p><button class="btn btn-primary btn-lg" type="submit"{% if has_addon_choices or cart.total == 0 %} aria-label="{% trans "Continue with order process" %}"{% endif %}>
|
||||||
|
|||||||
@@ -251,7 +251,8 @@ class CartMixin:
|
|||||||
'seconds_left': seconds_left,
|
'seconds_left': seconds_left,
|
||||||
'first_expiry': first_expiry,
|
'first_expiry': first_expiry,
|
||||||
'is_ordered': bool(order),
|
'is_ordered': bool(order),
|
||||||
'itemcount': sum(c.count for c in positions if not c.addon_to)
|
'itemcount': sum(c.count for c in positions if not c.addon_to),
|
||||||
|
'current_selected_payments': [p for p in self.current_selected_payments(total) if p.get('multi_use_supported')]
|
||||||
}
|
}
|
||||||
|
|
||||||
def current_selected_payments(self, total, warn=False, total_includes_payment_fees=False):
|
def current_selected_payments(self, total, warn=False, total_includes_payment_fees=False):
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from urllib.parse import quote
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import FileResponse, Http404, JsonResponse
|
from django.http import FileResponse, Http404, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
@@ -57,7 +58,7 @@ from django.views.generic import TemplateView, View
|
|||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CartPosition, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
|
CartPosition, GiftCard, InvoiceAddress, QuestionAnswer, SubEvent, Voucher,
|
||||||
)
|
)
|
||||||
from pretix.base.services.cart import (
|
from pretix.base.services.cart import (
|
||||||
CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages,
|
CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages,
|
||||||
@@ -438,8 +439,48 @@ class CartApplyVoucher(EventViewMixin, CartActionMixin, AsyncAction, View):
|
|||||||
return _('We applied the voucher to as many products in your cart as we could.')
|
return _('We applied the voucher to as many products in your cart as we could.')
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
from pretix.base.payment import GiftCardPayment
|
||||||
|
|
||||||
if 'voucher' in request.POST:
|
if 'voucher' in request.POST:
|
||||||
return self.do(self.request.event.id, request.POST.get('voucher'), get_or_create_cart_id(self.request),
|
code = request.POST.get('voucher').strip()
|
||||||
|
|
||||||
|
if not self.request.event.vouchers.filter(code__iexact=code):
|
||||||
|
try:
|
||||||
|
gc = self.request.event.organizer.accepted_gift_cards.get(secret=code)
|
||||||
|
gcp = GiftCardPayment(self.request.event)
|
||||||
|
if not gcp.is_enabled or not gcp.is_allowed(self.request, Decimal("1.00")):
|
||||||
|
raise ValidationError(error_messages['voucher_invalid'])
|
||||||
|
else:
|
||||||
|
cs = cart_session(request)
|
||||||
|
gcp._add_giftcard_to_cart(cs, gc)
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The gift card has been saved to your cart. Please continue your checkout.")
|
||||||
|
)
|
||||||
|
if "ajax" in self.request.POST or "ajax" in self.request.GET:
|
||||||
|
return JsonResponse({
|
||||||
|
'ready': True,
|
||||||
|
'success': True,
|
||||||
|
'redirect': self.get_success_url(),
|
||||||
|
'message': str(
|
||||||
|
_("The gift card has been saved to your cart. Please continue your checkout.")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return redirect_to_url(self.get_success_url())
|
||||||
|
except GiftCard.DoesNotExist:
|
||||||
|
pass
|
||||||
|
except ValidationError as e:
|
||||||
|
messages.error(self.request, str(e.message))
|
||||||
|
if "ajax" in self.request.POST or "ajax" in self.request.GET:
|
||||||
|
return JsonResponse({
|
||||||
|
'ready': True,
|
||||||
|
'success': False,
|
||||||
|
'redirect': self.get_success_url(),
|
||||||
|
'message': str(e.message)
|
||||||
|
})
|
||||||
|
return redirect_to_url(self.get_error_url())
|
||||||
|
|
||||||
|
return self.do(self.request.event.id, code, get_or_create_cart_id(self.request),
|
||||||
translation.get_language(), request.sales_channel.identifier,
|
translation.get_language(), request.sales_channel.identifier,
|
||||||
time_machine_now(default=None))
|
time_machine_now(default=None))
|
||||||
else:
|
else:
|
||||||
@@ -631,6 +672,8 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
from pretix.base.payment import GiftCardPayment
|
||||||
|
|
||||||
err = None
|
err = None
|
||||||
v = request.GET.get('voucher')
|
v = request.GET.get('voucher')
|
||||||
|
|
||||||
@@ -653,10 +696,24 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
|
|||||||
if v_avail < 1 and not err:
|
if v_avail < 1 and not err:
|
||||||
err = error_messages['voucher_redeemed_cart'] % self.request.event.settings.reservation_time
|
err = error_messages['voucher_redeemed_cart'] % self.request.event.settings.reservation_time
|
||||||
except Voucher.DoesNotExist:
|
except Voucher.DoesNotExist:
|
||||||
if self.request.event.organizer.accepted_gift_cards.filter(secret__iexact=request.GET.get("voucher")).exists():
|
try:
|
||||||
err = error_messages['gift_card']
|
gc = self.request.event.organizer.accepted_gift_cards.get(secret=v.strip())
|
||||||
else:
|
gcp = GiftCardPayment(self.request.event)
|
||||||
|
if not gcp.is_enabled or not gcp.is_allowed(self.request, Decimal("1.00")):
|
||||||
|
err = error_messages['voucher_invalid']
|
||||||
|
else:
|
||||||
|
cs = cart_session(request)
|
||||||
|
gcp._add_giftcard_to_cart(cs, gc)
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The gift card has been saved to your cart. Please now select the products "
|
||||||
|
"you want to purchase.")
|
||||||
|
)
|
||||||
|
return redirect_to_url(self.get_next_url())
|
||||||
|
except GiftCard.DoesNotExist:
|
||||||
err = error_messages['voucher_invalid']
|
err = error_messages['voucher_invalid']
|
||||||
|
except ValidationError as e:
|
||||||
|
err = str(e.message)
|
||||||
else:
|
else:
|
||||||
context = {}
|
context = {}
|
||||||
context['cart'] = self.get_cart()
|
context['cart'] = self.get_cart()
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
|||||||
context['subevent_list_cache_key'] = self._subevent_list_cachekey()
|
context['subevent_list_cache_key'] = self._subevent_list_cachekey()
|
||||||
|
|
||||||
context['show_cart'] = (
|
context['show_cart'] = (
|
||||||
context['cart']['positions'] and (
|
(context['cart']['positions'] or context['cart'].get('current_selected_payments')) and (
|
||||||
self.request.event.has_subevents or self.request.event.presale_is_running
|
self.request.event.has_subevents or self.request.event.presale_is_running
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2306,6 +2306,25 @@ class CartTest(CartTestMixin, TestCase):
|
|||||||
assert cp1.voucher is None
|
assert cp1.voucher is None
|
||||||
assert cp2.voucher is None
|
assert cp2.voucher is None
|
||||||
|
|
||||||
|
def test_voucher_apply_is_a_giftcard(self):
|
||||||
|
with scopes_disabled():
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||||
|
price=23, listed_price=23, price_after_voucher=23, expires=now() + timedelta(minutes=10)
|
||||||
|
)
|
||||||
|
CartPosition.objects.create(
|
||||||
|
event=self.event, cart_id=self.session_key, item=self.shirt, variation=self.shirt_blue,
|
||||||
|
price=8, expires=now() + timedelta(minutes=10),
|
||||||
|
)
|
||||||
|
gc = self.orga.issued_gift_cards.create(secret="GIFTCARD", currency=self.event.currency)
|
||||||
|
gc.transactions.create(value=Decimal("12.24"), acceptor=self.orga)
|
||||||
|
|
||||||
|
html = self.client.post('/%s/%s/cart/voucher' % (self.orga.slug, self.event.slug), {
|
||||||
|
'voucher': 'GIFTCARD',
|
||||||
|
}, follow=True)
|
||||||
|
assert "alert-success" in html.rendered_content
|
||||||
|
assert "€12.24" in html.rendered_content
|
||||||
|
|
||||||
def test_discount(self):
|
def test_discount(self):
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
Discount.objects.create(event=self.event, condition_min_count=2, benefit_discount_matching_percent=20,
|
Discount.objects.create(event=self.event, condition_min_count=2, benefit_discount_matching_percent=20,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||||
|
from pretix.testutils.sessions import get_cart_session_key
|
||||||
|
|
||||||
|
|
||||||
class EventTestMixin:
|
class EventTestMixin:
|
||||||
@@ -1003,6 +1004,25 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
|
|||||||
assert 'name="variation_%d_%d' % (self.item.pk, var1.pk) not in html.rendered_content
|
assert 'name="variation_%d_%d' % (self.item.pk, var1.pk) not in html.rendered_content
|
||||||
assert 'name="variation_%d_%d' % (self.item.pk, var2.pk) not in html.rendered_content
|
assert 'name="variation_%d_%d' % (self.item.pk, var2.pk) not in html.rendered_content
|
||||||
|
|
||||||
|
def test_voucher_is_a_gift_card(self):
|
||||||
|
gc = self.orga.issued_gift_cards.create(secret="GIFTCARD", currency=self.event.currency)
|
||||||
|
gc.transactions.create(value=Decimal("12.00"), acceptor=self.orga)
|
||||||
|
|
||||||
|
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, 'GIFTCARD'), follow=True)
|
||||||
|
assert "alert-success" in html.rendered_content
|
||||||
|
assert "€12.00" in html.rendered_content
|
||||||
|
|
||||||
|
payments = self.client.session['carts'][get_cart_session_key(self.client, self.event)]["payments"]
|
||||||
|
assert payments[0]["info_data"]["gift_card_secret"] == "GIFTCARD"
|
||||||
|
|
||||||
|
def test_voucher_is_a_gift_card_but_invalid(self):
|
||||||
|
gc = self.orga.issued_gift_cards.create(secret="GIFTCARD", currency=self.event.currency, expires=now() - datetime.timedelta(days=1))
|
||||||
|
gc.transactions.create(value=Decimal("12.00"), acceptor=self.orga)
|
||||||
|
|
||||||
|
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, 'GIFTCARD'), follow=True)
|
||||||
|
assert "alert-danger" in html.rendered_content
|
||||||
|
assert "This gift card is no longer valid" in html.rendered_content
|
||||||
|
|
||||||
|
|
||||||
class WaitingListTest(EventTestMixin, SoupTest):
|
class WaitingListTest(EventTestMixin, SoupTest):
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
|
|||||||
Reference in New Issue
Block a user