diff --git a/doc/api/resources/memberships.rst b/doc/api/resources/memberships.rst index 07dbe83ce..0f28bd59e 100644 --- a/doc/api/resources/memberships.rst +++ b/doc/api/resources/memberships.rst @@ -13,6 +13,7 @@ Field Type Description ===================================== ========================== ======================================================= id integer Internal ID of the membership customer string Identifier of the customer associated with this membership (can't be changed) +testmode boolean Whether this is a test membership membership_type integer Internal ID of the membership type date_start datetime Start of validity date_end datetime End of validity @@ -51,6 +52,7 @@ Endpoints "id": 2, "customer": "EGR9SYT", "membership_type": 1, + "testmode": false, "date_start": "2021-04-19T00:00:00+02:00", "date_end": "2021-04-20T00:00:00+02:00", "attendee_name_parts": { @@ -66,6 +68,7 @@ Endpoints :query integer page: The page number in case of a multi-page result set, default is 1 :query string customer: A customer identifier to filter for :query integer membership_type: A membership type ID to filter for + :query boolean testmode: Filter for memberships that are (not) in test mode. :param organizer: The ``slug`` field of the organizer to fetch :statuscode 200: no error :statuscode 401: Authentication failure @@ -95,6 +98,7 @@ Endpoints "id": 2, "customer": "EGR9SYT", "membership_type": 1, + "testmode": false, "date_start": "2021-04-19T00:00:00+02:00", "date_end": "2021-04-20T00:00:00+02:00", "attendee_name_parts": { @@ -127,6 +131,7 @@ Endpoints { "membership_type": 2, "customer": "EGR9SYT", + "testmode": false, "date_start": "2021-04-19T00:00:00+02:00", "date_end": "2021-04-20T00:00:00+02:00", "attendee_name_parts": { @@ -149,6 +154,7 @@ Endpoints "id": 3, "membership_type": 2, "customer": "EGR9SYT", + "testmode": false, "date_start": "2021-04-19T00:00:00+02:00", "date_end": "2021-04-20T00:00:00+02:00", "attendee_name_parts": { @@ -171,7 +177,7 @@ Endpoints the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you want to change. - You can change all fields of the resource except the ``id`` and ``customer`` fields. + You can change all fields of the resource except the ``id``, ``customer``, and ``testmode`` fields. **Example request**: diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index fb56f4f6b..b207bbc57 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -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) diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index 9fd724a42..eae5fccf5 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -598,7 +598,7 @@ with scopes_disabled(): class Meta: model = Membership - fields = ['customer', 'membership_type'] + fields = ['customer', 'membership_type', 'testmode'] class MembershipViewSet(viewsets.ModelViewSet): diff --git a/src/pretix/base/migrations/0189_auto_20210525_1311.py b/src/pretix/base/migrations/0189_auto_20210525_1311.py new file mode 100644 index 000000000..82d3ed227 --- /dev/null +++ b/src/pretix/base/migrations/0189_auto_20210525_1311.py @@ -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), + ), + ] diff --git a/src/pretix/base/models/memberships.py b/src/pretix/base/models/memberships.py index f72880227..c47296eff 100644 --- a/src/pretix/base/models/memberships.py +++ b/src/pretix/base/models/memberships.py @@ -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() diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 5ea12f70d..582736810 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -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) diff --git a/src/pretix/base/services/memberships.py b/src/pretix/base/services/memberships.py index 1f18bb8f9..49d2a4e0b 100644 --- a/src/pretix/base/services/memberships.py +++ b/src/pretix/base/services/memberships.py @@ -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): diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 9a4f38d71..d4596e092 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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) diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index ad10d2b41..5363f053b 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -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, diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 96886de2b..b021b710c 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -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.'), diff --git a/src/pretix/control/templates/pretixcontrol/organizers/customer.html b/src/pretix/control/templates/pretixcontrol/organizers/customer.html index 447d321f8..3767e8157 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/customer.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/customer.html @@ -82,6 +82,7 @@