forked from CGM_Public/pretix_original
* New models * CRUD UI * UI for adding/removing team members * Log display for teams * Fix invitations, move frontend * Drop old models (incomplete) * Drop more old stuff * Drop even more old stuff * Fix tests * Fix permission test * flake8 fix * Add tests fore the new code * Rebase migrations
This commit is contained in:
@@ -3,7 +3,7 @@ from .auth import U2FDevice, User
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventPermission, RequiredAction,
|
||||
Event, Event_SettingsStore, EventLock, RequiredAction,
|
||||
generate_invite_token,
|
||||
)
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
@@ -18,6 +18,6 @@ from .orders import (
|
||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, Organizer_SettingsStore, OrganizerPermission
|
||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, BaseUserManager, PermissionsMixin,
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
|
||||
@@ -81,6 +84,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._teamcache = {}
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
@@ -147,6 +154,103 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return LogEntry.objects.filter(content_type=ContentType.objects.get_for_model(User),
|
||||
object_id=self.pk)
|
||||
|
||||
def _get_teams_for_organizer(self, organizer):
|
||||
if 'o{}'.format(organizer.pk) not in self._teamcache:
|
||||
self._teamcache['o{}'.format(organizer.pk)] = list(self.teams.filter(organizer=organizer))
|
||||
return self._teamcache['o{}'.format(organizer.pk)]
|
||||
|
||||
def _get_teams_for_event(self, organizer, event):
|
||||
if 'e{}'.format(event.pk) not in self._teamcache:
|
||||
self._teamcache['e{}'.format(event.pk)] = list(self.teams.filter(organizer=organizer).filter(
|
||||
Q(all_events=True) | Q(limit_events=event)
|
||||
))
|
||||
return self._teamcache['e{}'.format(event.pk)]
|
||||
|
||||
class SuperuserPermissionSet:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular event
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> Union[set, SuperuserPermissionSet]:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
|
||||
def has_event_permisson(self, organizer, event, perm_name=None) -> bool:
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return True
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
if teams:
|
||||
self._teamcache['e{}'.format(event.pk)] = teams
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_organizer_permisson(self, organizer, perm_name=None):
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return True
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
if teams:
|
||||
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
"""
|
||||
Returns a queryset of events the user has any permissions to.
|
||||
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
|
||||
if self.is_superuser:
|
||||
return Event.objects.all()
|
||||
|
||||
return Event.objects.filter(
|
||||
Q(organizer_id__in=self.teams.filter(all_events=True).values_list('organizer', flat=True))
|
||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
|
||||
class U2FDevice(Device):
|
||||
json_data = models.TextField()
|
||||
|
||||
@@ -21,7 +21,6 @@ from pretix.base.validators import EventSlugBlacklistValidator
|
||||
from pretix.helpers.daterange import daterange
|
||||
|
||||
from ..settings import settings_hierarkey
|
||||
from .auth import User
|
||||
from .organizer import Organizer
|
||||
|
||||
|
||||
@@ -79,8 +78,6 @@ class Event(LoggedModel):
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
|
||||
permitted = models.ManyToManyField(User, through='EventPermission',
|
||||
related_name="events", )
|
||||
currency = models.CharField(max_length=10,
|
||||
verbose_name=_("Default currency"),
|
||||
choices=CURRENCY_CHOICES,
|
||||
@@ -307,69 +304,6 @@ def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class EventPermission(models.Model):
|
||||
"""
|
||||
The relation between an Event and a User who has permissions to
|
||||
access an event.
|
||||
|
||||
:param event: The event this permission refers to
|
||||
:type event: Event
|
||||
:param user: The user this permission set applies to
|
||||
:type user: User
|
||||
:param can_change_settings: If ``True``, the user can change all basic settings for this event.
|
||||
:type can_change_settings: bool
|
||||
:param can_change_items: If ``True``, the user can change and add items and related objects for this event.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the user can inspect details of all orders.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the user can change details of orders
|
||||
:type can_change_orders: bool
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(Event, related_name="user_perms", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, related_name="event_perms", on_delete=models.CASCADE, null=True, blank=True)
|
||||
invite_email = models.EmailField(null=True, blank=True)
|
||||
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
can_change_settings = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_permissions = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change permissions")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Event permission")
|
||||
verbose_name_plural = _("Event permissions")
|
||||
|
||||
def __str__(self):
|
||||
return _("%(name)s on %(object)s") % {
|
||||
'name': str(self.user),
|
||||
'object': str(self.event),
|
||||
}
|
||||
|
||||
|
||||
class EventLock(models.Model):
|
||||
event = models.CharField(max_length=36, primary_key=True)
|
||||
date = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -42,8 +42,6 @@ class Organizer(LoggedModel):
|
||||
],
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
permitted = models.ManyToManyField(User, through='OrganizerPermission',
|
||||
related_name="organizers")
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer")
|
||||
@@ -74,39 +72,131 @@ def generate_invite_token():
|
||||
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
|
||||
|
||||
class OrganizerPermission(models.Model):
|
||||
class Team(LoggedModel):
|
||||
"""
|
||||
The relation between an Organizer and a User who has permissions to
|
||||
access an organizer profile.
|
||||
A team is a collection of people given certain access rights to one or more events of an organizer.
|
||||
|
||||
:param organizer: The organizer this relation refers to
|
||||
:param name: The name of this team
|
||||
:type name: str
|
||||
:param organizer: The organizer this team belongs to
|
||||
:type organizer: Organizer
|
||||
:param user: The user this set of permissions is valid for
|
||||
:type user: User
|
||||
:param can_create_events: Whether or not this user can create new events with this
|
||||
organizer account.
|
||||
:param members: A set of users who belong to this team
|
||||
:param all_events: Whether this team has access to all events of this organizer
|
||||
:type all_events: bool
|
||||
:param limit_events: A set of events this team has access to. Irrelevant if ``all_events`` is ``True``.
|
||||
:param can_create_events: Whether or not the members can create new events with this organizer account.
|
||||
: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_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.
|
||||
:type can_change_event_settings: bool
|
||||
:param can_change_items: If ``True``, the members can change and add items and related objects for the associated events.
|
||||
:type can_change_items: bool
|
||||
:param can_view_orders: If ``True``, the members can inspect details of all orders of the associated events.
|
||||
:type can_view_orders: bool
|
||||
:param can_change_orders: If ``True``, the members can change details of orders of the associated events.
|
||||
:type can_change_orders: bool
|
||||
:param can_view_vouchers: If ``True``, the members can inspect details of all vouchers of the associated events.
|
||||
:type can_view_vouchers: bool
|
||||
:param can_change_vouchers: If ``True``, the members can change and create vouchers for the associated events.
|
||||
:type can_change_vouchers: bool
|
||||
"""
|
||||
organizer = models.ForeignKey(Organizer, related_name="teams", on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=190, verbose_name=_("Team name"))
|
||||
members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members"))
|
||||
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
|
||||
|
||||
organizer = models.ForeignKey(Organizer, related_name="user_perms", on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, related_name="organizer_perms", on_delete=models.CASCADE, null=True, blank=True)
|
||||
invite_email = models.EmailField(null=True, blank=True)
|
||||
invite_token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
can_create_events = models.BooleanField(
|
||||
default=True,
|
||||
default=False,
|
||||
verbose_name=_("Can create events"),
|
||||
)
|
||||
can_change_permissions = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Can change permissions"),
|
||||
can_change_teams = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change teams and permissions"),
|
||||
)
|
||||
can_change_organizer_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change organizer settings")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer permission")
|
||||
verbose_name_plural = _("Organizer permissions")
|
||||
can_change_event_settings = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change event settings")
|
||||
)
|
||||
can_change_items = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change product settings")
|
||||
)
|
||||
can_view_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view orders")
|
||||
)
|
||||
can_change_orders = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change orders")
|
||||
)
|
||||
can_view_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can view vouchers")
|
||||
)
|
||||
can_change_vouchers = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Can change vouchers")
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("%(name)s on %(object)s") % {
|
||||
'name': str(self.user),
|
||||
'name': str(self.name),
|
||||
'object': str(self.organizer),
|
||||
}
|
||||
|
||||
def permission_set(self) -> set:
|
||||
attribs = dir(self)
|
||||
return {
|
||||
a for a in attribs if a.startswith('can_') and self.has_permission(a)
|
||||
}
|
||||
|
||||
@property
|
||||
def can_change_settings(self): # Legacy compatiblilty
|
||||
return self.can_change_event_settings
|
||||
|
||||
def has_permission(self, perm_name):
|
||||
try:
|
||||
return getattr(self, perm_name)
|
||||
except AttributeError:
|
||||
raise ValueError('Invalid required permission: %s' % perm_name)
|
||||
|
||||
def permission_for_event(self, event):
|
||||
if self.all_events:
|
||||
return event.organizer_id == self.organizer_id
|
||||
else:
|
||||
return self.limit_events.filter(pk=event.pk).exists()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Team")
|
||||
verbose_name_plural = _("Teams")
|
||||
|
||||
|
||||
class TeamInvite(models.Model):
|
||||
"""
|
||||
A TeamInvite represents someone who has been invited to a team but hasn't accept the invitation
|
||||
yet.
|
||||
|
||||
:param team: The team the person is invited to
|
||||
:type team: Team
|
||||
:param email: The email the invite has been sent to
|
||||
:type email: str
|
||||
:param token: The secret required to redeem the invite
|
||||
:type token: str
|
||||
"""
|
||||
team = models.ForeignKey(Team, related_name="invites", on_delete=models.CASCADE)
|
||||
email = models.EmailField(null=True, blank=True)
|
||||
token = models.CharField(default=generate_invite_token, max_length=64, null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return _("Invite to team '{team}' for '{email}'").format(
|
||||
team=str(self.team), email=self.email
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user