Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions app/eventyay/api/serializers/organizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ class Meta:
'can_view_vouchers',
'can_change_vouchers',
'can_checkin_orders',
'can_change_submissions',
'is_reviewer',
'force_hide_speaker_names',
'can_video_create_stages',
'can_video_create_channels',
'can_video_direct_message',
'can_video_manage_announcements',
'can_video_view_users',
'can_video_manage_users',
'can_video_manage_rooms',
'can_video_manage_kiosks',
'can_video_manage_configuration',
)

def validate(self, data):
Expand Down
18 changes: 16 additions & 2 deletions app/eventyay/api/views/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
# from pretix.presale.views.organizer import filter_qs_by_attr # commented out
from eventyay.api.task import configure_video_settings_for_talks
from eventyay.api.utils import get_protocol
from eventyay.eventyay_common.video.permissions import VIDEO_TRAIT_ROLE_MAP

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -325,8 +326,12 @@ def post(request, *args, **kwargs) -> JsonResponse:

title = titles.get(locale) or titles.get("en") or title_default

attendee_trait_grants = request.data.get("traits", {}).get("attendee", "")
if not isinstance(attendee_trait_grants, str):
traits_payload = request.data.get("traits") or {}
if not isinstance(traits_payload, dict):
raise ValidationError("Traits must be provided as an object.")

attendee_trait_grants = traits_payload.get("attendee", "")
if attendee_trait_grants and not isinstance(attendee_trait_grants, str):
raise ValidationError("Attendee traits must be a string")

trait_grants = {
Expand All @@ -337,6 +342,15 @@ def post(request, *args, **kwargs) -> JsonResponse:
"scheduleuser": ["schedule-update"],
}

for trait_name, role_name in VIDEO_TRAIT_ROLE_MAP.items():
trait_value = traits_payload.get(trait_name, "")
if trait_value:
if not isinstance(trait_value, str):
raise ValidationError(
f"Trait '{trait_name}' must be a string value."
)
trait_grants[role_name] = [trait_value]

# if event already exists, update it, otherwise create a new event
event_id = request.data.get("id")
domain_path = "{}{}/{}".format(
Expand Down
2 changes: 1 addition & 1 deletion app/eventyay/api/views/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ class ProductAddOnViewSet(viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
write_permission = 'can_change_products'
write_permission = 'can_change_items'

@cached_property
def product(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Generated by Django 5.2.5 on 2025-11-27 13:17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('base', '0002_alter_event_header_image_alter_event_locale_and_more'),
]

operations = [
migrations.AddField(
model_name='team',
name='can_video_create_channels',
field=models.BooleanField(default=False, help_text='Allows creating chat/video channels inside Eventyay Video.', verbose_name='Video: Can create channels'),
),
migrations.AddField(
model_name='team',
name='can_video_create_stages',
field=models.BooleanField(default=False, help_text='Allows creating livestream stages inside Eventyay Video.', verbose_name='Video: Can create stages'),
),
migrations.AddField(
model_name='team',
name='can_video_direct_message',
field=models.BooleanField(default=False, help_text='Grants permission to open new direct message conversations.', verbose_name='Video: Can send direct messages'),
),
migrations.AddField(
model_name='team',
name='can_video_manage_announcements',
field=models.BooleanField(default=False, help_text='Allows posting announcements in the Eventyay Video interface.', verbose_name='Video: Can create announcements'),
),
migrations.AddField(
model_name='team',
name='can_video_manage_configuration',
field=models.BooleanField(default=False, help_text='Allows editing the global Eventyay Video configuration.', verbose_name='Video: Can edit event configuration'),
),
migrations.AddField(
model_name='team',
name='can_video_manage_kiosks',
field=models.BooleanField(default=False, help_text='Allows managing kiosk displays inside Eventyay Video.', verbose_name='Video: Can create and edit kiosks'),
),
migrations.AddField(
model_name='team',
name='can_video_manage_rooms',
field=models.BooleanField(default=False, help_text='Allows editing and deleting rooms inside Eventyay Video.', verbose_name='Video: Can create and edit rooms'),
),
migrations.AddField(
model_name='team',
name='can_video_manage_users',
field=models.BooleanField(default=False, help_text='Allows moderating users (ban, silence, reactivate) in Eventyay Video.', verbose_name='Video: Can message, ban, and silence users'),
),
migrations.AddField(
model_name='team',
name='can_video_view_users',
field=models.BooleanField(default=False, help_text='Allows access to the user directory in Eventyay Video.', verbose_name='Video: Can view users'),
),
]
46 changes: 40 additions & 6 deletions app/eventyay/base/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from eventyay.common.text.path import path_with_hash
from eventyay.common.text.phrases import phrases
from eventyay.common.urls import EventUrls
from eventyay.core.permissions import Permission, SYSTEM_ROLES
from eventyay.core.permissions import MAX_PERMISSIONS_IF_SILENCED, Permission, SYSTEM_ROLES
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The import statement from eventyay.core.permissions import MAX_PERMISSIONS_IF_SILENCED, Permission, SYSTEM_ROLES adds MAX_PERMISSIONS_IF_SILENCED but this import is never used in the visible code changes. Consider removing unused imports to keep the code clean.

Suggested change
from eventyay.core.permissions import MAX_PERMISSIONS_IF_SILENCED, Permission, SYSTEM_ROLES
from eventyay.core.permissions import Permission, SYSTEM_ROLES

Copilot uses AI. Check for mistakes.
from eventyay.core.utils.json import CustomJSONEncoder
from eventyay.consts import TIMEZONE_CHOICES
from eventyay.helpers.database import GroupConcat
Expand All @@ -65,6 +65,7 @@
has_any_permission,
is_event_visible,
)
from eventyay.eventyay_common.video.permissions import VIDEO_PERMISSION_BY_FIELD, VIDEO_TRAIT_ROLE_MAP
from .auth import User
from ..settings import settings_hierarkey
from .mixins import OrderedModel, PretalxModel
Expand Down Expand Up @@ -1394,6 +1395,16 @@ def decode_token(self, token, allow_raise=False):
if exc and allow_raise:
raise exc

def _get_trait_grants_with_defaults(self):
base_trait_grants = self.trait_grants if self.trait_grants is not None else default_grants()
slug = getattr(self, "slug", None) or getattr(self, "id", None)
if not slug:
return base_trait_grants
augmented = dict(base_trait_grants)
for role, trait_name in VIDEO_TRAIT_ROLE_MAP.items():
augmented.setdefault(role, [f"eventyay-video-event-{slug}-{trait_name.replace('_', '-')}"])
return augmented

def has_permission_implicit(
self,
*,
Expand All @@ -1403,7 +1414,7 @@ def has_permission_implicit(
allow_empty_traits=True,
):
# Ensure trait_grants and roles are not None - use defaults if missing
event_trait_grants = self.trait_grants if self.trait_grants is not None else default_grants()
event_trait_grants = self._get_trait_grants_with_defaults()
event_roles = self.roles if self.roles is not None else default_roles()

for role, required_traits in event_trait_grants.items():
Expand Down Expand Up @@ -1519,9 +1530,16 @@ def get_all_permissions(self, user):
allow_empty_traits = user.type == User.UserType.PERSON

# Ensure trait_grants and roles are not None
event_trait_grants = self.trait_grants if self.trait_grants is not None else default_grants()
event_trait_grants = self._get_trait_grants_with_defaults()
event_roles = self.roles if self.roles is not None else default_roles()

# Track if user has any organizer/admin role
has_organizer_role = False
organizer_roles = {'admin', 'apiuser', 'scheduleuser', 'video_stage_manager',
'video_channel_manager', 'video_announcement_manager',
'video_user_viewer', 'video_user_moderator', 'video_room_manager',
'video_kiosk_manager', 'video_config_manager'}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

The organizer_roles set is defined but includes single quotes in some role names (e.g., 'admin', 'apiuser') and uses a set literal. However, this set is used to check if a role is an "organizer role" to decide whether to remove EVENT_CHAT_DIRECT permission. The role name 'video_direct_messaging' is missing from this set, but it should be included since it's a video-related organizer permission. Without it, users with only the video_direct_messaging role would not be treated as having an organizer role, which could lead to inconsistent permission behavior.

Suggested change
'video_kiosk_manager', 'video_config_manager'}
'video_kiosk_manager', 'video_config_manager', 'video_direct_messaging'}

Copilot uses AI. Check for mistakes.

for role, required_traits in event_trait_grants.items():
if (
isinstance(required_traits, list)
Expand All @@ -1531,9 +1549,25 @@ def get_all_permissions(self, user):
)
and (required_traits or allow_empty_traits)
):
result[self].update(event_roles.get(role, SYSTEM_ROLES.get(role, [])))

# Removed user.world_grants loop (attribute not present on unified User model)
if role in organizer_roles:
has_organizer_role = True

role_perms = event_roles.get(role, SYSTEM_ROLES.get(role, []))
result[self].update(role_perms)

# If user has organizer/admin role but doesn't have direct messaging trait, remove EVENT_CHAT_DIRECT
# This ensures organizers only get direct messaging if explicitly granted via team permissions
# Attendees (without organizer roles) always keep EVENT_CHAT_DIRECT
if has_organizer_role:
direct_messaging_def = VIDEO_PERMISSION_BY_FIELD.get('can_video_direct_message')
if direct_messaging_def:
direct_messaging_trait = direct_messaging_def.trait_value(self.slug)
traits = user.traits or []
has_direct_messaging_trait = direct_messaging_trait in traits

if not has_direct_messaging_trait:
direct_message_value = Permission.EVENT_CHAT_DIRECT.value
result[self] = {p for p in result[self] if (p if isinstance(p, str) else p.value) != direct_message_value}

for room in self.rooms.all():
room_trait_grants = room.trait_grants if room.trait_grants is not None else {}
Expand Down
46 changes: 46 additions & 0 deletions app/eventyay/base/models/organizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,52 @@ class Meta:
default=False,
)

can_video_create_stages = models.BooleanField(
default=False,
verbose_name=_('Video: Can create stages'),
help_text=_('Allows creating livestream stages inside Eventyay Video.'),
)
can_video_create_channels = models.BooleanField(
default=False,
verbose_name=_('Video: Can create channels'),
help_text=_('Allows creating chat/video channels inside Eventyay Video.'),
)
can_video_direct_message = models.BooleanField(
default=False,
verbose_name=_('Video: Can send direct messages'),
help_text=_('Grants permission to open new direct message conversations.'),
)
can_video_manage_announcements = models.BooleanField(
default=False,
verbose_name=_('Video: Can create announcements'),
help_text=_('Allows posting announcements in the Eventyay Video interface.'),
)
can_video_view_users = models.BooleanField(
default=False,
verbose_name=_('Video: Can view users'),
help_text=_('Allows access to the user directory in Eventyay Video.'),
)
can_video_manage_users = models.BooleanField(
default=False,
verbose_name=_('Video: Can message, ban, and silence users'),
help_text=_('Allows moderating users (ban, silence, reactivate) in Eventyay Video.'),
)
can_video_manage_rooms = models.BooleanField(
default=False,
verbose_name=_('Video: Can create and edit rooms'),
help_text=_('Allows editing and deleting rooms inside Eventyay Video.'),
)
can_video_manage_kiosks = models.BooleanField(
default=False,
verbose_name=_('Video: Can create and edit kiosks'),
help_text=_('Allows managing kiosk displays inside Eventyay Video.'),
)
can_video_manage_configuration = models.BooleanField(
default=False,
verbose_name=_('Video: Can edit event configuration'),
help_text=_('Allows editing the global Eventyay Video configuration.'),
)

@cached_property
def permission_set_display(self) -> set:
"""The same as :meth:`permission_set`, but with human-readable names."""
Expand Down
50 changes: 34 additions & 16 deletions app/eventyay/base/models/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@

def default_roles():
attendee = [
Permission.WORLD_VIEW,
Permission.WORLD_EXHIBITION_CONTACT,
Permission.WORLD_CHAT_DIRECT,
Permission.EVENT_VIEW,
Permission.EVENT_EXHIBITION_CONTACT,
Permission.EVENT_CHAT_DIRECT,
]
viewer = attendee + [Permission.ROOM_VIEW, Permission.ROOM_CHAT_READ]
participant = viewer + [
Expand All @@ -49,7 +49,7 @@ def default_roles():
Permission.ROOM_JANUSCALL_JOIN,
Permission.ROOM_ZOOM_JOIN,
]
room_creator = [Permission.WORLD_ROOMS_CREATE_CHAT]
room_creator = [Permission.EVENT_ROOMS_CREATE_CHAT]
room_owner = participant + [
Permission.ROOM_INVITE,
Permission.ROOM_DELETE,
Expand All @@ -67,27 +67,36 @@ def default_roles():
Permission.ROOM_QUESTION_MODERATE,
Permission.ROOM_POLL_EARLY_RESULTS,
Permission.ROOM_POLL_MANAGE,
Permission.WORLD_ANNOUNCE,
Permission.EVENT_ANNOUNCE,
]
admin = (
moderator
+ room_creator
+ [
Permission.WORLD_UPDATE,
Permission.EVENT_UPDATE,
Permission.ROOM_DELETE,
Permission.ROOM_UPDATE,
Permission.WORLD_ROOMS_CREATE_BBB,
Permission.WORLD_ROOMS_CREATE_STAGE,
Permission.WORLD_ROOMS_CREATE_EXHIBITION,
Permission.WORLD_ROOMS_CREATE_POSTER,
Permission.WORLD_USERS_LIST,
Permission.WORLD_USERS_MANAGE,
Permission.WORLD_GRAPHS,
Permission.WORLD_CONNECTIONS_UNLIMITED,
Permission.EVENT_ROOMS_CREATE_BBB,
Permission.EVENT_ROOMS_CREATE_STAGE,
Permission.EVENT_ROOMS_CREATE_EXHIBITION,
Permission.EVENT_ROOMS_CREATE_POSTER,
Permission.EVENT_USERS_LIST,
Permission.EVENT_USERS_MANAGE,
Permission.EVENT_GRAPHS,
Permission.EVENT_CONNECTIONS_UNLIMITED,
]
)
apiuser = admin + [Permission.WORLD_API, Permission.WORLD_SECRETS]
scheduleuser = [Permission.WORLD_API]
apiuser = admin + [Permission.EVENT_API, Permission.EVENT_SECRETS]
scheduleuser = [Permission.EVENT_API]
video_stage_manager = [Permission.EVENT_ROOMS_CREATE_STAGE]
video_channel_manager = [Permission.EVENT_ROOMS_CREATE_CHAT, Permission.EVENT_ROOMS_CREATE_BBB]
video_direct_messaging = [Permission.EVENT_CHAT_DIRECT]
video_announcement_manager = [Permission.EVENT_ANNOUNCE]
video_user_viewer = [Permission.EVENT_USERS_LIST]
video_user_moderator = [Permission.EVENT_USERS_MANAGE]
video_room_manager = [Permission.ROOM_UPDATE, Permission.ROOM_DELETE]
video_kiosk_manager = [Permission.EVENT_KIOSKS_MANAGE]
video_config_manager = [Permission.EVENT_UPDATE]
return {
"attendee": attendee,
"viewer": viewer,
Expand All @@ -99,6 +108,15 @@ def default_roles():
"admin": admin,
"apiuser": apiuser,
"scheduleuser": scheduleuser,
"video_stage_manager": video_stage_manager,
"video_channel_manager": video_channel_manager,
"video_direct_messaging": video_direct_messaging,
"video_announcement_manager": video_announcement_manager,
"video_user_viewer": video_user_viewer,
"video_user_moderator": video_user_moderator,
"video_room_manager": video_room_manager,
"video_kiosk_manager": video_kiosk_manager,
"video_config_manager": video_config_manager,
}


Expand Down
2 changes: 2 additions & 0 deletions app/eventyay/base/services/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ def get_event_config_for_user(event, user):
for p in event_perm_values:
if p == "event.view":
world_aliases.append("world:view")
elif p == "event.update":
world_aliases.append("world:update")
elif p.startswith("event:"):
world_aliases.append("world:" + p[len("event:"):])
merged_permissions = sorted(set(event_perm_values) | set(world_aliases))
Expand Down
18 changes: 18 additions & 0 deletions app/eventyay/control/forms/organizer_forms/team_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ class Meta:
'is_reviewer',
'force_hide_speaker_names',
'limit_tracks',
'can_video_create_stages',
'can_video_create_channels',
'can_video_direct_message',
'can_video_manage_announcements',
'can_video_view_users',
'can_video_manage_users',
'can_video_manage_rooms',
'can_video_manage_kiosks',
'can_video_manage_configuration',
]
widgets = {
'limit_events': forms.CheckboxSelectMultiple(
Expand Down Expand Up @@ -91,6 +100,15 @@ def clean(self):
'can_change_vouchers',
'can_change_submissions',
'is_reviewer',
'can_video_create_stages',
'can_video_create_channels',
'can_video_direct_message',
'can_video_manage_announcements',
'can_video_view_users',
'can_video_manage_users',
'can_video_manage_rooms',
'can_video_manage_kiosks',
'can_video_manage_configuration',
)
if not any(data.get(permission) for permission in permissions):
error = forms.ValidationError(
Expand Down
Loading