Skip to content

Commit 110ebaa

Browse files
authored
Merge branch 'enext' into organizer-creation
2 parents 949e03b + 3f11827 commit 110ebaa

File tree

95 files changed

+21954
-685
lines changed

Some content is hidden

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

95 files changed

+21954
-685
lines changed

app/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ dist/
88
*.bak
99
eventyay/static/jsi18n/
1010
node_modules/
11-
11+
.zed

app/eventyay/agenda/templates/agenda/schedule.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
</a>
2525
<hr>
2626
{% for exporter in exporters %}
27-
<a class="dropdown-item" href="{{ exporter.urls.base }}" role="menuitem" tabindex="-1">
27+
<a class="dropdown-item" href="{{ exporter.urls.base }}" role="menuitem" tabindex="-1" target="_blank">
2828
{% if exporter.icon|slice:":3" == "fa-" %}
2929
<span class="fa {{ exporter.icon }} export-icon"></span>
3030
{% else %}

app/eventyay/agenda/views/talk.py

Lines changed: 133 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from enum import StrEnum
2+
import logging
13
import datetime as dt
2-
from urllib.parse import unquote, urlparse
4+
from http import HTTPStatus
5+
from urllib.parse import unquote, urlparse, urljoin
6+
from typing import TypeVar
37

48
import jwt
59
import requests
@@ -23,11 +27,27 @@
2327
PermissionRequired,
2428
SocialMediaCardMixin,
2529
)
26-
from eventyay.base.models import TalkSlot
30+
from eventyay.base.models import Event, TalkSlot, User
2731
from eventyay.submission.forms import FeedbackForm
2832
from eventyay.base.models import Submission, SubmissionStates
2933

3034

35+
logger = logging.getLogger(__name__)
36+
37+
38+
class TicketCheckResult(StrEnum):
39+
HAS_TICKET = 'has_ticket'
40+
MISCONFIGURED = 'missing_configuration'
41+
NO_TICKET = 'no_ticket'
42+
43+
44+
class VideoJoinError(StrEnum):
45+
# The string value looks diffrent from the enum name
46+
# because other code may depend on this string value.
47+
NOT_ALLOWED = 'user_not_allowed'
48+
MISCONFIGURED = 'missing_configuration'
49+
50+
3151
class TalkMixin(PermissionRequired):
3252
permission_required = 'base.view_public_submission'
3353

@@ -253,82 +273,117 @@ class OnlineVideoJoin(EventPermissionRequired, View):
253273
permission_required = 'base.view_schedule'
254274

255275
def get(self, request, *args, **kwargs):
256-
# First check video is configured or not
257-
if (
258-
'eventyay_venueless' not in request.event.plugin_list
259-
or not request.event.venueless_settings
260-
or not request.event.venueless_settings.join_url
261-
or not request.event.venueless_settings.secret
262-
or not request.event.venueless_settings.issuer
263-
or not request.event.venueless_settings.audience
264-
):
265-
return HttpResponse(status=403, content='missing_configuration')
266-
267276
if not request.user.is_authenticated:
268-
# redirect to login page if user not logged in yet
269-
return HttpResponse(status=403, content='user_not_allowed')
270-
271-
# prepare event data to check from ticket
272-
if 'ticket_link' not in request.event.display_settings:
273-
return HttpResponse(status=403, content='missing_configuration')
274-
275-
base_url, organizer, event = self.retrieve_info_from_url(request.event.display_settings['ticket_link'])
276-
277-
if not organizer or not event or not base_url:
278-
return HttpResponse(status=403, content='missing_configuration')
279-
280-
check_payload = {'user_email': request.user.email}
281-
282-
# call to ticket to check if user order ticket yet or not
283-
response = requests.post(f'{base_url}/api/v1/{organizer}/{event}/ticket-check', json=check_payload)
284-
285-
if response.status_code != 200:
286-
return HttpResponse(status=403, content='user_not_allowed')
287-
288-
else:
289-
# Redirect user to online event
290-
iat = dt.datetime.utcnow()
291-
exp = iat + dt.timedelta(days=30)
292-
profile = {
293-
'display_name': request.user.name,
294-
'fields': {
295-
'eventyay_id': request.user.code,
296-
},
297-
}
298-
if request.user.avatar_url:
299-
profile['profile_picture'] = request.user.get_avatar_url(request.event)
300-
301-
payload = {
302-
'iss': request.event.venueless_settings.issuer,
303-
'aud': request.event.venueless_settings.audience,
304-
'exp': exp,
305-
'iat': iat,
306-
'uid': encode_email(request.user.email),
307-
'profile': profile,
308-
'traits': list(
309-
{
310-
f'eventyay-video-event-{request.event.slug}',
311-
}
312-
),
313-
}
314-
token = jwt.encode(payload, request.event.venueless_settings.secret, algorithm='HS256')
315-
316-
return JsonResponse(
277+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.NOT_ALLOWED)
278+
279+
# First check video is configured or not
280+
if 'pretalx_venueless' not in request.event.plugin_list:
281+
logger.info('pretalx_venueless plugin is not enabled.')
282+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
283+
event = request.event
284+
logger.info('To check settings for event %s', event)
285+
if not (venueless_settings := event.venueless_settings):
286+
logger.info('venueless settings is missing.')
287+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
288+
if not venueless_settings.join_url:
289+
logger.info('venueless_settings.join_url is missing.')
290+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
291+
if not venueless_settings.secret:
292+
logger.info('venueless_settings.secret is missing.')
293+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
294+
if not venueless_settings.issuer:
295+
logger.info('venueless_settings.issuer is missing.')
296+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
297+
if not venueless_settings.audience:
298+
logger.info('venueless_settings.audience is missing.')
299+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
300+
301+
# If the logged-in user does not have "orga.view_schedule" permission, we check
302+
# if he/she owns a ticket.
303+
if not request.user.has_perm('agenda.view_schedule', event):
304+
res = check_user_owning_ticket(request.user, event)
305+
if res == TicketCheckResult.NO_TICKET:
306+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.NOT_ALLOWED)
307+
if res == TicketCheckResult.MISCONFIGURED:
308+
return HttpResponse(status=HTTPStatus.FORBIDDEN, content=VideoJoinError.MISCONFIGURED)
309+
310+
# Redirect user to online event
311+
iat = dt.datetime.now(dt.UTC)
312+
exp = iat + dt.timedelta(days=30)
313+
profile = {
314+
"display_name": request.user.name,
315+
"fields": {
316+
"pretalx_id": request.user.code,
317+
},
318+
}
319+
if request.user.avatar_url:
320+
profile["profile_picture"] = request.user.get_avatar_url(request.event)
321+
322+
payload = {
323+
"iss": venueless_settings.issuer,
324+
"aud": venueless_settings.audience,
325+
"exp": exp,
326+
"iat": iat,
327+
"uid": encode_email(request.user.email),
328+
"profile": profile,
329+
"traits": list(
317330
{
318-
'redirect_url': '{}/#token={}'.format(request.event.venueless_settings.join_url, token).replace(
319-
'//#', '/#'
320-
)
321-
},
322-
status=200,
323-
)
331+
f"eventyay-video-event-{request.event.slug}",
332+
}
333+
),
334+
}
335+
token = jwt.encode(
336+
payload, venueless_settings.secret, algorithm="HS256"
337+
)
338+
redirect_url = urljoin(venueless_settings.join_url, f'#token={token}')
339+
logger.info('Redirect URL to Video: %s', redirect_url)
340+
return JsonResponse(
341+
{
342+
'redirect_url': redirect_url
343+
},
344+
status=HTTPStatus.OK,
345+
)
346+
324347

325-
def retrieve_info_from_url(self, url):
326-
parsed_url = urlparse(url)
327-
ticket_host = settings.EVENTYAY_TICKET_BASE_PATH
328-
path = parsed_url.path
329-
parts = path.strip('/').split('/')
330-
if len(parts) >= 2:
331-
organizer, event = parts[-2:]
332-
return ticket_host, unquote(organizer), unquote(event)
333-
else:
334-
return ticket_host, None, None
348+
_T = TypeVar('_T', str, None)
349+
# We use TypeVar because the 2nd and 3rd items must be both `str` or both `None` at the same time.
350+
# The annotation `tuple[str, str | None, str | None]` doesn't satisfy this requirement.
351+
def extract_event_info_from_url(url: str) -> tuple[str, _T, _T]:
352+
parsed_url = urlparse(url)
353+
ticket_host = settings.EVENTYAY_TICKET_BASE_PATH
354+
path = parsed_url.path
355+
parts = path.strip("/").split("/")
356+
if len(parts) >= 2:
357+
organizer, event = parts[-2:]
358+
return ticket_host, unquote(organizer), unquote(event)
359+
return ticket_host, None, None
360+
361+
362+
def check_user_owning_ticket(user: User, event: Event) -> TicketCheckResult:
363+
"""
364+
Call eventyay-ticket API to check if user owns ticket for this event.
365+
366+
# NOTE: It doesn't work with the Docker setup for development, because we use fake domain then,
367+
and inside the container, the fake domain points to the container itself, not the host.
368+
"""
369+
if 'ticket_link' not in event.display_settings:
370+
logger.info('display_settings[ticket_link] is missing.')
371+
return TicketCheckResult.MISCONFIGURED
372+
base_url, organizer, event = extract_event_info_from_url(
373+
event.display_settings['ticket_link']
374+
)
375+
if not organizer or not event or not base_url:
376+
logger.info('display_settings[ticket_link] is not valid.')
377+
return TicketCheckResult.MISCONFIGURED
378+
check_payload = {'user_email': user.email}
379+
# call to ticket to check if user order ticket yet or not
380+
api_url = urljoin(base_url, f'api/v1/{organizer}/{event}/ticket-check')
381+
logger.info('To call API %s', api_url)
382+
# In development, we disable the SSL verification.
383+
response = requests.post(api_url, json=check_payload, verify=(not settings.DEBUG))
384+
385+
if response.status_code != HTTPStatus.OK:
386+
logger.debug('Response from eventyay-ticket: %s', response.text)
387+
logger.info('user is not allowed to join online event.')
388+
return TicketCheckResult.NO_TICKET
389+
return TicketCheckResult.HAS_TICKET

app/eventyay/agenda/views/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
def is_visible(exporter, request, public=False):
1818
if not public:
19-
return request.user.has_perm('base.orga_view_schedule', request.event)
19+
return request.user.is_authenticated
2020
if not request.user.has_perm('base.list_schedule', request.event):
2121
return False
2222
if hasattr(exporter, 'is_public'):

app/eventyay/api/serializers/event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ class EventSettingsSerializer(SettingsSerializer):
778778
'primary_font',
779779
'logo_image',
780780
'logo_image_large',
781+
'event_logo_image',
781782
'logo_show_title',
782783
'og_image',
783784
]

app/eventyay/base/configurations/default_setting.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2055,10 +2055,32 @@ def primary_font_kwargs():
20552055
label=_('Header image'),
20562056
ext_whitelist=('.png', '.jpg', '.gif', '.jpeg'),
20572057
max_size=10 * 1024 * 1024,
2058+
help_text=_(
2059+
'If you provide a header image, it will be displayed instead of your event’s color '
2060+
'and/or header pattern at the top of all event pages. It will be center-aligned, '
2061+
'so when the window shrinks, the center parts will continue to be displayed, you '
2062+
'can increase the size with the setting below. The image will not be stretched. '
2063+
'Please do not upload files larger than 10.0MB!'
2064+
),
2065+
),
2066+
'serializer_class': UploadedFileField,
2067+
'serializer_kwargs': dict(
2068+
allowed_types=['image/png', 'image/jpeg', 'image/gif'],
2069+
max_size=10 * 1024 * 1024,
2070+
),
2071+
},
2072+
'event_logo_image': {
2073+
'default': None,
2074+
'type': File,
2075+
'form_class': ExtFileField,
2076+
'form_kwargs': dict(
2077+
label=_('Logo'),
2078+
ext_whitelist=('.png', '.jpg', '.gif', '.jpeg'),
2079+
max_size=10 * 1024 * 1024,
20582080
help_text=_(
20592081
'If you provide a logo image, we will by default not show your event name and date '
2060-
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
2061-
'can increase the size with the setting below. We recommend not using small details on the picture '
2082+
'in the page header. By default, the logo will be scaled down to a height of 140px. '
2083+
'We recommend not using small details on the picture '
20622084
'as it will be resized on smaller screens.'
20632085
),
20642086
),
@@ -2084,7 +2106,7 @@ def primary_font_kwargs():
20842106
'form_class': forms.BooleanField,
20852107
'serializer_class': serializers.BooleanField,
20862108
'form_kwargs': dict(
2087-
label=_('Show event title even if a header image is present'),
2109+
label=_('Show event title even if a logo image is present'),
20882110
help_text=_('The title will only be shown on the event front page.'),
20892111
),
20902112
},

app/eventyay/base/email.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from itertools import groupby
1111
from pathlib import Path
1212
from smtplib import SMTPResponseException
13+
from typing import Iterable
1314

1415
from css_inline import inline as inline_css
1516
from django.conf import settings
@@ -385,7 +386,7 @@ def render_sample(self, event):
385386
return self._sample
386387

387388

388-
def get_available_placeholders(event, base_parameters):
389+
def get_available_placeholders(event: Event, base_parameters: Iterable[str]) -> dict[str, BaseMailTextPlaceholder]:
389390
if 'order' in base_parameters:
390391
base_parameters.append('invoice_address')
391392
base_parameters.append('position_or_address')
@@ -419,6 +420,7 @@ def get_email_context(**kwargs):
419420
for v in val:
420421
if all(rp in kwargs for rp in v.required_context):
421422
ctx[v.identifier] = v.render(kwargs)
423+
logger.info('Email context: %s', ctx)
422424
return ctx
423425

424426

@@ -469,12 +471,15 @@ def generate_sample_video_url():
469471

470472

471473
@receiver(register_mail_placeholders, dispatch_uid='pretixbase_register_mail_placeholders')
472-
def base_placeholders(sender, **kwargs):
474+
def base_placeholders(sender: Event, **kwargs):
473475
from eventyay.multidomain.urlreverse import (
474476
build_absolute_uri,
475477
build_join_video_url,
476478
)
477-
479+
def render_video_join_link(event: Event, order) -> str:
480+
url = build_join_video_url(event, order)
481+
# TODO: Make the label translatable.
482+
return f'<a href="{url}" class="button">Join online event</a>'
478483
ph = [
479484
SimpleFunctionalMailTextPlaceholder('event', ['event'], lambda event: event.name, lambda event: event.name),
480485
SimpleFunctionalMailTextPlaceholder(
@@ -783,10 +788,12 @@ def base_placeholders(sender, **kwargs):
783788
SimpleFunctionalMailTextPlaceholder(
784789
'join_online_event',
785790
['order', 'event'],
786-
lambda order, event: build_join_video_url(event=event, order=order),
791+
lambda order, event: render_video_join_link(event, order),
787792
generate_sample_video_url(),
788793
),
789794
)
795+
else:
796+
logger.info('pretix_venueless plugin not found, skipping join_online_event placeholder')
790797
name_scheme = PERSON_NAME_SCHEMES[sender.settings.name_scheme]
791798
for f, l, w in name_scheme['fields']:
792799
if f == 'full_name':

0 commit comments

Comments
 (0)