mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Customer accounts & Memberships (#2024)
This commit is contained in:
@@ -23,6 +23,7 @@ from ..settings import GlobalSettingsObject_SettingsStore
|
||||
from .auth import U2FDevice, User, WebAuthnDevice
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin, CheckinList
|
||||
from .customers import Customer
|
||||
from .devices import Device, Gate
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
@@ -36,6 +37,7 @@ from .items import (
|
||||
SubEventItemVariation, itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .memberships import Membership, MembershipType
|
||||
from .notifications import NotificationSetting
|
||||
from .orders import (
|
||||
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
|
||||
|
||||
173
src/pretix/base/models/customers.py
Normal file
173
src/pretix/base/models/customers.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#
|
||||
# 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.conf import settings
|
||||
from django.contrib.auth.hashers import (
|
||||
check_password, is_password_usable, make_password,
|
||||
)
|
||||
from django.db import models
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from jsonfallback.fields import FallbackJSONField
|
||||
|
||||
from pretix.base.banlist import banned
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.organizer import Organizer
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
|
||||
|
||||
class Customer(LoggedModel):
|
||||
"""
|
||||
Represents a registered customer of an organizer.
|
||||
"""
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
organizer = models.ForeignKey(Organizer, related_name='customers', on_delete=models.CASCADE)
|
||||
identifier = models.CharField(max_length=190, db_index=True, unique=True)
|
||||
email = models.EmailField(db_index=True, null=True, blank=False, verbose_name=_('E-mail'), max_length=190)
|
||||
password = models.CharField(verbose_name=_('Password'), max_length=128)
|
||||
name_cached = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||
name_parts = FallbackJSONField(default=dict)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Account active'))
|
||||
is_verified = models.BooleanField(default=True, verbose_name=_('Verified email address'))
|
||||
last_login = models.DateTimeField(verbose_name=_('Last login'), blank=True, null=True)
|
||||
date_joined = models.DateTimeField(auto_now_add=True, verbose_name=_('Registration date'))
|
||||
locale = models.CharField(max_length=50,
|
||||
choices=settings.LANGUAGES,
|
||||
default=settings.LANGUAGE_CODE,
|
||||
verbose_name=_('Language'))
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = ScopedManager(organizer='organizer')
|
||||
|
||||
class Meta:
|
||||
unique_together = [['organizer', 'email']]
|
||||
|
||||
def save(self, **kwargs):
|
||||
if self.email:
|
||||
self.email = self.email.lower()
|
||||
if 'update_fields' in kwargs and 'last_modified' not in kwargs['update_fields']:
|
||||
kwargs['update_fields'] = list(kwargs['update_fields']) + ['last_modified']
|
||||
if not self.identifier:
|
||||
self.assign_identifier()
|
||||
if self.name_parts:
|
||||
self.name_cached = self.name
|
||||
else:
|
||||
self.name_cached = ""
|
||||
self.name_parts = {}
|
||||
super().save(**kwargs)
|
||||
|
||||
def anonymize(self):
|
||||
self.is_active = False
|
||||
self.is_verified = False
|
||||
self.name_parts = {}
|
||||
self.name_cached = ''
|
||||
self.email = None
|
||||
self.save()
|
||||
self.all_logentries().update(data={}, shredded=True)
|
||||
self.orders.all().update(customer=None)
|
||||
self.memberships.all().update(attendee_name_parts=None)
|
||||
|
||||
@scopes_disabled()
|
||||
def assign_identifier(self):
|
||||
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ23456789')
|
||||
iteration = 0
|
||||
length = settings.ENTROPY['customer_identifier']
|
||||
while True:
|
||||
code = get_random_string(length=length, allowed_chars=charset)
|
||||
iteration += 1
|
||||
|
||||
if banned(code):
|
||||
continue
|
||||
|
||||
if not Customer.objects.filter(identifier=code).exists():
|
||||
self.identifier = code
|
||||
return
|
||||
|
||||
if iteration > 20:
|
||||
# Safeguard: If we don't find an unused and non-banlisted code within 20 iterations, we increase
|
||||
# the length.
|
||||
length += 1
|
||||
iteration = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if not self.name_parts:
|
||||
return ""
|
||||
if '_legacy' in self.name_parts:
|
||||
return self.name_parts['_legacy']
|
||||
if '_scheme' in self.name_parts:
|
||||
scheme = PERSON_NAME_SCHEMES[self.name_parts['_scheme']]
|
||||
else:
|
||||
raise TypeError("Invalid name given.")
|
||||
return scheme['concatenation'](self.name_parts).strip()
|
||||
|
||||
def __str__(self):
|
||||
s = f'#{self.identifier}'
|
||||
if self.name or self.email:
|
||||
s += f' – {self.name or self.email}'
|
||||
if not self.is_active:
|
||||
s += f' ({_("disabled")})'
|
||||
return s
|
||||
|
||||
def set_password(self, raw_password):
|
||||
self.password = make_password(raw_password)
|
||||
|
||||
def check_password(self, raw_password):
|
||||
"""
|
||||
Return a boolean of whether the raw_password was correct. Handles
|
||||
hashing formats behind the scenes.
|
||||
"""
|
||||
def setter(raw_password):
|
||||
self.set_password(raw_password)
|
||||
self.save(update_fields=["password"])
|
||||
return check_password(raw_password, self.password, setter)
|
||||
|
||||
def set_unusable_password(self):
|
||||
# Set a value that will never be a valid hash
|
||||
self.password = make_password(None)
|
||||
|
||||
def has_usable_password(self):
|
||||
"""
|
||||
Return False if set_unusable_password() has been called for this user.
|
||||
"""
|
||||
return is_password_usable(self.password)
|
||||
|
||||
def get_session_auth_hash(self):
|
||||
"""
|
||||
Return an HMAC of the password field.
|
||||
"""
|
||||
key_salt = "pretix.base.models.customers.Customer.get_session_auth_hash"
|
||||
payload = self.password
|
||||
payload += self.email
|
||||
return salted_hmac(key_salt, payload).hexdigest()
|
||||
|
||||
def get_email_context(self):
|
||||
ctx = {
|
||||
'name': self.name,
|
||||
'organizer': self.organizer.name,
|
||||
}
|
||||
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
|
||||
for f, l, w in name_scheme['fields']:
|
||||
if f == 'full_name':
|
||||
continue
|
||||
ctx['name_%s' % f] = self.name_parts.get(f, '')
|
||||
return ctx
|
||||
@@ -670,6 +670,7 @@ class Event(EventMixin, LoggedModel):
|
||||
variation_map = {}
|
||||
for i in Item.objects.filter(event=other).prefetch_related('variations'):
|
||||
vars = list(i.variations.all())
|
||||
require_membership_types = list(i.require_membership_types.all())
|
||||
item_map[i.pk] = i
|
||||
i.pk = None
|
||||
i.event = self
|
||||
@@ -679,8 +680,16 @@ class Event(EventMixin, LoggedModel):
|
||||
i.category = category_map[i.category_id]
|
||||
if i.tax_rule_id:
|
||||
i.tax_rule = tax_map[i.tax_rule_id]
|
||||
|
||||
if i.grant_membership_type and other.organizer_id != self.organizer_id:
|
||||
i.grant_membership_type = None
|
||||
|
||||
i.save()
|
||||
i.log_action('pretix.object.cloned')
|
||||
|
||||
if require_membership_types and other.organizer_id == self.organizer_id:
|
||||
i.require_membership_types.set(require_membership_types)
|
||||
|
||||
for v in vars:
|
||||
variation_map[v.pk] = v
|
||||
v.pk = None
|
||||
|
||||
@@ -514,6 +514,34 @@ class Item(LoggedModel):
|
||||
'product price.'),
|
||||
default=False
|
||||
)
|
||||
require_membership = models.BooleanField(
|
||||
verbose_name=_('Require a valid membership'),
|
||||
default=False,
|
||||
)
|
||||
require_membership_types = models.ManyToManyField(
|
||||
'MembershipType',
|
||||
verbose_name=_('Allowed membership types'),
|
||||
blank=True,
|
||||
)
|
||||
grant_membership_type = models.ForeignKey(
|
||||
'MembershipType',
|
||||
null=True, blank=True,
|
||||
related_name='granted_by',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('This product creates a membership of type'),
|
||||
)
|
||||
grant_membership_duration_like_event = models.BooleanField(
|
||||
verbose_name=_('The duration of the membership is the same as the duration of the event or event series date'),
|
||||
default=True,
|
||||
)
|
||||
grant_membership_duration_days = models.IntegerField(
|
||||
verbose_name=_('Membership duration in days'),
|
||||
default=0,
|
||||
)
|
||||
grant_membership_duration_months = models.IntegerField(
|
||||
verbose_name=_('Membership duration in months'),
|
||||
default=0,
|
||||
)
|
||||
# !!! Attention: If you add new fields here, also add them to the copying code in
|
||||
# pretix/control/forms/item.py if applicable.
|
||||
|
||||
@@ -760,6 +788,15 @@ class ItemVariation(models.Model):
|
||||
help_text=_('If set, this will be displayed next to the current price to show that the current price is a '
|
||||
'discounted one. This is just a cosmetic setting and will not actually impact pricing.')
|
||||
)
|
||||
require_membership = models.BooleanField(
|
||||
verbose_name=_('Require a valid membership'),
|
||||
default=False,
|
||||
)
|
||||
require_membership_types = models.ManyToManyField(
|
||||
'MembershipType',
|
||||
verbose_name=_('Membership types'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = ScopedManager(organizer='item__event__organizer')
|
||||
|
||||
|
||||
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
|
||||
@@ -47,6 +47,7 @@ import dateutil
|
||||
import pycountry
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
Case, Exists, F, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||
@@ -73,7 +74,7 @@ from pretix.base.banlist import banned
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User
|
||||
from pretix.base.models import Customer, User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.locking import NoLockManager
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
@@ -119,6 +120,8 @@ class Order(LockModel, LoggedModel):
|
||||
|
||||
:param event: The event this order belongs to
|
||||
:type event: Event
|
||||
:param customer: The customer this order belongs to
|
||||
:type customer: Customer
|
||||
:param email: The email of the person who ordered this
|
||||
:type email: str
|
||||
:param phone: The phone number of the person who ordered this
|
||||
@@ -177,6 +180,13 @@ class Order(LockModel, LoggedModel):
|
||||
related_name="orders",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
customer = models.ForeignKey(
|
||||
Customer,
|
||||
verbose_name=_("Customer"),
|
||||
related_name="orders",
|
||||
null=True, blank=True,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
email = models.EmailField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_('E-mail')
|
||||
@@ -822,7 +832,11 @@ class Order(LockModel, LoggedModel):
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
|
||||
check_voucher_usage=False) -> Union[bool, str]:
|
||||
check_voucher_usage=False, check_memberships=False) -> Union[bool, str]:
|
||||
from pretix.base.services.memberships import (
|
||||
validate_memberships_in_order,
|
||||
)
|
||||
|
||||
error_messages = {
|
||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||
'seat_unavailable': _('The seat "{seat}" is no longer available.'),
|
||||
@@ -830,11 +844,17 @@ class Order(LockModel, LoggedModel):
|
||||
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||
}
|
||||
now_dt = now_dt or now()
|
||||
positions = self.positions.all().select_related('item', 'variation', 'seat', 'voucher')
|
||||
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
|
||||
quota_cache = {}
|
||||
v_budget = {}
|
||||
v_usage = Counter()
|
||||
try:
|
||||
if check_memberships:
|
||||
try:
|
||||
validate_memberships_in_order(self.customer, positions, self.event, lock=False)
|
||||
except ValidationError as e:
|
||||
raise Quota.QuotaExceededException(e.message)
|
||||
|
||||
for i, op in enumerate(positions):
|
||||
if op.seat:
|
||||
if not op.seat.is_available(ignore_orderpos=op):
|
||||
@@ -1181,6 +1201,9 @@ class AbstractPosition(models.Model):
|
||||
voucher = models.ForeignKey(
|
||||
'Voucher', null=True, blank=True, on_delete=models.PROTECT
|
||||
)
|
||||
used_membership = models.ForeignKey(
|
||||
'Membership', null=True, blank=True, on_delete=models.PROTECT
|
||||
)
|
||||
addon_to = models.ForeignKey(
|
||||
'self', null=True, blank=True, on_delete=models.PROTECT, related_name='addons'
|
||||
)
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
import string
|
||||
from datetime import date, datetime, time
|
||||
|
||||
import pytz
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import MinLengthValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
@@ -123,6 +125,10 @@ class Organizer(LoggedModel):
|
||||
|
||||
return ObjectRelatedCache(self)
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
return pytz.timezone(self.settings.timezone)
|
||||
|
||||
@cached_property
|
||||
def all_logentries_link(self):
|
||||
return reverse(
|
||||
@@ -173,6 +179,24 @@ class Organizer(LoggedModel):
|
||||
e.delete()
|
||||
self.teams.all().delete()
|
||||
|
||||
def get_mail_backend(self, timeout=None, force_custom=False):
|
||||
"""
|
||||
Returns an email server connection, either by using the system-wide connection
|
||||
or by returning a custom one based on the organizer's settings.
|
||||
"""
|
||||
from pretix.base.email import CustomSMTPBackend
|
||||
|
||||
if self.settings.smtp_use_custom or force_custom:
|
||||
return CustomSMTPBackend(host=self.settings.smtp_host,
|
||||
port=self.settings.smtp_port,
|
||||
username=self.settings.smtp_username,
|
||||
password=self.settings.smtp_password,
|
||||
use_tls=self.settings.smtp_use_tls,
|
||||
use_ssl=self.settings.smtp_use_ssl,
|
||||
fail_silently=False, timeout=timeout)
|
||||
else:
|
||||
return get_connection(fail_silently=False)
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
@@ -198,6 +222,8 @@ class Team(LoggedModel):
|
||||
:type can_create_events: bool
|
||||
:param can_change_teams: If ``True``, the members can change the teams of this organizer account.
|
||||
:type can_change_teams: bool
|
||||
:param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts.
|
||||
:type can_manage_customers: bool
|
||||
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
|
||||
:type can_change_organizer_settings: bool
|
||||
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
|
||||
@@ -235,11 +261,14 @@ class Team(LoggedModel):
|
||||
help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy '
|
||||
'reports, so be careful who you add to this team!')
|
||||
)
|
||||
can_manage_customers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage customer accounts")
|
||||
)
|
||||
can_manage_gift_cards = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can manage gift cards")
|
||||
)
|
||||
|
||||
can_change_event_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change event settings")
|
||||
|
||||
Reference in New Issue
Block a user