forked from CGM_Public/pretix_original
Fix #2090 -- Test mode for memberships
This commit is contained in:
@@ -87,7 +87,7 @@ class MembershipSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ('id', 'customer', 'membership_type', 'date_start', 'date_end', 'attendee_name_parts')
|
||||
fields = ('id', 'testmode', 'customer', 'membership_type', 'date_start', 'date_end', 'attendee_name_parts')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -96,6 +96,7 @@ class MembershipSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data['customer'] = instance.customer # no modifying
|
||||
validated_data['testmode'] = instance.testmode # no modifying
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
|
||||
@@ -598,7 +598,7 @@ with scopes_disabled():
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ['customer', 'membership_type']
|
||||
fields = ['customer', 'membership_type', 'testmode']
|
||||
|
||||
|
||||
class MembershipViewSet(viewsets.ModelViewSet):
|
||||
|
||||
26
src/pretix/base/migrations/0189_auto_20210525_1311.py
Normal file
26
src/pretix/base/migrations/0189_auto_20210525_1311.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.3 on 2021-05-25 13:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.event
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0188_delete_requiredaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='membership',
|
||||
name='testmode',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='sales_channels',
|
||||
field=pretix.base.models.fields.MultiStringField(default=pretix.base.models.event.default_sales_channels),
|
||||
),
|
||||
]
|
||||
@@ -114,6 +114,10 @@ class MembershipQuerySetManager(ScopedManager(organizer='customer__organizer')._
|
||||
|
||||
class Membership(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
testmode = models.BooleanField(
|
||||
verbose_name=_('Test mode'),
|
||||
default=False
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
related_name='memberships',
|
||||
@@ -168,3 +172,6 @@ class Membership(models.Model):
|
||||
dt = now()
|
||||
|
||||
return dt >= self.date_start and dt <= self.date_end
|
||||
|
||||
def allow_delete(self):
|
||||
return self.testmode and not self.orderposition_set.exists()
|
||||
|
||||
@@ -258,7 +258,7 @@ class Order(LockModel, LoggedModel):
|
||||
return self.full_code
|
||||
|
||||
def gracefully_delete(self, user=None, auth=None):
|
||||
from . import GiftCard, GiftCardTransaction, Voucher
|
||||
from . import GiftCard, GiftCardTransaction, Membership, Voucher
|
||||
|
||||
if not self.testmode:
|
||||
raise TypeError("Only test mode orders can be deleted.")
|
||||
@@ -280,6 +280,7 @@ class Order(LockModel, LoggedModel):
|
||||
GiftCardTransaction.objects.filter(refund__in=self.refunds.all()).update(refund=None)
|
||||
GiftCardTransaction.objects.filter(order=self).update(order=None)
|
||||
GiftCard.objects.filter(issued_in__in=self.positions.all()).update(issued_in=None)
|
||||
Membership.objects.filter(granted_in__order=self, testmode=True).update(granted_in=None)
|
||||
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
|
||||
OrderPosition.all.filter(order=self).delete()
|
||||
OrderFee.all.filter(order=self).delete()
|
||||
@@ -846,7 +847,7 @@ class Order(LockModel, LoggedModel):
|
||||
try:
|
||||
if check_memberships:
|
||||
try:
|
||||
validate_memberships_in_order(self.customer, positions, self.event, lock=False)
|
||||
validate_memberships_in_order(self.customer, positions, self.event, lock=False, testmode=self.testmode)
|
||||
except ValidationError as e:
|
||||
raise Quota.QuotaExceededException(e.message)
|
||||
|
||||
|
||||
@@ -76,11 +76,12 @@ def create_membership(customer: Customer, position: OrderPosition):
|
||||
granted_in=position,
|
||||
date_start=date_start,
|
||||
date_end=date_end,
|
||||
attendee_name_parts=position.attendee_name_parts
|
||||
attendee_name_parts=position.attendee_name_parts,
|
||||
testmode=position.order.testmode,
|
||||
)
|
||||
|
||||
|
||||
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None):
|
||||
def validate_memberships_in_order(customer: Customer, positions: List[AbstractPosition], event: Event, lock=False, ignored_order: Order = None, testmode=False):
|
||||
"""
|
||||
Validate that a set of cart or order positions. This currently does not validate
|
||||
|
||||
@@ -89,6 +90,7 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
||||
:param event: Event this all is computed in
|
||||
:param lock: Whether to place a SELECT FOR UPDATE lock on the selected memberships
|
||||
:param ignored_order: An order that should be ignored for usage counting
|
||||
:param testmode: If ``True``, only test mode memberships are allowed. If ``False``, test mode memberships are not allowed.
|
||||
"""
|
||||
tz = event.timezone
|
||||
applicable_positions = [
|
||||
@@ -139,6 +141,11 @@ def validate_memberships_in_order(customer: Customer, positions: List[AbstractPo
|
||||
_('You selected a membership that is connected to a different customer account.')
|
||||
)
|
||||
|
||||
if m.testmode != testmode:
|
||||
raise ValidationError(
|
||||
_('You can only use a test mode membership for test mode tickets.')
|
||||
)
|
||||
|
||||
ev = p.subevent or event
|
||||
|
||||
if not m.is_valid(ev):
|
||||
|
||||
@@ -817,7 +817,7 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
raise OrderError(_("You cannot pay with gift cards when buying a gift card."))
|
||||
|
||||
try:
|
||||
validate_memberships_in_order(customer, positions, event, lock=True)
|
||||
validate_memberships_in_order(customer, positions, event, lock=True, testmode=event.testmode)
|
||||
except ValidationError as e:
|
||||
raise OrderError(e.message)
|
||||
|
||||
@@ -2063,7 +2063,7 @@ class OrderChangeManager:
|
||||
)
|
||||
fake_cart.append(cp)
|
||||
try:
|
||||
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order)
|
||||
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
|
||||
except ValidationError as e:
|
||||
raise OrderError(e.message)
|
||||
|
||||
|
||||
@@ -560,7 +560,7 @@ class MembershipUpdateForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ['membership_type', 'date_start', 'date_end', 'attendee_name_parts']
|
||||
fields = ['testmode', 'membership_type', 'date_start', 'date_end', 'attendee_name_parts']
|
||||
field_classes = {
|
||||
'date_start': SplitDateTimeField,
|
||||
'date_end': SplitDateTimeField,
|
||||
@@ -573,6 +573,9 @@ class MembershipUpdateForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance and self.instance.pk:
|
||||
del self.fields['testmode']
|
||||
|
||||
self.fields['membership_type'].queryset = self.instance.customer.organizer.membership_types.all()
|
||||
self.fields['attendee_name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
|
||||
@@ -325,6 +325,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.customer.changed': _('The account has been changed.'),
|
||||
'pretix.customer.membership.created': _('A membership for this account has been added.'),
|
||||
'pretix.customer.membership.changed': _('A membership of this account has been changed.'),
|
||||
'pretix.customer.membership.deleted': _('A membership of this account has been deleted.'),
|
||||
'pretix.customer.anonymized': _('The account has been disabled and anonymized.'),
|
||||
'pretix.customer.password.resetrequested': _('A new password has been requested.'),
|
||||
'pretix.customer.password.set': _('A new password has been set.'),
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
{{ m.membership_type.name }}
|
||||
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
@@ -118,6 +119,14 @@
|
||||
class="btn btn-default">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% if m.testmode %}
|
||||
<a href="{% url "control:organizer.customer.membership.delete" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
|
||||
data-toggle="tooltip"
|
||||
title="{% trans "Delete" %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Membership" %}
|
||||
{% if membership.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||
</h1>
|
||||
<div class="panel panel-primary items">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% trans "Membership" %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% trans "Membership" %}
|
||||
{% if membership.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||
</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if is_allowed %}
|
||||
<p>{% blocktrans %}Are you sure you want to delete this membership?{% endblocktrans %}
|
||||
{% else %}
|
||||
<p>{% blocktrans %}This membership cannot be deleted since it has been used in an order. Change its end date to the past instead.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=membership.customer.identifier %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
{% if is_allowed %}
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -141,6 +141,8 @@ urlpatterns = [
|
||||
organizer.MembershipCreateView.as_view(), name='organizer.customer.membership.add'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/membership/(?P<id>[^/]+)/edit$',
|
||||
organizer.MembershipUpdateView.as_view(), name='organizer.customer.membership.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/membership/(?P<id>[^/]+)/delete$',
|
||||
organizer.MembershipDeleteView.as_view(), name='organizer.customer.membership.delete'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/customer/(?P<customer>[^/]+)/anonymize$',
|
||||
organizer.CustomerAnonymizeView.as_view(), name='organizer.customer.anonymize'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/giftcards$', organizer.GiftCardListView.as_view(), name='organizer.giftcards'),
|
||||
|
||||
@@ -535,6 +535,7 @@ class OrderDelete(OrderView):
|
||||
'organizer': self.request.organizer.slug,
|
||||
}))
|
||||
except ProtectedError:
|
||||
logger.exception('Could not delete order')
|
||||
messages.error(self.request, _('The order could not be deleted as some constraints (e.g. data created '
|
||||
'by plug-ins) do not allow it.'))
|
||||
return self.get(self.request, *self.args, **self.kwargs)
|
||||
|
||||
@@ -1887,6 +1887,45 @@ class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequired
|
||||
})
|
||||
|
||||
|
||||
class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
|
||||
template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
|
||||
permission = 'can_manage_customers'
|
||||
context_object_name = 'membership'
|
||||
form_class = MembershipUpdateForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(
|
||||
Membership,
|
||||
customer__organizer=self.request.organizer,
|
||||
customer__identifier=self.kwargs.get('customer'),
|
||||
testmode=True,
|
||||
pk=self.kwargs.get('id')
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['is_allowed'] = self.object.allow_delete()
|
||||
return ctx
|
||||
|
||||
@transaction.atomic
|
||||
def delete(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.customer = self.object.customer
|
||||
success_url = self.get_success_url()
|
||||
if self.object.allow_delete():
|
||||
self.object.cartposition_set.all().delete()
|
||||
self.object.customer.log_action('pretix.customer.membership.deleted', user=self.request.user)
|
||||
self.object.delete()
|
||||
messages.success(request, _('The selected object has been deleted.'))
|
||||
return redirect(success_url)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.customer', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'customer': self.customer.identifier,
|
||||
})
|
||||
|
||||
|
||||
class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
template_name = 'pretixcontrol/organizers/customer_membership.html'
|
||||
permission = 'can_manage_customers'
|
||||
|
||||
@@ -395,7 +395,7 @@ class MembershipStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
f.position.used_membership = f.cleaned_data['membership']
|
||||
|
||||
try:
|
||||
validate_memberships_in_order(self.cart_customer, self.positions, self.request.event, lock=False)
|
||||
validate_memberships_in_order(self.cart_customer, self.positions, self.request.event, lock=False, testmode=self.request.event.testmode)
|
||||
except ValidationError as e:
|
||||
messages.error(self.request, e.message)
|
||||
self.render()
|
||||
|
||||
@@ -227,11 +227,13 @@ class MembershipForm(forms.Form):
|
||||
usages = f'({obj.usages} / {obj.membership_type.max_usages})'
|
||||
else:
|
||||
usages = ''
|
||||
return mark_safe(
|
||||
f'<strong>{escape(obj.membership_type)}</strong> {usages}<br>'
|
||||
f'{escape(obj.attendee_name)}<br>'
|
||||
f'<span class="text-muted">{ds} – {de}</span>'
|
||||
)
|
||||
d = f'<strong>{escape(obj.membership_type)}</strong> {usages}<br>'
|
||||
if obj.attendee_name:
|
||||
d += f'{escape(obj.attendee_name)}<br>'
|
||||
d += f'<span class="text-muted">{ds} – {de}</span>'
|
||||
if obj.testmode:
|
||||
d += ' <span class="label label-warning">{}</span>'.format(_("TEST MODE"))
|
||||
return mark_safe(d)
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
{{ m.membership_type.name }}
|
||||
{% if m.testmode %}<span class="label label-warning">{% trans "TEST MODE" %}</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ m.date_start|date:"SHORT_DATETIME_FORMAT" }}
|
||||
|
||||
@@ -68,6 +68,7 @@ TEST_MEMBERSHIP_RES = {
|
||||
"customer": "8WSAJCJ",
|
||||
"date_start": "2021-04-01T00:00:00Z",
|
||||
"date_end": "2021-04-08T23:59:59.999999Z",
|
||||
"testmode": False,
|
||||
"attendee_name_parts": {
|
||||
"_scheme": "given_family",
|
||||
'given_name': 'John',
|
||||
@@ -136,11 +137,13 @@ def test_membership_patch(token_client, organizer, customer, membership):
|
||||
format='json',
|
||||
data={
|
||||
"customer": other_customer.identifier,
|
||||
"testmode": True,
|
||||
}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
membership.refresh_from_db()
|
||||
assert membership.customer == customer # change is ignored
|
||||
assert not membership.testmode # change is ignored
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -227,6 +227,44 @@ def test_validate_membership_ensure_locking(event, customer, membership, requiri
|
||||
assert any('FOR UPDATE' in s['sql'] for s in captured)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_validate_membership_test_mode(event, customer, membership, requiring_ticket, membership_type):
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
membership.testmode = True
|
||||
membership.save()
|
||||
validate_memberships_in_order(
|
||||
customer,
|
||||
[
|
||||
CartPosition(
|
||||
item=requiring_ticket,
|
||||
used_membership=membership
|
||||
)
|
||||
],
|
||||
event,
|
||||
lock=False,
|
||||
ignored_order=None,
|
||||
testmode=False,
|
||||
)
|
||||
assert "test mode" in str(excinfo.value)
|
||||
with pytest.raises(ValidationError) as excinfo:
|
||||
membership.testmode = False
|
||||
membership.save()
|
||||
validate_memberships_in_order(
|
||||
customer,
|
||||
[
|
||||
CartPosition(
|
||||
item=requiring_ticket,
|
||||
used_membership=membership
|
||||
)
|
||||
],
|
||||
event,
|
||||
lock=False,
|
||||
ignored_order=None,
|
||||
testmode=True,
|
||||
)
|
||||
assert "test mode" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_validate_membership_wrong_customer(event, customer, membership, requiring_ticket, membership_type):
|
||||
customer2 = event.organizer.customers.create(email="doe@example.org")
|
||||
|
||||
Reference in New Issue
Block a user