diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 9388150c4..4b9c91146 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -38,6 +38,8 @@ from ..settings import settings_hierarkey from .organizer import Organizer, OrganizerBillingModel, Team +TALK_HOSTNAME = settings.TALK_HOSTNAME + class EventMixin: @@ -1134,20 +1136,27 @@ def has_paid_things(self): @property def talk_schedule_url(self): - talk_host = settings.TALK_HOSTNAME - url = urljoin(talk_host, f"{self.slug}/schedule") + url = urljoin(TALK_HOSTNAME, f"{self.slug}/schedule") return url @property def talk_session_url(self): - talk_host = settings.TALK_HOSTNAME - url = urljoin(talk_host, f"{self.slug}/talk") + url = urljoin(TALK_HOSTNAME, f"{self.slug}/talk") return url @property def talk_speaker_url(self): - talk_host = settings.TALK_HOSTNAME - url = urljoin(talk_host, f"{self.slug}/speaker") + url = urljoin(TALK_HOSTNAME, f"{self.slug}/speaker") + return url + + @property + def talk_dashboard_url(self): + url = urljoin(TALK_HOSTNAME, f"orga/event/{self.slug}") + return url + + @property + def talk_settings_url(self): + url = urljoin(TALK_HOSTNAME, f"orga/event/{self.slug}/settings") return url @cached_property diff --git a/src/pretix/control/context.py b/src/pretix/control/context.py index 3eb93d5fb..36ecf16f4 100644 --- a/src/pretix/control/context.py +++ b/src/pretix/control/context.py @@ -15,7 +15,7 @@ get_organizer_navigation, ) -from ..eventyay_common.views.event import EventCreatedFor +from ..eventyay_common.utils import EventCreatedFor from ..helpers.i18n import ( get_javascript_format, get_javascript_output_format, get_moment_locale, ) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 7923dc799..9a792ddbb 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -369,10 +369,6 @@ class Meta: class EventUpdateForm(I18nModelForm): - is_video_creation = forms.BooleanField( - required=False, - ) - def __init__(self, *args, **kwargs): self.change_slug = kwargs.pop("change_slug", False) self.domain = kwargs.pop("domain", False) @@ -411,9 +407,6 @@ def __init__(self, *args, **kwargs): ), widget=forms.CheckboxSelectMultiple, ) - self.is_video_creation = self.initial.get("is_video_creation") - if self.is_video_creation: - self.fields["is_video_creation"].disabled = True def clean_domain(self): d = self.cleaned_data["domain"] @@ -484,7 +477,6 @@ class Meta: "geo_lat", "geo_lon", "sales_channels", - "is_video_creation", ] field_classes = { "date_from": SplitDateTimeField, diff --git a/src/pretix/eventyay_common/base_tasks.py b/src/pretix/eventyay_common/base_tasks.py new file mode 100644 index 000000000..9e145a112 --- /dev/null +++ b/src/pretix/eventyay_common/base_tasks.py @@ -0,0 +1,228 @@ +import logging +from importlib import import_module +from importlib.util import find_spec +from typing import Any + +from celery import Task +from django_scopes import scopes_disabled +from pretix_venueless.views import VenuelessSettingsForm + +from ..base.models import Event +from .utils import EventCreatedFor + +logger = logging.getLogger(__name__) + + +class CreateWorldTask(Task): + """ + A base class for handling video platform creation tasks for events. + + This class is responsible for: + - Setting up video plugins for events + - Managing plugin attachments + - Configuring video settings and JWT authentication + - Handling post-task success operations + + Raises: + ValueError: When event doesn't exist or validation fails + ConfigurationError: When plugin setup fails + """ + + def add_plugin(self, event: Event, plugin_name: str) -> str: + """ + Add a plugin to an event's plugin list. + + Args: + event: The event instance to add the plugin to + plugin_name: Name of the plugin to add + + Returns: + str: Comma-separated string of all plugins including the new one + """ + if not event.plugins: + return plugin_name + plugins = set(event.plugins.split(",")) + plugins.add(plugin_name) + return ",".join(plugins) + + def attach_plugin_to_event(self, plugin_name: str, event_slug: str) -> None: + """ + Attach a plugin to an event by updating its plugins list. + + Args: + plugin_name: Name of the plugin to attach + event_slug: Unique slug identifier of the event + + Raises: + ValueError: If the event does not exist + """ + try: + with scopes_disabled(): + event = Event.objects.get(slug=event_slug) + event.plugins = self.add_plugin(event, plugin_name) + event.save() + except Event.DoesNotExist: + logger.error("Event does not exist: %s", event_slug) + raise ValueError(f"Event with slug '{event_slug}' does not exist") + + def save_video_settings_information( + self, event_id: str, video_settings: dict + ) -> None: + """ + Save video configuration settings for an event. + + Args: + event_id: The event identifier + video_settings: Dictionary containing video configuration parameters + + Raises: + ValueError: If event doesn't exist or settings validation fails + """ + try: + with scopes_disabled(): + event_instance = Event.objects.get(slug=event_id) + video_config_form = VenuelessSettingsForm( + data=video_settings, obj=event_instance + ) + if video_config_form.is_valid(): + video_config_form.save() + else: + errors = video_config_form.errors + logger.error( + "Video integration configuration failed - Validation errors: %s", + errors, + ) + raise ValueError( + f"Failed to validate video integration settings: {errors}" + ) + except Event.DoesNotExist: + logger.error("Event does not exist: %s", event_id) + raise ValueError(f"Event with ID '{event_id}' does not exist") + + def check_installed_plugin(self, plugin_name: str) -> bool: + """ + Check if a plugin is installed. + + Args: + plugin_name: Name of the plugin to check + + Returns: + Boolean: True if installed, False otherwise + """ + try: + if find_spec(plugin_name) is not None: + import_module(plugin_name) + return True + return False + except ImportError: + logger.warning("Failed to import plugin: %s", plugin_name) + return False + + def extract_jwt_config(self, world_data: dict) -> dict: + """ + Extract JWT configuration from world data. + + Args: + world_data: Dictionary containing world configuration data + + Returns: + dict: JWT configuration with secret, issuer, and audience + """ + try: + config = world_data.get("config", {}) + jwt_secrets = config.get("JWT_secrets", []) + jwt_config = jwt_secrets[0] if jwt_secrets else {} + + return { + "secret": jwt_config.get("secret", ""), + "issuer": jwt_config.get("issuer", ""), + "audience": jwt_config.get("audience", ""), + } + except (KeyError, IndexError) as e: + logger.warning("Failed to extract JWT config: %s", e) + return {"secret": "", "issuer": "", "audience": ""} + + def setup_video_plugin(self, world_data: dict) -> None: + """ + Setup and configure the video plugin for an event. + + Args: + world_data: Dictionary containing world configuration data + + Raises: + ValueError: If plugin is not installed or configuration fails + """ + plugin_name = "pretix_venueless" + if not self.check_installed_plugin(plugin_name): + logger.error( + "Video integration configuration failed - Plugin not installed" + ) + raise ValueError(f"Plugin '{plugin_name}' is not installed") + + event_id = world_data.get("id") + if not event_id: + raise ValueError("World data missing event ID") + + # Setup plugin + self.attach_plugin_to_event(plugin_name, event_id) + + # Configure video settings + jwt_config = self.extract_jwt_config(world_data) + video_settings = { + "venueless_url": world_data.get("domain", ""), + "venueless_secret": jwt_config["secret"], + "venueless_issuer": jwt_config["issuer"], + "venueless_audience": jwt_config["audience"], + "venueless_all_items": True, + "venueless_items": [], + "venueless_questions": [], + } + + self.save_video_settings_information(event_id, video_settings) + + def on_success(self, retval: Any, task_id: str, args: tuple, kwargs: dict) -> Any: + """ + Handle successful task completion by setting up the video plugin. + Enable video plugin for the event + + Args: + retval: Task return value containing world data + task_id: ID of the completed task + args: Original task arguments + kwargs: Original task keyword arguments + + Returns: + Any: Result from parent class on_success method + """ + if isinstance(retval, dict): + try: + self.setup_video_plugin(retval) + except ValueError as e: + logger.error("Video integration configuration failed: %s", e) + return super().on_success(retval, task_id, args, kwargs) + + +class SendEventTask(Task): + def on_success(self, retval: Any, task_id: str, args: tuple, kwargs: dict) -> Any: + """ + Handle successful task completion by updating event settings. + Set the event settings to "create_for" both: + indicate that the event is created for both tickets and talk + + Args: + retval: Task return value + task_id: ID of the completed task + args: Original task arguments + kwargs: Original task keyword arguments + + Returns: + Any: Result from parent class on_success method + """ + event = kwargs.get("event", {}).get("slug", "") + try: + event = Event.objects.get(slug=event) + event.settings.set("create_for", EventCreatedFor.BOTH.value) + event.save() + except Event.DoesNotExist: + logger.error("Event with slug %s does not exist", event) + return super().on_success(retval, task_id, args, kwargs) diff --git a/src/pretix/eventyay_common/tasks.py b/src/pretix/eventyay_common/tasks.py index 55e23a430..0a6c1a624 100644 --- a/src/pretix/eventyay_common/tasks.py +++ b/src/pretix/eventyay_common/tasks.py @@ -1,10 +1,9 @@ import base64 -import importlib.util import logging from datetime import datetime, timezone as tz from decimal import Decimal -from importlib import import_module from typing import Optional, Tuple +from urllib.parse import urljoin import pytz import requests @@ -16,7 +15,6 @@ from django.db import DatabaseError from django.db.models import Q from django_scopes import scopes_disabled -from pretix_venueless.views import VenuelessSettingsForm from pretix.base.models.vouchers import InvoiceVoucher from pretix.helpers.stripe_utils import ( @@ -28,6 +26,7 @@ from ..base.services.mail import mail_send_task from ..base.settings import GlobalSettingsObject from ..helpers.jwt_generate import generate_sso_token +from .base_tasks import CreateWorldTask, SendEventTask from .billing_invoice import InvoicePDFGenerator from .schemas.billing import CollectBillingResponse @@ -86,7 +85,9 @@ def send_team_webhook(self, user_id, team): try: # Send the POST request with the payload and the headers response = requests.post( - settings.TALK_HOSTNAME + "/webhook/team/", json=payload, headers=headers + urljoin(settings.TALK_HOSTNAME, "webhook/team/"), + json=payload, + headers=headers ) response.raise_for_status() # Raise exception for bad status codes except requests.RequestException as e: @@ -100,9 +101,9 @@ def send_team_webhook(self, user_id, team): @shared_task( - bind=True, max_retries=5, default_retry_delay=60 + bind=True, max_retries=5, default_retry_delay=60, base=SendEventTask ) # Retries up to 5 times with a 60-second delay -def send_event_webhook(self, user_id, event, action): +def send_event_webhook(self, user_id: int, event: dict, action: str) -> Optional[dict]: # Define the payload to send to the webhook user_model = get_user_model() user = user_model.objects.get(id=user_id) @@ -124,9 +125,12 @@ def send_event_webhook(self, user_id, event, action): try: # Send the POST request with the payload and the headers response = requests.post( - settings.TALK_HOSTNAME + "/webhook/event/", json=payload, headers=headers + urljoin(settings.TALK_HOSTNAME, "webhook/event/"), + json=payload, + headers=headers ) response.raise_for_status() # Raise exception for bad status codes + return response.json() except requests.RequestException as e: # Log any errors that occur logger.error("Error sending webhook to talk component: %s", e) @@ -138,9 +142,9 @@ def send_event_webhook(self, user_id, event, action): @shared_task( - bind=True, max_retries=5, default_retry_delay=60 + bind=True, max_retries=5, default_retry_delay=60, base=CreateWorldTask ) # Retries up to 5 times with a 60-second delay -def create_world(self, is_video_creation, event_data): +def create_world(self, is_video_creation: bool, event_data: dict) -> Optional[dict]: """ Create a video system for the specified event. @@ -158,156 +162,59 @@ def create_world(self, is_video_creation, event_data): - The user must have the necessary permission. - The user must choose to create a video. """ - event_slug = event_data.get("id") - title = event_data.get("title") - event_timezone = event_data.get("timezone") - locale = event_data.get("locale") - token = event_data.get("token") - has_permission = event_data.get("has_permission") + def _create_world(payload: dict, headers: dict) -> Optional[dict]: + try: + response = requests.post( + urljoin(settings.VIDEO_SERVER_HOSTNAME, "api/v1/create-world/"), + json=payload, + headers=headers, + ) + response.raise_for_status() + return response.json() + except ( + requests.exceptions.JSONDecodeError, + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.RequestException, + ) as e: + error_type = type(e).__name__ + logger.error("%s during video world creation: %s", error_type, e) + raise + + has_permission = event_data.get("has_permission", False) + if not (is_video_creation and has_permission): + logger.info( + "Skipping video world creation - Video enabled: %s, Has permission: %s", + is_video_creation, + has_permission, + ) + return None + event_slug = event_data.get("id", "") payload = { "id": event_slug, - "title": title, - "timezone": event_timezone, - "locale": locale, + "title": event_data.get("title", ""), + "timezone": event_data.get("timezone", ""), + "locale": event_data.get("locale", ""), "traits": { 'attendee': 'eventyay-video-event-{}'.format(event_slug), } } - headers = {"Authorization": "Bearer " + token} - - if is_video_creation and has_permission: + try: + return _create_world( + payload=payload, + headers={"Authorization": "Bearer " + event_data.get("token", "")} + ) + except requests.RequestException as e: try: - response = requests.post( - "{}/api/v1/create-world/".format(settings.VIDEO_SERVER_HOSTNAME), - json=payload, - headers=headers, - ) - setup_video_plugin(response.json()) - except requests.exceptions.ConnectionError as e: - logger.error("Connection error: %s", str(e)) - self.retry(exc=e) - except requests.exceptions.Timeout as e: - logger.error("Request timed out: %s", str(e)) self.retry(exc=e) - except requests.exceptions.RequestException as e: - logger.error("Request failed: %s", str(e)) - self.retry(exc=e) - except ValueError as e: - logger.error("Value error: %s", str(e)) - - -def extract_jwt_config(world_data): - """ - Extract the JWT configuration from the world data. - @param world_data: A dictionary containing the world data. - @return: A dictionary containing the JWT configuration. - """ - config = world_data.get('config', {}) - jwt_secrets = config.get('JWT_secrets', []) - jwt_config = jwt_secrets[0] if jwt_secrets else {} - return { - 'secret': jwt_config.get('secret'), - 'issuer': jwt_config.get('issuer'), - 'audience': jwt_config.get('audience') - } - - -def setup_video_plugin(world_data): - """ - Setup the video plugin for the event. - @param world_data: A dictionary containing the world data. - if the plugin is installed, add the plugin to the event and save the video settings information. - """ - jwt_config = extract_jwt_config(world_data) - video_plugin = get_installed_plugin('pretix_venueless') - event_id = world_data.get("id") - if video_plugin: - attach_plugin_to_event('pretix_venueless', event_id) - video_settings = { - 'venueless_url': world_data.get('domain', ""), - 'venueless_secret': jwt_config.get('secret', ""), - 'venueless_issuer': jwt_config.get('issuer', ""), - 'venueless_audience': jwt_config.get('audience', ""), - 'venueless_all_items': True, - 'venueless_items': [], - 'venueless_questions': [], - } - save_video_settings_information(event_id, video_settings) - else: - logger.error("Video integration configuration failed - Plugin not installed") - raise ValueError("Failed to configure video integration") - - -def save_video_settings_information(event_id, video_settings): - """ - Save the video settings information to the event. - @param event_id: A string representing the unique identifier for the event. - @param video_settings: A dictionary containing the video settings information. - @return: The video configuration form. - """ - try: - with scopes_disabled(): - event_instance = Event.objects.get(slug=event_id) - video_config_form = VenuelessSettingsForm( - data=video_settings, - obj=event_instance + except self.MaxRetriesExceededError: + logger.error( + "Max retries exceeded for video world creation. Event: %s, Error: %s", + event_slug, + e, ) - if video_config_form.is_valid(): - video_config_form.save() - else: - logger.error("Video integration configuration failed - Validation errors: %s", video_config_form.errors) - raise ValueError("Failed to validate video integration settings") - return video_config_form - except Event.DoesNotExist: - logger.error("Event does not exist: %s", event_id) - raise ValueError("Event does not exist") - - -def get_installed_plugin(plugin_name): - """ - Check if a plugin is installed. - @param plugin_name: A string representing the name of the plugin to check. - @return: The installed plugin if it exists, otherwise None. - """ - if importlib.util.find_spec(plugin_name) is not None: - installed_plugin = import_module(plugin_name) - else: - installed_plugin = None - return installed_plugin - - -def attach_plugin_to_event(plugin_name, event_slug): - """ - Attach a plugin to an event. - @param plugin_name: A string representing the name of the plugin to add. - @param event_slug: A string representing the slug of the event to which the plugin should be added. - @return: The event instance with the added plugin. - """ - try: - with scopes_disabled(): - event = Event.objects.get(slug=event_slug) - event.plugins = add_plugin(event, plugin_name) - event.save() - return event - except Event.DoesNotExist: - logger.error("Event does not exist: %s", event_slug) - raise ValueError("Event does not exist") - - -def add_plugin(event, plugin_name): - """ - Add a plugin - @param event: The event instance to which the plugin should be added. - @param plugin_name: A string representing the name of the plugin to add. - @return: The updated list of plugins for the event. - """ - if not event.plugins: - return plugin_name - plugins = set(event.plugins.split(',')) - plugins.add(plugin_name) - return ','.join(plugins) def get_header_token(user_id): diff --git a/src/pretix/eventyay_common/templates/eventyay_common/base.html b/src/pretix/eventyay_common/templates/eventyay_common/base.html index 10c09899b..669eea4b2 100644 --- a/src/pretix/eventyay_common/templates/eventyay_common/base.html +++ b/src/pretix/eventyay_common/templates/eventyay_common/base.html @@ -13,6 +13,7 @@ + {% endcompress %} {% if DEBUG %}