mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Customer accounts & Memberships (#2024)
This commit is contained in:
168
src/pretix/base/models/memberships.py
Normal file
168
src/pretix/base/models/memberships.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.db import models
|
||||
from django.db.models import Count, OuterRef, Subquery, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from i18nfield.fields import I18nCharField
|
||||
from jsonfallback.fields import FallbackJSONField
|
||||
|
||||
from pretix.base.models import Customer
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
|
||||
class MembershipType(LoggedModel):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
organizer = models.ForeignKey(Organizer, related_name='membership_types', on_delete=models.CASCADE)
|
||||
name = I18nCharField(
|
||||
verbose_name=_('Name'),
|
||||
)
|
||||
transferable = models.BooleanField(
|
||||
verbose_name=_('Membership is transferable'),
|
||||
help_text=_('If this is selected, the membership can be used to purchase tickets for multiple persons. If not, '
|
||||
'the attendee name always needs to stay the same.'),
|
||||
default=False
|
||||
)
|
||||
allow_parallel_usage = models.BooleanField(
|
||||
verbose_name=_('Parallel usage is allowed'),
|
||||
help_text=_('If this is selected, the membership can be used to purchase tickets for events happening at the same time. Note '
|
||||
'that this will only check for an identical start time of the events, not for any overlap between events.'),
|
||||
default=False
|
||||
)
|
||||
max_usages = models.PositiveIntegerField(
|
||||
verbose_name=_("Maximum usages"),
|
||||
help_text=_("Number of times this membership can be used in a purchase."),
|
||||
null=True, blank=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def allow_delete(self):
|
||||
return not self.memberships.exists() and not self.granted_by.exists()
|
||||
|
||||
|
||||
class MembershipQuerySet(models.QuerySet):
|
||||
|
||||
@scopes_disabled() # no scoping of subquery
|
||||
def with_usages(self, ignored_order=None):
|
||||
from . import Order, OrderPosition
|
||||
|
||||
sq = OrderPosition.all.filter(
|
||||
used_membership_id=OuterRef('pk'),
|
||||
canceled=False,
|
||||
).exclude(
|
||||
order__status=Order.STATUS_CANCELED
|
||||
)
|
||||
if ignored_order:
|
||||
sq = sq.exclude(order__id=ignored_order.pk)
|
||||
return self.annotate(
|
||||
usages=Coalesce(
|
||||
Subquery(
|
||||
sq.order_by().values('used_membership_id').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
),
|
||||
Value('0')
|
||||
)
|
||||
)
|
||||
|
||||
def active(self, ev):
|
||||
return self.filter(
|
||||
date_start__lte=ev.date_from,
|
||||
date_end__gte=ev.date_from
|
||||
)
|
||||
|
||||
|
||||
class MembershipQuerySetManager(ScopedManager(organizer='customer__organizer').__class__):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._queryset_class = MembershipQuerySet
|
||||
|
||||
def with_usages(self, ignored_order=None):
|
||||
return self.get_queryset().with_usages(ignored_order)
|
||||
|
||||
def active(self, ev):
|
||||
return self.get_queryset().active(ev)
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
related_name='memberships',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
membership_type = models.ForeignKey(
|
||||
MembershipType,
|
||||
verbose_name=_('Membership type'),
|
||||
related_name='memberships',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
granted_in = models.ForeignKey(
|
||||
'OrderPosition',
|
||||
related_name='granted_memberships',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True,
|
||||
)
|
||||
date_start = models.DateTimeField(
|
||||
verbose_name=_('Start date')
|
||||
)
|
||||
date_end = models.DateTimeField(
|
||||
verbose_name=_('End date')
|
||||
)
|
||||
attendee_name_parts = FallbackJSONField(default=dict, null=True)
|
||||
|
||||
objects = MembershipQuerySetManager()
|
||||
|
||||
class Meta:
|
||||
ordering = "-date_end", "-date_start", "membership_type"
|
||||
|
||||
def __str__(self):
|
||||
ds = date_format(self.date_start, 'SHORT_DATE_FORMAT')
|
||||
de = date_format(self.date_end, 'SHORT_DATE_FORMAT')
|
||||
return f'{self.membership_type.name}: {self.attendee_name} ({ds} – {de})'
|
||||
|
||||
@property
|
||||
def attendee_name(self):
|
||||
if not self.attendee_name_parts:
|
||||
return None
|
||||
if '_legacy' in self.attendee_name_parts:
|
||||
return self.attendee_name_parts['_legacy']
|
||||
if '_scheme' in self.attendee_name_parts:
|
||||
scheme = PERSON_NAME_SCHEMES[self.attendee_name_parts['_scheme']]
|
||||
else:
|
||||
scheme = PERSON_NAME_SCHEMES[self.customer.organizer.settings.name_scheme]
|
||||
return scheme['concatenation'](self.attendee_name_parts).strip()
|
||||
|
||||
def is_valid(self, ev=None):
|
||||
if ev:
|
||||
dt = ev.date_from
|
||||
else:
|
||||
dt = now()
|
||||
|
||||
return dt >= self.date_start and dt <= self.date_end
|
||||
Reference in New Issue
Block a user