Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/eventyay/control/forms/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,22 @@ def __init__(self, *args, **kwargs):
validators=[MinValueValidator(0)],
),
),
(
'allow_all_users_create_organizer',
forms.BooleanField(
label=_('All registered users can create organizers'),
help_text=_('If enabled, all registered users will be allowed to create organizers. System admins can always create organizers.'),
required=False,
),
),
(
'allow_payment_users_create_organizer',
forms.BooleanField(
label=_('All accounts with payment information can create organizers'),
help_text=_('If enabled, users with valid payment information on file will be allowed to create organizers. System admins can always create organizers.'),
required=False,
),
),
Comment on lines +281 to +296
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The help texts for the two organizer creation permission settings don't explain how they interact when both are enabled or what takes precedence. Consider updating the help texts to clarify:

  1. When both settings are disabled (unchecked), only system admins can create organizers
  2. When "All registered users can create organizers" is enabled, it takes precedence and all authenticated users can create organizers, regardless of the payment setting
  3. When only "All accounts with payment information can create organizers" is enabled, only users with payment info on file can create organizers

For example:

  • allow_all_users_create_organizer: "If enabled, all registered users will be allowed to create organizers. This takes precedence over the payment information requirement below."
  • allow_payment_users_create_organizer: "If enabled (and 'All registered users' is disabled), only users with valid payment information on file will be allowed to create organizers."

Copilot uses AI. Check for mistakes.
]
)

Expand Down Expand Up @@ -321,6 +337,10 @@ def __init__(self, *args, **kwargs):
('maps', _('Maps'), [
'opencagedata_apikey', 'mapquest_apikey', 'leaflet_tiles', 'leaflet_tiles_attribution',
]),
('organizers', _('Organizers'), [
'allow_all_users_create_organizer',
'allow_payment_users_create_organizer',
]),
]


Expand Down
85 changes: 85 additions & 0 deletions app/eventyay/control/permissions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from urllib.parse import quote

from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _

from eventyay.base.models import Organizer
from eventyay.base.models.organizer import OrganizerBillingModel
from eventyay.base.settings import GlobalSettingsObject


def current_url(request):
if request.GET:
Expand Down Expand Up @@ -157,3 +162,83 @@ class StaffMemberRequiredMixin:
def as_view(cls, **initkwargs):
view = super(StaffMemberRequiredMixin, cls).as_view(**initkwargs)
return staff_member_required()(view)


class OrganizerCreationPermissionMixin:
"""
Mixin to check if a user has permission to create organizers.
Can be used in any view that needs to check organizer creation permissions.
"""

def _can_create_organizer(self, user):
"""
Check if the user has permission to create an organizer.

Permission precedence (highest to lowest):
1. System admins (staff with active session) - always allowed
2. Default when both settings are None - allow all users (permissive default)
3. allow_all_users_create_organizer=True - allow all authenticated users
4. allow_payment_users_create_organizer=True - allow users with payment info
5. Both False - deny (admin only)

Note: If allow_all_users=True, it takes precedence over allow_payment_users
(no need to check payment info if all users are already allowed).

Args:
user: The user to check permissions for

Returns:
bool: True if user can create organizers, False otherwise
"""
# System admins can always create organizers
if user.has_active_staff_session(self.request.session.session_key):
return True

# Get global settings
gs = GlobalSettingsObject()
allow_all_users = gs.settings.get('allow_all_users_create_organizer', None, as_type=bool)
allow_payment_users = gs.settings.get('allow_payment_users_create_organizer', None, as_type=bool)

# If neither option is explicitly set, default to allowing all users (permissive default)
if allow_all_users is None and allow_payment_users is None:
return True

# If all users are allowed (takes precedence over payment check)
if allow_all_users:
return True

# If users with payment information are allowed
if allow_payment_users:
return self._user_has_payment_info(user)

# By default, deny access if settings are explicitly set to False
return False

def _user_has_payment_info(self, user):
"""
Check if the user has valid payment information on file.

This checks if any of the user's organizers have billing records with payment method setup.
Checks for:
- stripe_customer_id: Indicates Stripe customer account
- stripe_payment_method_id: Indicates saved payment method

Args:
user: The user to check payment info for

Returns:
bool: True if user has payment info, False otherwise
"""
# Get all organizers where the user is a team member
user_organizers = Organizer.objects.filter(
teams__members=user
).distinct()

# Single query to check if any billing record has payment info
# Check for either stripe_customer_id OR stripe_payment_method_id
return OrganizerBillingModel.objects.filter(
organizer__in=user_organizers
).filter(
(Q(stripe_customer_id__isnull=False) & ~Q(stripe_customer_id='')) |
(Q(stripe_payment_method_id__isnull=False) & ~Q(stripe_payment_method_id=''))
).exists()
Comment on lines +232 to +244
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The query could potentially be optimized by combining the two filter operations into a single query using joins. Instead of:

user_organizers = Organizer.objects.filter(teams__members=user).distinct()
return OrganizerBillingModel.objects.filter(organizer__in=user_organizers).filter(...).exists()

Consider:

return OrganizerBillingModel.objects.filter(
    organizer__teams__members=user
).filter(
    (Q(stripe_customer_id__isnull=False) & ~Q(stripe_customer_id='')) |
    (Q(stripe_payment_method_id__isnull=False) & ~Q(stripe_payment_method_id=''))
).exists()

This would eliminate the intermediate queryset and potentially improve performance, though the impact may be minimal.

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +244
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a circular dependency issue in the payment information check. The _user_has_payment_info method checks if the user has payment info by looking at billing records of organizers where the user is already a team member. However, for new users who haven't created any organizers yet, user_organizers will always be empty, causing this check to always return False.

This means when allow_payment_users_create_organizer=True and allow_all_users_create_organizer=False, new users will never be able to create their first organizer, even if they have payment information on file.

Consider modifying the logic to check for payment information at the user level (if such a model exists) or document that users need to be added to an existing organizer first before they can create new ones.

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ <h1>{% trans "Organizers" %}</h1>
</button>
</div>
</form>
{% if can_create_organizer %}
<p>
<a href='{% url "control:organizers.add" %}' class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new organizer" %}
</a>
</p>
{% endif %}
<table class="table table-condensed table-hover">
<thead>
<tr>
Expand Down
11 changes: 8 additions & 3 deletions app/eventyay/control/views/organizer_views/organizer_view.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging

from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files import File
from django.db import transaction
from django.db.models import Max, Min, Prefetch, ProtectedError
Expand Down Expand Up @@ -33,6 +33,7 @@
)
from eventyay.control.permissions import (
AdministratorPermissionRequiredMixin,
OrganizerCreationPermissionMixin,
OrganizerPermissionRequiredMixin,
)
from eventyay.control.signals import nav_organizer
Expand All @@ -52,13 +53,16 @@
logger = logging.getLogger(__name__)


class OrganizerCreate(CreateView):
class OrganizerCreate(OrganizerCreationPermissionMixin, CreateView):
model = Organizer
form_class = OrganizerForm
template_name = 'pretixcontrol/organizers/create.html'
context_object_name = 'organizer'

def dispatch(self, request, *args, **kwargs):
# Check if user has permission to create organizers
if not self._can_create_organizer(request.user):
raise PermissionDenied(_('You do not have permission to create organizers. Please contact an administrator.'))
return super().dispatch(request, *args, **kwargs)

@transaction.atomic
Expand Down Expand Up @@ -362,7 +366,7 @@ def get_object(self, queryset=None) -> Organizer:
return self.request.organizer


class OrganizerList(PaginationMixin, ListView):
class OrganizerList(OrganizerCreationPermissionMixin, PaginationMixin, ListView):
model = Organizer
context_object_name = 'organizers'
template_name = 'pretixcontrol/organizers/index.html'
Expand All @@ -379,6 +383,7 @@ def get_queryset(self):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['can_create_organizer'] = self._can_create_organizer(self.request.user)
return ctx

@cached_property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h1>{% translate "Organizers" %}</h1>
</button>
</div>
</form>
{% if staff_session %}
{% if can_create_organizer %}
<p>
<a href='{% url "eventyay_common:organizers.add" %}' class="btn btn-default">
<span class="fa fa-plus"></span>
Expand Down
15 changes: 10 additions & 5 deletions app/eventyay/eventyay_common/views/organizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@

from eventyay.base.models import Organizer, Team
from eventyay.control.forms.filter import OrganizerFilterForm
from eventyay.control.permissions import (
OrganizerCreationPermissionMixin,
OrganizerPermissionRequiredMixin,
)
from eventyay.control.views import CreateView, PaginationMixin, UpdateView

from ...control.forms.organizer_forms import OrganizerForm, OrganizerUpdateForm
from ...control.permissions import OrganizerPermissionRequiredMixin

logger = logging.getLogger(__name__)


class OrganizerList(PaginationMixin, ListView):
class OrganizerList(OrganizerCreationPermissionMixin, PaginationMixin, ListView):
model = Organizer
context_object_name = 'organizers'
template_name = 'eventyay_common/organizers/index.html'
Expand All @@ -37,22 +40,24 @@ def get_queryset(self):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['can_create_organizer'] = self._can_create_organizer(self.request.user)
return ctx

@cached_property
def filter_form(self):
return OrganizerFilterForm(data=self.request.GET, request=self.request)


class OrganizerCreate(CreateView):
class OrganizerCreate(OrganizerCreationPermissionMixin, CreateView):
model = Organizer
form_class = OrganizerForm
template_name = 'eventyay_common/organizers/create.html'
context_object_name = 'organizer'

def dispatch(self, request, *args, **kwargs):
if not request.user.has_active_staff_session(self.request.session.session_key):
raise PermissionDenied()
# Check if user has permission to create organizers
if not self._can_create_organizer(request.user):
raise PermissionDenied(_('You do not have permission to create organizers. Please contact an administrator.'))
return super().dispatch(request, *args, **kwargs)

@transaction.atomic
Expand Down
Loading