Skip to content

Commit 2bb8650

Browse files
unified admin base
1 parent d4d88a8 commit 2bb8650

File tree

53 files changed

+1752
-1520
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1752
-1520
lines changed

app/eventyay/api/serializers/organizer.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,18 @@ class Meta:
121121
'can_view_vouchers',
122122
'can_change_vouchers',
123123
'can_checkin_orders',
124+
'can_change_submissions',
125+
'is_reviewer',
126+
'force_hide_speaker_names',
127+
'can_video_create_stages',
128+
'can_video_create_channels',
129+
'can_video_direct_message',
130+
'can_video_manage_announcements',
131+
'can_video_view_users',
132+
'can_video_manage_users',
133+
'can_video_manage_rooms',
134+
'can_video_manage_kiosks',
135+
'can_video_manage_configuration',
124136
)
125137

126138
def validate(self, data):

app/eventyay/api/views/event.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
# from pretix.presale.views.organizer import filter_qs_by_attr # commented out
6363
from eventyay.api.task import configure_video_settings_for_talks
6464
from eventyay.api.utils import get_protocol
65+
from eventyay.eventyay_common.video.permissions import VIDEO_TRAIT_ROLE_MAP
6566

6667
logger = logging.getLogger(__name__)
6768

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

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

328-
attendee_trait_grants = request.data.get("traits", {}).get("attendee", "")
329-
if not isinstance(attendee_trait_grants, str):
329+
traits_payload = request.data.get("traits") or {}
330+
if not isinstance(traits_payload, dict):
331+
raise ValidationError("Traits must be provided as an object.")
332+
333+
attendee_trait_grants = traits_payload.get("attendee", "")
334+
if attendee_trait_grants and not isinstance(attendee_trait_grants, str):
330335
raise ValidationError("Attendee traits must be a string")
331336

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

345+
for trait_name, role_name in VIDEO_TRAIT_ROLE_MAP.items():
346+
trait_value = traits_payload.get(trait_name, "")
347+
if trait_value:
348+
if not isinstance(trait_value, str):
349+
raise ValidationError(
350+
f"Trait '{trait_name}' must be a string value."
351+
)
352+
trait_grants[role_name] = [trait_value]
353+
340354
# if event already exists, update it, otherwise create a new event
341355
event_id = request.data.get("id")
342356
domain_path = "{}{}/{}".format(

app/eventyay/api/views/product.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class ProductAddOnViewSet(viewsets.ModelViewSet):
264264
ordering_fields = ('id', 'position')
265265
ordering = ('id',)
266266
permission = None
267-
write_permission = 'can_change_products'
267+
write_permission = 'can_change_items'
268268

269269
@cached_property
270270
def product(self):
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Generated by Django 5.2.5 on 2025-11-27 13:17
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('base', '0002_alter_event_header_image_alter_event_locale_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='team',
15+
name='can_video_create_channels',
16+
field=models.BooleanField(default=False, help_text='Allows creating chat/video channels inside Eventyay Video.', verbose_name='Video: Can create channels'),
17+
),
18+
migrations.AddField(
19+
model_name='team',
20+
name='can_video_create_stages',
21+
field=models.BooleanField(default=False, help_text='Allows creating livestream stages inside Eventyay Video.', verbose_name='Video: Can create stages'),
22+
),
23+
migrations.AddField(
24+
model_name='team',
25+
name='can_video_direct_message',
26+
field=models.BooleanField(default=False, help_text='Grants permission to open new direct message conversations.', verbose_name='Video: Can send direct messages'),
27+
),
28+
migrations.AddField(
29+
model_name='team',
30+
name='can_video_manage_announcements',
31+
field=models.BooleanField(default=False, help_text='Allows posting announcements in the Eventyay Video interface.', verbose_name='Video: Can create announcements'),
32+
),
33+
migrations.AddField(
34+
model_name='team',
35+
name='can_video_manage_configuration',
36+
field=models.BooleanField(default=False, help_text='Allows editing the global Eventyay Video configuration.', verbose_name='Video: Can edit event configuration'),
37+
),
38+
migrations.AddField(
39+
model_name='team',
40+
name='can_video_manage_kiosks',
41+
field=models.BooleanField(default=False, help_text='Allows managing kiosk displays inside Eventyay Video.', verbose_name='Video: Can create and edit kiosks'),
42+
),
43+
migrations.AddField(
44+
model_name='team',
45+
name='can_video_manage_rooms',
46+
field=models.BooleanField(default=False, help_text='Allows editing and deleting rooms inside Eventyay Video.', verbose_name='Video: Can create and edit rooms'),
47+
),
48+
migrations.AddField(
49+
model_name='team',
50+
name='can_video_manage_users',
51+
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'),
52+
),
53+
migrations.AddField(
54+
model_name='team',
55+
name='can_video_view_users',
56+
field=models.BooleanField(default=False, help_text='Allows access to the user directory in Eventyay Video.', verbose_name='Video: Can view users'),
57+
),
58+
]

app/eventyay/base/models/event.py

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from collections import OrderedDict, defaultdict
66
from datetime import datetime, time, timedelta
77
from operator import attrgetter
8-
from typing import List
98
from urllib.parse import urljoin, urlparse
109
from zoneinfo import ZoneInfo
1110

@@ -50,7 +49,14 @@
5049
from eventyay.common.text.phrases import phrases
5150
from eventyay.common.urls import EventUrls
5251
from eventyay.consts import TIMEZONE_CHOICES
53-
from eventyay.core.permissions import MAX_PERMISSIONS_IF_SILENCED, SYSTEM_ROLES, Permission
52+
from eventyay.core.permissions import (
53+
MAX_PERMISSIONS_IF_SILENCED,
54+
ORGANIZER_ROLES,
55+
SYSTEM_ROLES,
56+
Permission,
57+
normalize_permission_value,
58+
traits_match_required,
59+
)
5460
from eventyay.core.utils.json import CustomJSONEncoder
5561
from eventyay.helpers.database import GroupConcat
5662
from eventyay.helpers.daterange import daterange
@@ -62,7 +68,7 @@
6268
has_any_permission,
6369
is_event_visible,
6470
)
65-
71+
from eventyay.eventyay_common.video.permissions import VIDEO_PERMISSION_BY_FIELD, VIDEO_TRAIT_ROLE_MAP
6672
from ..settings import settings_hierarkey
6773
from .auth import User
6874
from .mixins import OrderedModel, PretalxModel
@@ -85,7 +91,6 @@ def default_roles():
8591
attendee = [
8692
Permission.EVENT_VIEW,
8793
Permission.EVENT_EXHIBITION_CONTACT,
88-
Permission.EVENT_CHAT_DIRECT,
8994
]
9095
viewer = attendee + [Permission.ROOM_VIEW, Permission.ROOM_CHAT_READ]
9196
participant = viewer + [
@@ -1352,38 +1357,73 @@ def decode_token(self, token, allow_raise=False):
13521357
if exc and allow_raise:
13531358
raise exc
13541359

1360+
def _get_trait_grants_with_defaults(self):
1361+
base_trait_grants = self.trait_grants if self.trait_grants is not None else default_grants()
1362+
slug = getattr(self, "slug", None) or getattr(self, "id", None)
1363+
if not slug:
1364+
return base_trait_grants
1365+
augmented = dict(base_trait_grants)
1366+
for role, trait_name in VIDEO_TRAIT_ROLE_MAP.items():
1367+
augmented.setdefault(role, [f"eventyay-video-event-{slug}-{trait_name.replace('_', '-')}"])
1368+
return augmented
1369+
1370+
def _remove_direct_messaging_if_unauthorized(self, result, user_traits):
1371+
"""Remove EVENT_CHAT_DIRECT permission if user doesn't have the direct messaging trait.
1372+
1373+
Args:
1374+
result: Permission result dictionary to modify
1375+
user_traits: List of user traits
1376+
"""
1377+
direct_messaging_def = VIDEO_PERMISSION_BY_FIELD.get('can_video_direct_message')
1378+
if not direct_messaging_def:
1379+
return
1380+
1381+
direct_messaging_trait = direct_messaging_def.trait_value(self.slug)
1382+
has_direct_messaging_trait = direct_messaging_trait in user_traits
1383+
1384+
if not has_direct_messaging_trait:
1385+
direct_message_value = Permission.EVENT_CHAT_DIRECT.value
1386+
result[self] = {
1387+
p for p in result[self]
1388+
if normalize_permission_value(p) != direct_message_value
1389+
}
1390+
13551391
def has_permission_implicit(
13561392
self,
13571393
*,
13581394
traits,
1359-
permissions: List[Permission],
1395+
permissions: list[Permission],
13601396
room=None,
13611397
allow_empty_traits=True,
13621398
):
13631399
# Ensure trait_grants and roles are not None - use defaults if missing
1364-
event_trait_grants = self.trait_grants if self.trait_grants is not None else default_grants()
1400+
event_trait_grants = self._get_trait_grants_with_defaults()
13651401
event_roles = self.roles if self.roles is not None else default_roles()
13661402

13671403
for role, required_traits in event_trait_grants.items():
13681404
if (
1369-
isinstance(required_traits, list)
1370-
and all(any(x in traits for x in (r if isinstance(r, list) else [r])) for r in required_traits)
1405+
traits_match_required(traits, required_traits)
13711406
and (required_traits or allow_empty_traits)
13721407
):
13731408
role_permissions = event_roles.get(role, SYSTEM_ROLES.get(role, []))
1374-
if any(p in role_permissions or p.value in role_permissions for p in permissions):
1409+
if any(
1410+
normalize_permission_value(p) in role_permissions
1411+
for p in permissions
1412+
):
13751413
return True
13761414

13771415
if room:
13781416
room_trait_grants = room.trait_grants if room.trait_grants is not None else {}
13791417
for role, required_traits in room_trait_grants.items():
13801418
if (
1381-
isinstance(required_traits, list)
1382-
and all(any(x in traits for x in (r if isinstance(r, list) else [r])) for r in required_traits)
1419+
traits_match_required(traits, required_traits)
13831420
and (required_traits or allow_empty_traits)
13841421
):
13851422
role_permissions = event_roles.get(role, SYSTEM_ROLES.get(role, []))
1386-
if any(p in role_permissions or p.value in role_permissions for p in permissions):
1423+
if any(
1424+
normalize_permission_value(p) in role_permissions
1425+
for p in permissions
1426+
):
13871427
return True
13881428

13891429
# Return False if no permission was granted
@@ -1405,7 +1445,7 @@ def has_permission(self, *, user, permission: Permission, room=None):
14051445
return False
14061446

14071447
if self.has_permission_implicit(
1408-
traits=user.traits,
1448+
traits=user.traits or [],
14091449
permissions=permission,
14101450
room=room,
14111451
allow_empty_traits=user.type == User.UserType.PERSON,
@@ -1415,7 +1455,8 @@ def has_permission(self, *, user, permission: Permission, room=None):
14151455
roles = user.get_role_grants(room)
14161456
event_roles = self.roles if self.roles is not None else default_roles()
14171457
for r in roles:
1418-
if any(p.value in event_roles.get(r, SYSTEM_ROLES.get(r, [])) for p in permission):
1458+
role_perms = event_roles.get(r, SYSTEM_ROLES.get(r, []))
1459+
if any(normalize_permission_value(p) in role_perms for p in permission):
14191460
return True
14201461

14211462
async def has_permission_async(self, *, user, permission: Permission, room=None):
@@ -1434,7 +1475,7 @@ async def has_permission_async(self, *, user, permission: Permission, room=None)
14341475
return False
14351476

14361477
if self.has_permission_implicit(
1437-
traits=user.traits,
1478+
traits=user.traits or [],
14381479
permissions=permission,
14391480
room=room,
14401481
allow_empty_traits=user.type == User.UserType.PERSON,
@@ -1444,7 +1485,8 @@ async def has_permission_async(self, *, user, permission: Permission, room=None)
14441485
roles = await user.get_role_grants_async(room)
14451486
event_roles = self.roles if self.roles is not None else default_roles()
14461487
for r in roles:
1447-
if any(p.value in event_roles.get(r, SYSTEM_ROLES.get(r, [])) for p in permission):
1488+
role_perms = event_roles.get(r, SYSTEM_ROLES.get(r, []))
1489+
if any(normalize_permission_value(p) in role_perms for p in permission):
14481490
return True
14491491

14501492
def get_all_permissions(self, user):
@@ -1455,25 +1497,38 @@ def get_all_permissions(self, user):
14551497
allow_empty_traits = user.type == User.UserType.PERSON
14561498

14571499
# Ensure trait_grants and roles are not None
1458-
event_trait_grants = self.trait_grants if self.trait_grants is not None else default_grants()
1500+
event_trait_grants = self._get_trait_grants_with_defaults()
14591501
event_roles = self.roles if self.roles is not None else default_roles()
14601502

1503+
user_traits = user.traits or []
1504+
14611505
for role, required_traits in event_trait_grants.items():
14621506
if (
1463-
isinstance(required_traits, list)
1464-
and all(any(x in user.traits for x in (r if isinstance(r, list) else [r])) for r in required_traits)
1507+
traits_match_required(user_traits, required_traits)
14651508
and (required_traits or allow_empty_traits)
14661509
):
1467-
result[self].update(event_roles.get(role, SYSTEM_ROLES.get(role, [])))
1468-
1469-
# Removed user.world_grants loop (attribute not present on unified User model)
1510+
role_perms = event_roles.get(role, SYSTEM_ROLES.get(role, []))
1511+
result[self].update(role_perms)
1512+
1513+
# Admin mode in the ticket/talk system is represented by the ``admin`` trait on the video side.
1514+
# When admin mode is ON, the user has the ``admin`` trait and should retain full access.
1515+
admin_mode_active = "admin" in user_traits
1516+
1517+
if admin_mode_active:
1518+
# Grant all video manager permissions when admin mode is active
1519+
for role_name in ORGANIZER_ROLES:
1520+
role_perms = event_roles.get(role_name, SYSTEM_ROLES.get(role_name, []))
1521+
result[self].update(role_perms)
1522+
else:
1523+
# Remove EVENT_CHAT_DIRECT from ALL users unless they have the direct messaging trait.
1524+
# Only users with can_video_direct_message team permission get the video_direct_messaging trait.
1525+
self._remove_direct_messaging_if_unauthorized(result, user_traits)
14701526

14711527
for room in self.rooms.all():
14721528
room_trait_grants = room.trait_grants if room.trait_grants is not None else {}
14731529
for role, required_traits in room_trait_grants.items():
14741530
if (
1475-
isinstance(required_traits, list)
1476-
and all(any(x in user.traits for x in (r if isinstance(r, list) else [r])) for r in required_traits)
1531+
traits_match_required(user_traits, required_traits)
14771532
and (required_traits or allow_empty_traits)
14781533
):
14791534
result[room].update(event_roles.get(role, SYSTEM_ROLES.get(role, [])))

app/eventyay/base/models/organizer.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,52 @@ class Meta:
447447
default=False,
448448
)
449449

450+
can_video_create_stages = models.BooleanField(
451+
default=False,
452+
verbose_name=_('Video: Can create stages'),
453+
help_text=_('Allows creating livestream stages inside Eventyay Video.'),
454+
)
455+
can_video_create_channels = models.BooleanField(
456+
default=False,
457+
verbose_name=_('Video: Can create channels'),
458+
help_text=_('Allows creating chat/video channels inside Eventyay Video.'),
459+
)
460+
can_video_direct_message = models.BooleanField(
461+
default=False,
462+
verbose_name=_('Video: Can send direct messages'),
463+
help_text=_('Grants permission to open new direct message conversations.'),
464+
)
465+
can_video_manage_announcements = models.BooleanField(
466+
default=False,
467+
verbose_name=_('Video: Can create announcements'),
468+
help_text=_('Allows posting announcements in the Eventyay Video interface.'),
469+
)
470+
can_video_view_users = models.BooleanField(
471+
default=False,
472+
verbose_name=_('Video: Can view users'),
473+
help_text=_('Allows access to the user directory in Eventyay Video.'),
474+
)
475+
can_video_manage_users = models.BooleanField(
476+
default=False,
477+
verbose_name=_('Video: Can message, ban, and silence users'),
478+
help_text=_('Allows moderating users (ban, silence, reactivate) in Eventyay Video.'),
479+
)
480+
can_video_manage_rooms = models.BooleanField(
481+
default=False,
482+
verbose_name=_('Video: Can create and edit rooms'),
483+
help_text=_('Allows editing and deleting rooms inside Eventyay Video.'),
484+
)
485+
can_video_manage_kiosks = models.BooleanField(
486+
default=False,
487+
verbose_name=_('Video: Can create and edit kiosks'),
488+
help_text=_('Allows managing kiosk displays inside Eventyay Video.'),
489+
)
490+
can_video_manage_configuration = models.BooleanField(
491+
default=False,
492+
verbose_name=_('Video: Can edit event configuration'),
493+
help_text=_('Allows editing the global Eventyay Video configuration.'),
494+
)
495+
450496
@cached_property
451497
def permission_set_display(self) -> set:
452498
"""The same as :meth:`permission_set`, but with human-readable names."""

0 commit comments

Comments
 (0)