Skip to content
4 changes: 4 additions & 0 deletions app/eventyay/base/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@ def default_feature_flags():
'use_submission_comments': True,
'present_multiple_times': False,
'submission_public_review': True,
# Video/webapp feature flags
'chat-moderation': True,
'polls': True,
'schedule-control': True,
}


Expand Down
65 changes: 57 additions & 8 deletions app/eventyay/multidomain/maindomain_urlconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
from django.apps import apps
from django.core.serializers.json import DjangoJSONEncoder
from django.http import Http404, HttpResponse
from django.shortcuts import redirect
from django.urls import include, path, re_path
from django.utils.encoding import force_str
from django.utils.functional import Promise
from django.utils.timezone import now
from django.views.generic import TemplateView, View
from django.views.static import serve as static_serve
from django_scopes import scope
from i18nfield.strings import LazyI18nString

from eventyay.base.models import Event # Added for /video event context
from eventyay.cfp.views.event import EventStartpage
from eventyay.base.models.room import AnonymousInvite
from eventyay.common.urls import OrganizerSlugConverter # noqa: F401 (registers converter)

# Ticket-video integration: plugin URLs are auto-included via plugin handler below.
Expand All @@ -27,6 +29,7 @@
locale_patterns,
organizer_patterns,
)
from eventyay.cfp.views.event import EventStartpage

BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
WEBAPP_DIST_DIR = os.path.normpath(os.path.join(BASE_DIR, 'static', 'webapp'))
Expand Down Expand Up @@ -79,6 +82,12 @@ def safe_reverse(name, **kw):

base_path = event.urls.video_base.rstrip('/')
base_href = event.urls.video_base

# Merge default feature flags with event-specific flags to ensure
# newer features like 'polls' are available even for older events
from eventyay.base.models.event import default_feature_flags
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The inline import of default_feature_flags within the view method can impact performance on every request. Consider moving this import to the top of the file with the other imports for better performance and code organization.

Move line 88 to the top-level imports section (around line 20-21 where other model imports are located).

Copilot uses AI. Check for mistakes.
features = {**default_feature_flags(), **(getattr(event, 'feature_flags', {}) or {})}

injected = {
'api': {
'base': api_base,
Expand All @@ -91,7 +100,7 @@ def safe_reverse(name, **kw):
'scheduleImport': safe_reverse('storage:schedule_import', event_id=event.pk) or '',
'systemlog': safe_reverse('live:systemlog') or '',
},
'features': getattr(event, 'feature_flags', {}) or {},
'features': features,
'externalAuthUrl': getattr(event, 'external_auth_url', None),
'locale': event.locale,
'date_locale': cfg.get('date_locale', 'en-ie'),
Expand Down Expand Up @@ -150,6 +159,34 @@ def get(self, request, path='', *args, **kwargs):
raise Http404()


class AnonymousInviteRedirectView(View):
"""
Handle anonymous room invite short tokens (e.g., /eGHhXr/).
Redirects to the video SPA standalone anonymous room view:
/{organizer}/{event}/video/standalone/{room_id}/anonymous#invite={token}
"""
def get(self, request, token, *args, **kwargs):
try:
invite = AnonymousInvite.objects.select_related(
'event', 'event__organizer', 'room'
).get(
short_token=token,
expires__gte=now(),
)
except AnonymousInvite.DoesNotExist:
raise Http404("Invalid or expired anonymous room link")

# Build redirect URL to the video SPA standalone anonymous view
event = invite.event
organizer_slug = event.organizer.slug
event_slug = event.slug
room_id = invite.room_id

# Redirect to /{organizer}/{event}/video/standalone/{room_id}/anonymous#invite={token}
redirect_url = f"/{organizer_slug}/{event_slug}/video/standalone/{room_id}/anonymous#invite={token}"
return redirect(redirect_url)


presale_patterns_main = [
path(
'',
Expand Down Expand Up @@ -271,31 +308,43 @@ def get(self, request, path='', *args, **kwargs):
include(
[
# Video patterns under {organizer}/{event}/video/
re_path(r'^video/assets/(?P<path>.*)$', VideoAssetView.as_view(), name='video.assets'),
re_path(
r'^video/(?P<path>[^?]*\.[a-zA-Z0-9._-]+)$', VideoAssetView.as_view(), name='video.assets.file'
),
path(
'video/<path:path>',
r'^video/(?P<path>[^?]*\.[a-zA-Z0-9._-]+)$',
VideoAssetView.as_view(),
name='video.assets',
name='video.assets.file',
),
# The frontend Video SPA app is not served by Nginx so the Django view needs to
# serve all paths under /video/ to allow client-side routing.
re_path(r'^video(?:/.*)?$', VideoSPAView.as_view(), name='video.spa'),
path('talk/', EventStartpage.as_view(), name='event.talk'),
re_path(r'^talk/?$', EventStartpage.as_view(), name='event.talk'),
path('', include(('eventyay.agenda.urls', 'agenda'))),
path('', include(('eventyay.cfp.urls', 'cfp'))),
]
),
),
]

# Anonymous room invite short token pattern (6 characters)
# Must be placed before presale_patterns_main to avoid conflict with organizer slugs
# The token uses characters: abcdefghijklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789
# (excludes visually confusing characters: l, o, I, O, 0)
anonymous_invite_patterns = [
re_path(
r'^(?P<token>[a-km-np-zA-HJ-NP-Z1-9]{6})/?$',
AnonymousInviteRedirectView.as_view(),
name='anonymous.invite.redirect',
),
]

urlpatterns = (
common_patterns
+ storage_patterns
# The plugins patterns must be before presale_patterns_main
# to avoid misdetection of plugin prefixes and organizer/event slugs.
+ plugin_patterns
# Anonymous invite short token redirects (before presale to avoid slug conflict)
+ anonymous_invite_patterns
+ presale_patterns_main
+ unified_event_patterns
)
Expand Down
3 changes: 2 additions & 1 deletion app/eventyay/webapp/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ async function init({ token, inviteToken }) {
// resolved when components (RouterLink) are rendered.
await router.replace(relativePath).catch(() => {})

const route = router.resolve(relativePath).route
// In Vue Router 4, resolve() returns the route object directly (no .route property)
const route = router.resolve(relativePath)
const anonymousRoomId = route?.name === 'standalone:anonymous' ? route?.params?.roomId : null

window.vapp = app.mount('#app')
Expand Down