diff --git a/doc/development/implementation/models.rst b/doc/development/implementation/models.rst index e914f123d..d9177bfbc 100644 --- a/doc/development/implementation/models.rst +++ b/doc/development/implementation/models.rst @@ -20,13 +20,10 @@ Organizers and events .. autoclass:: pretix.base.models.Organizer :members: -.. autoclass:: pretix.base.models.OrganizerPermission - :members: - .. autoclass:: pretix.base.models.Event :members: -.. autoclass:: pretix.base.models.EventPermission +.. autoclass:: pretix.base.models.Team :members: .. autoclass:: pretix.base.models.RequiredAction diff --git a/src/make_testdata.py b/src/make_testdata.py index 5c684676c..580168b80 100644 --- a/src/make_testdata.py +++ b/src/make_testdata.py @@ -23,9 +23,6 @@ user.save() organizer = Organizer.objects.create( name='BigEvents LLC', slug='bigevents' ) -OrganizerPermission.objects.get_or_create( - organizer=organizer, user=user -) year = now().year + 1 event = Event.objects.create( organizer=organizer, name='Demo Conference {}'.format(year), @@ -33,9 +30,13 @@ event = Event.objects.create( date_from=datetime(year, 9, 4, 17, 0, 0), date_to=datetime(year, 9, 6, 17, 0, 0), ) -EventPermission.objects.get_or_create( - event=event, user=user +t = Team.objects.get_or_create( + organizer=organizer, name='Admin Team', + all_events=True, can_create_events=True, can_change_teams=True, + can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True, + can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True ) +t.members.add(user) cat_tickets = ItemCategory.objects.create( event=event, name='Tickets' ) diff --git a/src/pretix/base/migrations/0052_team_teaminvite.py b/src/pretix/base/migrations/0052_team_teaminvite.py new file mode 100644 index 000000000..79d01ee5e --- /dev/null +++ b/src/pretix/base/migrations/0052_team_teaminvite.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-04-27 09:11 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import pretix.base.models.organizer + + +def create_teams(apps, schema_editor): + Event = apps.get_model('pretixbase', 'Event') + Organizer = apps.get_model('pretixbase', 'Organizer') + Team = apps.get_model('pretixbase', 'Team') + TeamInvite = apps.get_model('pretixbase', 'TeamInvite') + EventPermission = apps.get_model('pretixbase', 'EventPermission') + OrganizerPermission = apps.get_model('pretixbase', 'OrganizerPermission') + + for o in Organizer.objects.prefetch_related('events'): + for e in o.events.all(): + teams = {} + + for p in e.user_perms.all(): + pkey = (p.can_change_settings, p.can_change_items, p.can_view_orders, + p.can_change_permissions, p.can_change_orders, p.can_view_vouchers, + p.can_change_vouchers) + if pkey not in teams: + team = Team() + team.can_change_event_settings = p.can_change_settings + team.can_change_items = p.can_change_items + team.can_view_orders = p.can_view_orders + team.can_change_orders = p.can_change_orders + team.can_view_vouchers = p.can_view_vouchers + team.can_change_vouchers = p.can_change_vouchers + team.organizer = o + team.name = '{} Team {}'.format( + str(e.name), len(teams) + 1 + ) + team.save() + team.limit_events.add(e) + + teams[pkey] = team + + if p.user: + teams[pkey].members.add(p.user) + else: + teams[pkey].invites.create(email=p.invite_email, token=p.invite_token) + + teams = {} + for p in o.user_perms.all(): + pkey = (p.can_create_events, p.can_change_permissions) + if pkey not in teams: + team = Team() + team.can_change_organizer_settings = True + team.can_create_events = p.can_create_events + team.can_change_teams = p.can_change_permissions + team.organizer = o + team.name = '{} Team {}'.format( + str(o.name), len(teams) + 1 + ) + team.save() + teams[pkey] = team + + if p.user: + teams[pkey].members.add(p.user) + else: + teams[pkey].invites.create(email=p.invite_email, token=p.invite_token) + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0051_auto_20170206_2027_squashed_0057_auto_20170501_2116'), + ] + + operations = [ + migrations.CreateModel( + name='Team', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=190, verbose_name='Team name')), + ('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')), + ('can_create_events', models.BooleanField(default=False, verbose_name='Can create events')), + ('can_change_teams', models.BooleanField(default=False, verbose_name='Can change permissions')), + ('can_change_organizer_settings', models.BooleanField(default=False, verbose_name='Can change organizer settings')), + ('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')), + ('limit_events', models.ManyToManyField(to='pretixbase.Event', verbose_name='Limit to events')), + ('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL, verbose_name='Team members')), + ('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='pretixbase.Organizer')), + ], + options={ + 'verbose_name_plural': 'Teams', + 'verbose_name': 'Team', + }, + ), + migrations.CreateModel( + name='TeamInvite', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('token', models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64, null=True)), + ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='pretixbase.Team')), + ], + ), + migrations.RunPython( + create_teams, migrations.RunPython.noop + ) + ] diff --git a/src/pretix/base/migrations/0058_auto_20170429_1020.py b/src/pretix/base/migrations/0058_auto_20170429_1020.py new file mode 100644 index 000000000..ec37c6a1f --- /dev/null +++ b/src/pretix/base/migrations/0058_auto_20170429_1020.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-04-29 10:20 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0052_team_teaminvite'), + ] + + operations = [ + migrations.RemoveField( + model_name='eventpermission', + name='event', + ), + migrations.RemoveField( + model_name='eventpermission', + name='user', + ), + migrations.RemoveField( + model_name='organizerpermission', + name='organizer', + ), + migrations.RemoveField( + model_name='organizerpermission', + name='user', + ), + migrations.RemoveField( + model_name='event', + name='permitted', + ), + migrations.RemoveField( + model_name='organizer', + name='permitted', + ), + migrations.AlterField( + model_name='team', + name='can_change_teams', + field=models.BooleanField(default=False, verbose_name='Can change teams and permissions'), + ), + migrations.AlterField( + model_name='team', + name='limit_events', + field=models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events'), + ), + migrations.DeleteModel( + name='EventPermission', + ), + migrations.DeleteModel( + name='OrganizerPermission', + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index b44ae4122..ffde2cb67 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -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 diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index c36c1d52c..5fc274147 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -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() diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index c81a1a905..4c216ece1 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -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) diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index a917e7f34..d71e5049b 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -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 + ) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 0a7e3c1ae..e46bd0b90 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -2,6 +2,7 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import RegexValidator +from django.db.models import Q from django.utils.timezone import get_current_timezone_name from django.utils.translation import ugettext_lazy as _ from i18nfield.forms import I18nFormField, I18nTextarea @@ -26,7 +27,7 @@ class EventWizardFoundationForm(forms.Form): self.fields['organizer'] = forms.ModelChoiceField( label=_("Organizer"), queryset=Organizer.objects.filter( - id__in=self.user.organizer_perms.filter(can_create_events=True).values_list('organizer', flat=True) + id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True) ), widget=forms.RadioSelect, empty_label=None, @@ -111,6 +112,16 @@ class EventWizardBasicsForm(I18nModelForm): class EventWizardCopyForm(forms.Form): + @staticmethod + def copy_from_queryset(user): + return Event.objects.filter( + Q(organizer_id__in=user.teams.filter( + all_events=True, can_change_event_settings=True, can_change_items=True + ).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter( + can_change_event_settings=True, can_change_items=True + ).values_list('limit_events__id', flat=True)) + ) + def __init__(self, *args, **kwargs): kwargs.pop('organizer') kwargs.pop('locales') @@ -118,11 +129,7 @@ class EventWizardCopyForm(forms.Form): super().__init__(*args, **kwargs) self.fields['copy_from_event'] = forms.ModelChoiceField( label=_("Copy configuration from"), - queryset=Event.objects.filter( - id__in=self.user.event_perms.filter( - can_change_items=True, can_change_settings=True - ).values_list('event', flat=True) - ), + queryset=EventWizardCopyForm.copy_from_queryset(self.user), widget=forms.RadioSelect, empty_label=_('Do not copy'), required=False diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index f70ae341c..867739c2a 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -1,8 +1,9 @@ from django import forms +from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from pretix.base.forms import I18nModelForm -from pretix.base.models import Organizer +from pretix.base.models import Organizer, Team from pretix.multidomain.models import KnownDomain @@ -65,3 +66,35 @@ class OrganizerUpdateForm(OrganizerForm): instance.get_cache().clear() return instance + + +class TeamForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + organizer = kwargs.pop('organizer') + super().__init__(*args, **kwargs) + self.fields['limit_events'].queryset = organizer.events.all() + + class Meta: + model = Team + fields = ['name', 'all_events', 'limit_events', 'can_create_events', + 'can_change_teams', 'can_change_organizer_settings', + 'can_change_event_settings', 'can_change_items', + 'can_view_orders', 'can_change_orders', + 'can_view_vouchers', 'can_change_vouchers'] + widgets = { + 'limit_events': forms.CheckboxSelectMultiple(attrs={ + 'data-inverse-dependency': '#id_all_events' + }), + } + + def clean(self): + data = super().clean() + if self.instance.pk and not data['can_change_teams']: + if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter( + can_change_teams=True, members__isnull=False + ).exists(): + raise ValidationError(_('The changes could not be saved because there would be no remaining team with ' + 'the permission to change teams and permissions.')) + + return data diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 63dbd10e3..058fc5925 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -121,7 +121,10 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.permissions.invited': _('A user has been invited to the event team.'), 'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'), 'pretix.event.permissions.deleted': _('A user has been removed from the event team.'), - 'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.') + 'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), + 'pretix.team.created': _('The team has been created.'), + 'pretix.team.changed': _('The team settings have been modified.'), + 'pretix.team.deleted': _('The team settings has been deleted.'), } data = json.loads(logentry.data) @@ -149,6 +152,23 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): if logentry.action_type.startswith('pretix.event.tickets.provider.'): return _('The settings of a ticket output provider have been changed.') + if logentry.action_type == 'pretix.team.member.added': + return _('{user} has been added to the team.').format(user=data.get('email')) + + if logentry.action_type == 'pretix.team.member.removed': + return _('{user} has been removed from the team.').format(user=data.get('email')) + + if logentry.action_type == 'pretix.team.member.joined': + return _('{user} has joined the team using the invite sent to {email}.').format( + user=data.get('email'), email=data.get('invite_email') + ) + + if logentry.action_type == 'pretix.team.invite.created': + return _('{user} has been invited to the team.').format(user=data.get('email')) + + if logentry.action_type == 'pretix.team.invite.deleted': + return _('The invite for {user} has been revoked.').format(user=data.get('email')) + if logentry.action_type == 'pretix.user.settings.changed': text = str(_('Your account settings have been changed.')) if 'email' in data: diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index c3d2d743e..fab7dae3b 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -9,9 +9,7 @@ from django.utils.deprecation import MiddlewareMixin from django.utils.encoding import force_str from django.utils.translation import ugettext as _ -from pretix.base.models import ( - Event, EventPermission, Organizer, OrganizerPermission, -) +from pretix.base.models import Event, Organizer class PermissionMiddleware(MiddlewareMixin): @@ -61,53 +59,23 @@ class PermissionMiddleware(MiddlewareMixin): return redirect_to_login( path, resolved_login_url, REDIRECT_FIELD_NAME) - events = Event.objects.all() if request.user.is_superuser else request.user.events - request.user.events_cache = events.order_by( - "organizer", "date_from").prefetch_related("organizer") + events = request.user.get_events_with_any_permission() + request.user.events_cache = events.order_by("organizer", "date_from").prefetch_related("organizer") if 'event' in url.kwargs and 'organizer' in url.kwargs: - try: - if request.user.is_superuser: - request.event = Event.objects.filter( - slug=url.kwargs['event'], - organizer__slug=url.kwargs['organizer'], - ).select_related('organizer')[0] - request.eventperm = EventPermission( - event=request.event, - user=request.user - ) - else: - request.event = Event.objects.filter( - slug=url.kwargs['event'], - permitted__id__exact=request.user.id, - organizer__slug=url.kwargs['organizer'], - ).select_related('organizer')[0] - request.eventperm = EventPermission.objects.get( - event=request.event, - user=request.user - ) - request.organizer = request.event.organizer - except IndexError: + request.event = Event.objects.filter( + slug=url.kwargs['event'], + organizer__slug=url.kwargs['organizer'], + ).select_related('organizer').first() + if not request.event or not request.user.has_event_permisson(request.event.organizer, request.event): raise Http404(_("The selected event was not found or you " "have no permission to administrate it.")) + request.organizer = request.event.organizer + request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event) elif 'organizer' in url.kwargs: - try: - if request.user.is_superuser: - request.organizer = Organizer.objects.filter( - slug=url.kwargs['organizer'], - )[0] - request.orgaperm = OrganizerPermission( - organizer=request.organizer, - user=request.user - ) - else: - request.organizer = Organizer.objects.filter( - slug=url.kwargs['organizer'], - permitted__id__exact=request.user.id, - )[0] - request.orgaperm = OrganizerPermission.objects.get( - organizer=request.organizer, - user=request.user - ) - except IndexError: + request.organizer = Organizer.objects.filter( + slug=url.kwargs['organizer'], + ).first() + if not request.organizer or not request.user.has_organizer_permisson(request.organizer): raise Http404(_("The selected organizer was not found or you " "have no permission to administrate it.")) + request.orgapermset = request.user.get_organizer_permission_set(request.organizer) diff --git a/src/pretix/control/permissions.py b/src/pretix/control/permissions.py index 088d72a9b..feb908068 100644 --- a/src/pretix/control/permissions.py +++ b/src/pretix/control/permissions.py @@ -1,37 +1,29 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext as _ -from pretix.base.models import EventPermission, OrganizerPermission - def event_permission_required(permission): """ This view decorator rejects all requests with a 403 response which are not from users having the given permission for the event the request is associated with. """ + if permission == 'can_change_settings': + # Legacy support + permission = 'can_change_event_settings' + def decorator(function): def wrapper(request, *args, **kw): if not request.user.is_authenticated: # NOQA # just a double check, should not ever happen raise PermissionDenied() - if request.user.is_superuser: + + allowed = ( + request.user.is_superuser + or request.user.has_event_permisson(request.organizer, request.event, permission) + ) + if allowed: return function(request, *args, **kw) - try: - perm = EventPermission.objects.get( - event=request.event, - user=request.user - ) - except EventPermission.DoesNotExist: - pass - else: - allowed = not permission - try: - if permission: - allowed = getattr(perm, permission) - except AttributeError: - pass - if allowed or request.user.is_superuser: - return function(request, *args, **kw) + raise PermissionDenied(_('You do not have permission to view this content.')) return wrapper return decorator @@ -55,29 +47,23 @@ def organizer_permission_required(permission): This view decorator rejects all requests with a 403 response which are not from users having the given permission for the event the request is associated with. """ + if permission == 'can_change_settings': + # Legacy support + permission = 'can_change_organizer_settings' + def decorator(function): def wrapper(request, *args, **kw): if not request.user.is_authenticated: # NOQA # just a double check, should not ever happen raise PermissionDenied() - if request.user.is_superuser: + + allowed = ( + request.user.is_superuser + or request.user.has_organizer_permisson(request.organizer, permission) + ) + if allowed: return function(request, *args, **kw) - try: - perm = OrganizerPermission.objects.get( - organizer=request.organizer, - user=request.user - ) - except OrganizerPermission.DoesNotExist: - pass - else: - allowed = not permission - try: - if permission: - allowed = getattr(perm, permission) - except AttributeError: - pass - if allowed or request.user.is_superuser: - return function(request, *args, **kw) + raise PermissionDenied(_('You do not have permission to view this content.')) return wrapper return decorator diff --git a/src/pretix/control/templates/pretixcontrol/email/invitation.txt b/src/pretix/control/templates/pretixcontrol/email/invitation.txt index f625b981a..550a4ec13 100644 --- a/src/pretix/control/templates/pretixcontrol/email/invitation.txt +++ b/src/pretix/control/templates/pretixcontrol/email/invitation.txt @@ -1,9 +1,10 @@ {% load i18n %}{% blocktrans with url=url|safe %}Hello, -you have been invited to the team of an event that uses pretix for their +you have been invited to a team on pretix, a platform to perform event ticket sales. -Event: {{ event }} +Organizer: {{ organizer }} +Team: {{ team }} If you want to join that team, just click on the following link: {{ url }} diff --git a/src/pretix/control/templates/pretixcontrol/email/invitation_organizer.txt b/src/pretix/control/templates/pretixcontrol/email/invitation_organizer.txt deleted file mode 100644 index 77f284740..000000000 --- a/src/pretix/control/templates/pretixcontrol/email/invitation_organizer.txt +++ /dev/null @@ -1,16 +0,0 @@ -{% load i18n %}{% blocktrans with url=url|safe %}Hello, - -you have been invited to the team of an event organizer that uses pretix -for their ticket sales. - -Organizer: {{ organizer }} - -If you want to join that team, just click on the following link: -{{ url }} - -If you do not want to join, you can safely ignore or delete this email. - -Best regards, - -Your pretix team -{% endblocktrans %} diff --git a/src/pretix/control/templates/pretixcontrol/event/base.html b/src/pretix/control/templates/pretixcontrol/event/base.html index d1961f97c..bda5526e4 100644 --- a/src/pretix/control/templates/pretixcontrol/event/base.html +++ b/src/pretix/control/templates/pretixcontrol/event/base.html @@ -10,7 +10,7 @@ {% trans "Dashboard" %} - {% if request.eventperm.can_change_settings or request.eventperm.can_change_permissions %} + {% if 'can_change_event_settings' in request.eventpermset or 'can_change_permissions' in request.eventpermset %}
  • @@ -18,7 +18,7 @@
  • {% endif %} - {% if request.eventperm.can_change_items %} + {% if 'can_change_items' in request.eventpermset %}
  • @@ -55,7 +55,7 @@
  • {% endif %} - {% if request.eventperm.can_view_orders %} + {% if 'can_view_orders' in request.eventpermset %}
  • @@ -93,7 +93,7 @@
  • {% endif %} - {% if request.eventperm.can_view_vouchers %} + {% if 'can_view_vouchers' in request.eventpermset %}
  • diff --git a/src/pretix/control/templates/pretixcontrol/event/logs.html b/src/pretix/control/templates/pretixcontrol/event/logs.html index b288cb002..81af0c75b 100644 --- a/src/pretix/control/templates/pretixcontrol/event/logs.html +++ b/src/pretix/control/templates/pretixcontrol/event/logs.html @@ -15,9 +15,11 @@ {% trans "Customer actions" %} {% for up in userlist %} - + {% if up.user__id %} + + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/event/permissions.html b/src/pretix/control/templates/pretixcontrol/event/permissions.html index 24d87ee28..c7ccc6748 100644 --- a/src/pretix/control/templates/pretixcontrol/event/permissions.html +++ b/src/pretix/control/templates/pretixcontrol/event/permissions.html @@ -1,88 +1,19 @@ {% extends "pretixcontrol/event/settings_base.html" %} {% load i18n %} +{% load staticfiles %} {% load bootstrap3 %} {% block inside %} -
    - {% csrf_token %} -
    - {% trans "Permissions" %} - {% bootstrap_formset_errors formset %} - {{ formset.management_form }} -
    - - - - - - - - - - - - - - - - {% for form in formset %} - - - - - - - - - - - - {% endfor %} - - - - - - - - - - - - - - - - -
    {% trans "User" %}{% trans "Change settings" %}{% trans "Change products" %}{% trans "View orders" %}{% trans "Change orders" %}{% trans "Change permissions" %}{% trans "View vouchers" %}{% trans "Change vouchers" %}{% trans "Delete" %}
    - {{ form.id }} - {% if form.instance.user %} - {{ form.instance.user }} - {% else %} - {{ form.instance.invite_email }} - - {% endif %} - {{ form.can_change_settings }}{{ form.can_change_items }}{{ form.can_view_orders }}{{ form.can_change_orders }}{{ form.can_change_permissions }}{{ form.can_view_vouchers }}{{ form.can_change_vouchers }}{{ form.DELETE }}
    - {% trans "Adding a new user" %}
    - {% blocktrans trimmed %} - To add a new user, you can enter their email address here. If they already have a - pretix account, they will immediately be added to the event. Otherwise, they will - be sent an email with an invitation. - {% endblocktrans %} -
    -
    -
    - {% bootstrap_field add_form.user layout='inline' %} -
    -
    -
    {{ add_form.can_change_settings }}{{ add_form.can_change_items }}{{ add_form.can_view_orders }}{{ add_form.can_change_orders }}{{ add_form.can_change_permissions }}{{ add_form.can_view_vouchers }}{{ add_form.can_change_vouchers }}
    -
    -
    -
    - + - {% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/settings_base.html b/src/pretix/control/templates/pretixcontrol/event/settings_base.html index 842c1a8ba..fe7618757 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings_base.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings_base.html @@ -5,7 +5,7 @@ {% block content %}

    {% trans "Settings" %}