diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 000000000..d787271b5 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +disable_error_code = import-untyped diff --git a/vc_dummy/indico_vc_dummy/plugin.py b/vc_dummy/indico_vc_dummy/plugin.py index 8fa655dc6..6adf3bf20 100644 --- a/vc_dummy/indico_vc_dummy/plugin.py +++ b/vc_dummy/indico_vc_dummy/plugin.py @@ -38,11 +38,11 @@ class DummyPlugin(VCPluginMixin, IndicoPlugin): @property def logo_url(self): - return url_for_plugin(self.name + '.static', filename='images/dummy_logo.png') + return url_for_plugin(self.name + '.static', filename='images/dummy_logo.svg') @property def icon_url(self): - return url_for_plugin(self.name + '.static', filename='images/dummy_icon.png') + return url_for_plugin(self.name + '.static', filename='images/dummy_icon.svg') def get_blueprints(self): return IndicoPluginBlueprint('vc_dummy', __name__) diff --git a/vc_dummy/indico_vc_dummy/static/images/dummy_icon.png b/vc_dummy/indico_vc_dummy/static/images/dummy_icon.png deleted file mode 100644 index 2eaa7cad5..000000000 Binary files a/vc_dummy/indico_vc_dummy/static/images/dummy_icon.png and /dev/null differ diff --git a/vc_dummy/indico_vc_dummy/static/images/dummy_icon.svg b/vc_dummy/indico_vc_dummy/static/images/dummy_icon.svg new file mode 100644 index 000000000..4dd2b4082 --- /dev/null +++ b/vc_dummy/indico_vc_dummy/static/images/dummy_icon.svg @@ -0,0 +1 @@ + diff --git a/vc_dummy/indico_vc_dummy/static/images/dummy_logo.png b/vc_dummy/indico_vc_dummy/static/images/dummy_logo.png deleted file mode 100644 index a4a1bcfbc..000000000 Binary files a/vc_dummy/indico_vc_dummy/static/images/dummy_logo.png and /dev/null differ diff --git a/vc_dummy/indico_vc_dummy/static/images/dummy_logo.svg b/vc_dummy/indico_vc_dummy/static/images/dummy_logo.svg new file mode 100644 index 000000000..7748902a4 --- /dev/null +++ b/vc_dummy/indico_vc_dummy/static/images/dummy_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vc_zoom/README.md b/vc_zoom/README.md index 3f3cb7097..53ccc3a7d 100644 --- a/vc_zoom/README.md +++ b/vc_zoom/README.md @@ -11,6 +11,11 @@ ## Changelog +### 3.3.1 + +- Adapt to Indico 3.3.5 changes +- Modernize UI elements + ### 3.3 - Adapt to Indico 3.3 changes diff --git a/vc_zoom/conftest.py b/vc_zoom/conftest.py index 66cf3fd02..315d53649 100644 --- a/vc_zoom/conftest.py +++ b/vc_zoom/conftest.py @@ -5,4 +5,4 @@ # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. -pytest_plugins = 'indico' +pytest_plugins = ('indico', 'indico_vc_zoom.fixtures') diff --git a/vc_zoom/indico_vc_zoom/client/JoinButton.module.scss b/vc_zoom/indico_vc_zoom/client/JoinButton.module.scss new file mode 100644 index 000000000..1bd0fee0a --- /dev/null +++ b/vc_zoom/indico_vc_zoom/client/JoinButton.module.scss @@ -0,0 +1,13 @@ +// This file is part of the Indico plugins. +// Copyright (C) 2020 - 2024 CERN and ENEA +// +// The Indico plugins are free software; you can redistribute +// them and/or modify them under the terms of the MIT License; +// see the LICENSE file for more details. + +.button-label:global(.ui.image.label) { + > img { + margin: 0; + height: 1.3em !important; + } +} diff --git a/vc_zoom/indico_vc_zoom/client/JoinButton.tsx b/vc_zoom/indico_vc_zoom/client/JoinButton.tsx new file mode 100644 index 000000000..7a065b24b --- /dev/null +++ b/vc_zoom/indico_vc_zoom/client/JoinButton.tsx @@ -0,0 +1,151 @@ +// This file is part of the Indico plugins. +// Copyright (C) 2020 - 2024 CERN and ENEA +// +// The Indico plugins are free software; you can redistribute +// them and/or modify them under the terms of the MIT License; +// see the LICENSE file for more details. + +import staticURL from 'indico-url:plugin_vc_zoom.static'; + +import React, {useState} from 'react'; +import {Button, ButtonGroup, Confirm, Dropdown, Icon, Label, Loader, Popup, SemanticICONS} from 'semantic-ui-react'; + +import {Translate} from 'indico/react/i18n'; +import {handleAxiosError, indicoAxios} from 'indico/utils/axios'; + +import './JoinButton.module.scss'; + +interface OptionsButtonProps { + url: string; + onMadeAltHost: () => void; +} + +/** A dropdown button which shows additional actions, such as the possibility to take over as meeting co-host */ +function OptionsButton({url, onMadeAltHost}: OptionsButtonProps) { + const [isConfirmOpen, setConfirmOpen] = useState(false); + const [state, setState] = useState('idle'); + + async function makeAlternativeHost() { + setConfirmOpen(false); + try { + setState('submitting'); + await indicoAxios.post(url); + setState('success'); + } catch (error) { + handleAxiosError(error); + } + setTimeout(() => { + setState('idle'); + onMadeAltHost(); + }, 3000); + } + + const icons = new Map([ + ['idle', 'cog'], + ['submitting', null], + ['success', 'checkmark'] + ]); + + const trigger = + state == 'submitting' ? ( + + ) : ( + + ); + + return ( + <> + + + setConfirmOpen(true)} + > + + Make me alternative host + + + + setConfirmOpen(false)} + size="tiny" + /> + + ); +} + +interface JoinButtonProps { + classes: string; + href: string; + target: string; + icon: SemanticICONS; + caption: string; + description: string; + altHostUrl: string; + meetingTitle: string | undefined; + meetingDataHtml: string | undefined; +} + +/** The join button, which can optionally include an alternative host URL (creates menu) as well as a pop-up */ +export default function JoinButton({ + classes, + href, + target, + icon, + caption, + description, + altHostUrl = '', + meetingTitle, + meetingDataHtml, +}: JoinButtonProps) { + const [isAltHost, setAltHost] = useState(!altHostUrl); + + let buttons = ( + <> + + {!isAltHost ? ( + { + setAltHost(true); + }} + /> + ) : null} + + ); + + const labeledButton = ( + + ); + + buttons = meetingDataHtml ? ( + <> + +

{meetingTitle}

+ {/* the HTML data is generated server-side from the Zoom API output and should be in a sanitized state */} +
+ + {buttons} + + ) : ( + buttons + ); + + return {buttons}; +} diff --git a/vc_zoom/indico_vc_zoom/client/ind_vc_zoom_join_button.js b/vc_zoom/indico_vc_zoom/client/ind_vc_zoom_join_button.js new file mode 100644 index 000000000..f57204ef3 --- /dev/null +++ b/vc_zoom/indico_vc_zoom/client/ind_vc_zoom_join_button.js @@ -0,0 +1,46 @@ +// This file is part of the Indico plugins. +// Copyright (C) 2020 - 2024 CERN and ENEA +// +// The Indico plugins are free software; you can redistribute +// them and/or modify them under the terms of the MIT License; +// see the LICENSE file for more details. + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import JoinButton from './JoinButton'; + +import './ind_vc_zoom_join_button.scss'; + +/** Custom element wrapper for a React-managed JoinButton */ +customElements.define( + 'ind-vc-zoom-join-button', + class extends HTMLElement { + connectedCallback() { + const classes = this.getAttribute('classes'); + const href = this.getAttribute('href'); + const target = this.getAttribute('target'); + const icon = this.getAttribute('icon'); + const caption = this.getAttribute('caption'); + const description = this.getAttribute('description'); + const altHostUrl = this.getAttribute('alt-host-url'); + const meetingTitle = this.getAttribute('meeting-title'); + const meetingDataHtml = this.getAttribute('meeting-data-html'); + + ReactDOM.render( + , + this + ); + } + } +); diff --git a/vc_zoom/indico_vc_zoom/client/ind_vc_zoom_join_button.scss b/vc_zoom/indico_vc_zoom/client/ind_vc_zoom_join_button.scss new file mode 100644 index 000000000..edf19b580 --- /dev/null +++ b/vc_zoom/indico_vc_zoom/client/ind_vc_zoom_join_button.scss @@ -0,0 +1,12 @@ +// This file is part of the Indico plugins. +// Copyright (C) 2020 - 2024 CERN and ENEA +// +// The Indico plugins are free software; you can redistribute +// them and/or modify them under the terms of the MIT License; +// see the LICENSE file for more details. + +ind-vc-zoom-join-button { + display: flex; + gap: 0.2em; + margin-right: 0.2em; +} diff --git a/vc_zoom/indico_vc_zoom/client/index.js b/vc_zoom/indico_vc_zoom/client/index.js index db4e05a4a..4c2808832 100644 --- a/vc_zoom/indico_vc_zoom/client/index.js +++ b/vc_zoom/indico_vc_zoom/client/index.js @@ -5,34 +5,4 @@ // them and/or modify them under the terms of the MIT License; // see the LICENSE file for more details. -/* global confirmPrompt:false, $T:false */ - -import {handleAxiosError, indicoAxios} from 'indico/utils/axios'; - -const $t = $T.domain('vc_zoom'); - -document.addEventListener('DOMContentLoaded', async () => { - $('.vc-toolbar').dropdown({ - positioning: { - level1: {my: 'right top', at: 'right bottom', offset: '0px 0px'}, - }, - }); - - document.querySelectorAll('.vc-toolbar .action-make-host').forEach(elem => { - elem.addEventListener('click', () => { - confirmPrompt( - $t.gettext('Are you sure you want to be added as an alternative host?'), - $t.gettext('Make me an alternative host') - ).then(async () => { - const killProgress = IndicoUI.Dialogs.Util.progress(); - try { - await indicoAxios.post(elem.dataset.href); - window.location.reload(); - } catch (error) { - handleAxiosError(error); - killProgress(); - } - }); - }); - }); -}); +import './ind_vc_zoom_join_button'; diff --git a/vc_zoom/indico_vc_zoom/controllers.py b/vc_zoom/indico_vc_zoom/controllers.py index 7e7224f05..d555f7e65 100644 --- a/vc_zoom/indico_vc_zoom/controllers.py +++ b/vc_zoom/indico_vc_zoom/controllers.py @@ -8,7 +8,7 @@ import hashlib import hmac -from flask import flash, jsonify, request, session +from flask import jsonify, request, session from flask_pluginengine import current_plugin from marshmallow import EXCLUDE from sqlalchemy.orm.attributes import flag_modified @@ -29,8 +29,7 @@ class RHRoomAlternativeHost(RHVCSystemEventBase): def _process(self): new_identifier = session.user.identifier if new_identifier == self.vc_room.data['host'] or new_identifier in self.vc_room.data['alternative_hosts']: - flash(_('You were already an (alternative) host of this meeting'), 'warning') - return jsonify(success=False) + raise UserValueError(_('You were already an (alternative) host of this meeting')) try: self.plugin.refresh_room(self.vc_room, self.event) @@ -43,9 +42,7 @@ def _process(self): except VCRoomError: db.session.rollback() raise - else: - flash(_("You are now an alternative host of room '{room}'").format(room=self.vc_room.name), 'success') - return jsonify(success=True) + return '', 204 class RHWebhook(RH): diff --git a/vc_zoom/indico_vc_zoom/fixtures.py b/vc_zoom/indico_vc_zoom/fixtures.py new file mode 100644 index 000000000..327393ad5 --- /dev/null +++ b/vc_zoom/indico_vc_zoom/fixtures.py @@ -0,0 +1,129 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2020 - 2024 CERN and ENEA +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + +import itertools + +import pytest + +from indico.core.plugins import plugin_engine + + +@pytest.fixture +def zoom_plugin(app): + """Return a callable which lets you create dummy Zoom room occurrences.""" + from indico_vc_zoom.plugin import ZoomPlugin + + plugin = ZoomPlugin(plugin_engine, app) + plugin.settings.set_multi({ + 'email_domains': ('megacorp.xyz',), + 'user_lookup_mode': 'email_domains', + }) + return plugin + + +@pytest.fixture +def create_zoom_meeting(db, test_client, no_csrf_check, smtp, zoom_api): + def _create(obj, link_type, zoom_name='Zoom Meeting', **kwargs): + with test_client.session_transaction() as sess: + sess.set_session_user(zoom_api['user']) + # despite no_csrf_check, this is still needed since the form does its own + sess['_csrf_token'] = 'supersecure' # noqa: S105 + + match link_type: + case 'event': + event_id = obj.id + case 'contribution': + event_id = obj.event_id + case 'block': + event_id = obj.session.event_id + + with db.session.no_autoflush: + resp = test_client.post( + f'/event/{event_id}/manage/videoconference/zoom/create', + data={ + 'vc-csrf_token': 'supersecure', + 'vc-name': zoom_name, + 'vc-linking': link_type, + 'vc-host_choice': 'myself', + 'vc-password': '13371337', + 'vc-password_visibility': 'logged_in', + 'vc-description': 'nothing to add', + 'vc-show': 'y', + 'vc-mute_participant_video': 'y', + 'vc-mute_host_video': 'y', + 'vc-mute_audio': 'y', + f'vc-{link_type}': obj.id, + }, + ) + assert resp.status_code == 200 + + db.session.flush() + + # dirty, but this endpoint still returns an old WTForm's HTML + if b'data-error' in resp.data: + print(resp.text) + assert False # Form error detected + + return obj.vc_room_associations[0].vc_room + + return _create + + +JSON_DATA = { + 'join_url': 'https://example.com/kitties', + 'start_url': 'https://example.com/puppies', + 'password': '13371337', + 'host_id': 'don.orange@megacorp.xyz', + 'topic': 'New Room', + 'agenda': 'something something', + 'settings': { + 'host_video': True, + 'mute_upon_entry': False, + 'participant_video': True, + 'waiting_room': False, + 'alternative_hosts': 'foo@example.com', + 'approval_type': 2 + }, +} + + +@pytest.fixture +def zoom_api(zoom_plugin, create_user, mocker): + """Mock some Zoom API endpoints.""" + meeting_ids = iter(f'zmeeting{n}' for n in itertools.count(start=1, step=1)) + + def _create_meeting(*args, **kwargs): + return {**JSON_DATA, 'id': next(meeting_ids)} + + api_create_meeting = mocker.patch('indico_vc_zoom.api.ZoomIndicoClient.create_meeting') + api_create_meeting.side_effect = _create_meeting + + api_update_meeting = mocker.patch('indico_vc_zoom.api.ZoomIndicoClient.update_meeting') + api_update_meeting.return_value = {} + + user = create_user(1, email='don.orange@megacorp.xyz') + + api_get_user = mocker.patch('indico_vc_zoom.api.ZoomIndicoClient.get_user') + api_get_user.return_value = {'id': '7890abcd', 'email': 'don.orange@megacorp.xyz'} + + def _get_meeting(id_, *args, **kwargs): + return dict(JSON_DATA, id=id_) + + api_get_meeting = mocker.patch('indico_vc_zoom.api.ZoomIndicoClient.get_meeting') + api_get_meeting.side_effect = _get_meeting + + api_delete_meeting = mocker.patch('indico_vc_zoom.api.ZoomIndicoClient.delete_meeting') + api_delete_meeting.return_value = {} + + return { + 'user': user, + 'create_meeting': api_create_meeting, + 'get_meeting': api_get_meeting, + 'update_meeting': api_update_meeting, + 'api_delete_meeting': api_delete_meeting, + 'get_user': api_get_user, + } diff --git a/vc_zoom/indico_vc_zoom/plugin.py b/vc_zoom/indico_vc_zoom/plugin.py index 86ed27498..c23a2e7ff 100644 --- a/vc_zoom/indico_vc_zoom/plugin.py +++ b/vc_zoom/indico_vc_zoom/plugin.py @@ -159,18 +159,17 @@ def init(self): self.connect(signals.event.times_changed, self._check_meetings) self.connect(signals.event.metadata_postprocess, self._event_metadata_postprocess, sender='ical-export') self.template_hook('event-vc-room-list-item-labels', self._render_vc_room_labels) - self.inject_bundle('main.js', WPSimpleEventDisplay) - self.inject_bundle('main.js', WPVCEventPage) - self.inject_bundle('main.js', WPVCManageEvent) - self.inject_bundle('main.js', WPConferenceDisplay) + for wp in (WPSimpleEventDisplay, WPVCEventPage, WPVCManageEvent, WPConferenceDisplay): + self.inject_bundle('main.js', wp) + self.inject_bundle('main.css', wp) @property def logo_url(self): - return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png') + return url_for_plugin(self.name + '.static', filename='images/zoom_logo.svg') @property def icon_url(self): - return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png') + return url_for_plugin(self.name + '.static', filename='images/zoom_icon.svg') def create_form(self, event, existing_vc_room=None, existing_event_vc_room=None): """Override the default room form creation mechanism.""" @@ -218,16 +217,14 @@ def _extend_indico_cli(self, sender, **kwargs): def update_data_association(self, event, vc_room, room_assoc, data): # XXX: This feels slightly hacky. Maybe we should change the API on the core? association_is_new = room_assoc.vc_room is None - old_link = room_assoc.link_object # in a new room, `meeting_type` comes in `data`, otherwise it's already in the VCRoom is_webinar = data.get('meeting_type', vc_room.data and vc_room.data.get('meeting_type')) == 'webinar' - super().update_data_association(event, vc_room, room_assoc, data) + assoc_has_changed = super().update_data_association(event, vc_room, room_assoc, data) if vc_room.data: try: - # this is not a new room if association_is_new: self.refresh_room(vc_room, event) if vc_room.data.get('registration_required'): @@ -245,7 +242,7 @@ def update_data_association(self, event, vc_room, room_assoc, data): else ZoomMeetingType.recurring_meeting_no_time ) }) - elif room_assoc.link_object != old_link: + elif assoc_has_changed: # the booking should now be linked to something else new_schedule_args = (get_schedule_args(room_assoc.link_object) if room_assoc.link_object.start_dt @@ -273,8 +270,10 @@ def update_data_association(self, event, vc_room, room_assoc, data): room_assoc.data['password_visibility'] = data.pop('password_visibility') flag_modified(room_assoc, 'data') + return assoc_has_changed + def update_data_vc_room(self, vc_room, data, is_new=False): - super().update_data_vc_room(vc_room, data) + super().update_data_vc_room(vc_room, data, is_new=is_new) fields = {'description', 'password'} # we may end up not getting a meeting_type from the form diff --git a/vc_zoom/indico_vc_zoom/static/images/zoom_icon.svg b/vc_zoom/indico_vc_zoom/static/images/zoom_icon.svg new file mode 100644 index 000000000..f6b086fcc --- /dev/null +++ b/vc_zoom/indico_vc_zoom/static/images/zoom_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vc_zoom/indico_vc_zoom/static/images/zoom_logo.png b/vc_zoom/indico_vc_zoom/static/images/zoom_logo.png deleted file mode 100644 index c8347b437..000000000 Binary files a/vc_zoom/indico_vc_zoom/static/images/zoom_logo.png and /dev/null differ diff --git a/vc_zoom/indico_vc_zoom/static/images/zoom_logo.svg b/vc_zoom/indico_vc_zoom/static/images/zoom_logo.svg new file mode 100644 index 000000000..c43d97cf7 --- /dev/null +++ b/vc_zoom/indico_vc_zoom/static/images/zoom_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vc_zoom/indico_vc_zoom/templates/buttons.html b/vc_zoom/indico_vc_zoom/templates/buttons.html index 157dea34a..d4d3893a0 100644 --- a/vc_zoom/indico_vc_zoom/templates/buttons.html +++ b/vc_zoom/indico_vc_zoom/templates/buttons.html @@ -1,62 +1,67 @@ -{% macro render_make_me_owner(event, vc_room, event_vc_room, extra_classes='') %} - {% if session.user.identifier != vc_room.data['host'] - and session.user.identifier not in vc_room.data.get('alternative_hosts', []) - and event.can_manage(session.user) %} - - - {% endif %} -{% endmacro %} - -{% macro render_join_button(vc_room, event_vc_room, extra_classes='') %} - {% if event_vc_room.data.password_visibility == 'everyone' or - event_vc_room.event.can_manage(session.user) or +{% macro render_join_button(vc_room, event_vc_room, show_logo=false, extra_classes='') %} + {% set event = event_vc_room.event %} + {% set plugin = vc_room.plugin %} + {% set show_full_join_link = ( + event_vc_room.data.password_visibility == 'everyone' or + event.can_manage(session.user) or (session.user and event_vc_room.data.password_visibility == 'logged_in') or - (session.user and event_vc_room.data.password_visibility == 'registered' and event_vc_room.event.is_user_registered(session.user)) %} - - {% trans %}Join{% endtrans %} - - {% elif event_vc_room.data.password_visibility == 'no_one' %} - - {% trans %}Join{% endtrans %} - - {% elif event_vc_room.data.password_visibility == 'registered' %} - {% if session.user %} - {% if event_vc_room.event.type == 'conference' %} - - {% trans %}Please register{% endtrans %} - + (session.user and event_vc_room.data.password_visibility == 'registered' and event.is_user_registered(session.user)) + ) %} + + icon="lock" + caption="{% trans %}Join{% endtrans %}" + {% elif event_vc_room.data.password_visibility == 'registered' %} + {% if session.user %} + {% if event.type == 'conference' %} + classes="blue {{ extra_classes }}" + description="{% trans %}This Zoom Meeting can only be seen by registered participants{% endtrans %}" + href="{{ url_for('event_registration.display_regform_list', event) }}" + icon="ticket" + caption="{% trans %}Please register{% endtrans %}" + {% else %} + classes="disabled {{ extra_classes }}" + description="{% trans %}This Zoom Meeting can only be seen by registered participants{% endtrans %}" + icon="ticket" + caption="{% trans %}Registration required{% endtrans %}" + {% endif %} {% else %} - - {% trans %}Registration required{% endtrans %} - + classes="blue {{ extra_classes }}" + description="{% trans %}This Zoom Meeting can only be seen by registered participants{% endtrans %}" + href="{{ url_for_login(request.relative_url) }}" + icon="ticket" + caption="{% trans %}Please log in and register{% endtrans %}" {% endif %} {% else %} - - {% trans %}Please log in and register{% endtrans %} - + classes="orange {{ extra_classes }}" + description="{% trans %}This Zoom Meeting can only be seen by logged in users{% endtrans %}" + href="{{ url_for_login(request.relative_url) }}" + icon="lock" + caption="{% trans %}Please log in{% endtrans %}" + {% endif %} + + {% if session.user.identifier != vc_room.data['host'] + and session.user.identifier not in vc_room.data.get('alternative_hosts', []) + and event.can_manage(session.user) %} + alt-host-url="{{ url_for_plugin('vc_zoom.make_me_alt_host', event_vc_room) }}" + {% endif %} + + meeting-title="{{ vc_room.name }}" + + {% if show_logo %} + meeting-data-html="{{ plugin.render_info_box(vc_room, event_vc_room, event) | escape }}" {% endif %} - {% else %} - - {% trans %}Please log in{% endtrans %} - - {% endif %} + > + {{ caller() if caller is defined }} + {% endmacro %} diff --git a/vc_zoom/indico_vc_zoom/templates/event_buttons.html b/vc_zoom/indico_vc_zoom/templates/event_buttons.html index 401ef93f3..2397024f3 100644 --- a/vc_zoom/indico_vc_zoom/templates/event_buttons.html +++ b/vc_zoom/indico_vc_zoom/templates/event_buttons.html @@ -1,7 +1,7 @@ -{% extends 'vc/event_buttons.html' %} -{% from 'vc_zoom:buttons.html' import render_join_button, render_make_me_owner %} +{% from 'vc_zoom:buttons.html' import render_join_button %} -{% block buttons %} - {{ render_join_button(vc_room, event_vc_room, "i-button-small event-service-right-button join-button") }} - {{ render_make_me_owner(event, vc_room, event_vc_room, extra_classes="i-button-small") }} -{% endblock %} +
+ {% call render_join_button(vc_room, event_vc_room) %} + {{ template_hook('event-timetable-vc-extra-buttons', event_vc_room=event_vc_room) }} + {% endcall %} +
diff --git a/vc_zoom/indico_vc_zoom/templates/info_box.html b/vc_zoom/indico_vc_zoom/templates/info_box.html index d034908a2..146df08a9 100644 --- a/vc_zoom/indico_vc_zoom/templates/info_box.html +++ b/vc_zoom/indico_vc_zoom/templates/info_box.html @@ -8,46 +8,52 @@ (session.user and event_vc_room.data.password_visibility == 'logged_in') or (session.user and event_vc_room.data.password_visibility == 'registered' and event_vc_room.event.is_user_registered(session.user)) ) %} -
-
{% trans %}Zoom Meeting ID{% endtrans %}
-
{{ vc_room.data.zoom_id }}
+
+
+
{% trans %}Zoom Meeting ID{% endtrans %}
+ {{ vc_room.data.zoom_id }} +
{% if vc_room.data.description %} -
{% trans %}Description{% endtrans %}
-
{{ vc_room.data.description }}
+
+
{% trans %}Description{% endtrans %}
+ {{ vc_room.data.description }} +
{% endif %} {% if host %} -
{% trans %}Host{% endtrans %}
-
{{ (host|decodeprincipal).full_name }}
+
+
{% trans %}Host{% endtrans %}
+ {{ (host|decodeprincipal).full_name }} +
{% endif %} {% if alt_hosts %} -
- {% trans count=alt_hosts|length -%} - Alternative host - {%- pluralize -%} - Alternative hosts - {%- endtrans %} -
-
+
+
+ {% trans count=alt_hosts|length -%} + Alternative host + {%- pluralize -%} + Alternative hosts + {%- endtrans %} +
{{ alt_hosts | map('decodeprincipal') | map(attribute='full_name') | join(', ') }} -
+
{% endif %} {% if passcode_visible %} -
{% trans %}Passcode{% endtrans %}
-
{{ vc_room.data.password }}
+
+
{% trans %}Passcode{% endtrans %}
+ {{ vc_room.data.password }} +
{% endif %} {% if phone_link %} -
- {% trans %}Useful links{% endtrans %} -
-
+
+
{% trans %}Useful links{% endtrans %}
{% trans %}Join via phone{% endtrans %} -
+
{% endif %} -
{% trans %}Zoom URL{% endtrans %}
-
+
+
{% trans %}Zoom URL{% endtrans %}
{{ clipboard_input(vc_room.data.url if passcode_visible else vc_room.data.public_url, - name="vc-room-url-%s"|format(event_vc_room.id)) }} -
- + name="vc-room-url-%s"|format(event_vc_room.id)) }} + + diff --git a/vc_zoom/indico_vc_zoom/templates/management_buttons.html b/vc_zoom/indico_vc_zoom/templates/management_buttons.html index 2ec6e31f1..4ef956c57 100644 --- a/vc_zoom/indico_vc_zoom/templates/management_buttons.html +++ b/vc_zoom/indico_vc_zoom/templates/management_buttons.html @@ -2,6 +2,5 @@ {% from 'vc_zoom:buttons.html' import render_join_button, render_make_me_owner %} {% block buttons %} - {{ render_join_button(vc_room, event_vc_room, extra_classes='icon-play') }} - {{ render_make_me_owner(event_vc_room.event, vc_room, event_vc_room) }} + {{ render_join_button(vc_room, event_vc_room) }} {% endblock %} diff --git a/vc_zoom/indico_vc_zoom/templates/vc_room_timetable_buttons.html b/vc_zoom/indico_vc_zoom/templates/vc_room_timetable_buttons.html index 67c79c18e..6f472367c 100644 --- a/vc_zoom/indico_vc_zoom/templates/vc_room_timetable_buttons.html +++ b/vc_zoom/indico_vc_zoom/templates/vc_room_timetable_buttons.html @@ -1,8 +1,7 @@ -{% extends 'vc/vc_room_timetable_buttons.html' %} -{% from 'vc_zoom:buttons.html' import render_join_button, render_make_me_owner %} +{% from 'vc_zoom:buttons.html' import render_join_button %} + {% set vc_room = event_vc_room.vc_room %} -{% block buttons %} - {{ render_join_button(vc_room, event_vc_room, "i-button-small event-service-right-button join-button") }} - {{ render_make_me_owner(event, vc_room, event_vc_room, extra_classes="i-button-small") }} -{% endblock %} +{% call render_join_button(vc_room, event_vc_room, true) %} + {{ template_hook('event-timetable-vc-extra-buttons', event_vc_room=event_vc_room) }} +{% endcall %} diff --git a/vc_zoom/pyproject.toml b/vc_zoom/pyproject.toml index 1877c215d..4c689c28e 100644 --- a/vc_zoom/pyproject.toml +++ b/vc_zoom/pyproject.toml @@ -2,7 +2,7 @@ name = 'indico-plugin-vc-zoom' description = 'Zoom video-conferencing plugin for Indico' readme = 'README.md' -version = '3.3' +version = '3.3.1' license = 'MIT' authors = [ { name = 'Indico Team', email = 'indico-team@cern.ch' }, @@ -15,7 +15,7 @@ classifiers = [ 'Programming Language :: Python :: 3.12', ] requires-python = '>=3.12.2, <3.13' -dependencies = ['indico>=3.3'] +dependencies = ['indico>=3.3.5.dev0'] [project.urls] GitHub = 'https://github.com/indico/indico-plugins' diff --git a/vc_zoom/tests/operation_test.py b/vc_zoom/tests/operation_test.py index 62b73cbfa..b9257a39b 100644 --- a/vc_zoom/tests/operation_test.py +++ b/vc_zoom/tests/operation_test.py @@ -5,103 +5,38 @@ # them and/or modify them under the terms of the MIT License; # see the LICENSE file for more details. -import pytest +from datetime import datetime +from zoneinfo import ZoneInfo -from indico.core.plugins import plugin_engine -from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomLinkType, VCRoomStatus -from indico_vc_zoom.plugin import ZoomPlugin +TZ = ZoneInfo('Europe/Zurich') -@pytest.fixture -def zoom_plugin(app): - """Return a callable which lets you create dummy Zoom room occurrences.""" - plugin = ZoomPlugin(plugin_engine, app) - plugin.settings.set('email_domains', 'megacorp.xyz') - return plugin - - -@pytest.fixture -def create_meeting(create_user, dummy_event, db, zoom_plugin): - def _create_meeting(name='New Room'): - user_joe = create_user(1, email='don.orange@megacorp.xyz') - - vc_room = VCRoom( - type='zoom', - status=VCRoomStatus.created, - name=name, - created_by_id=0, - data={ - 'description': 'something something', - 'password': '1234', - 'host': user_joe.identifier, - 'meeting_type': 'meeting', - 'mute_host_video': False, - 'mute_audio': False, - 'mute_participant_video': False, - 'waiting_room': False - } - ) - VCRoomEventAssociation(linked_event=dummy_event, vc_room=vc_room, link_type=VCRoomLinkType.event, data={}) - db.session.flush() - zoom_plugin.create_room(vc_room, dummy_event) - return vc_room - return _create_meeting - - -@pytest.fixture -def zoom_api(create_user, mocker): - """Mock some Zoom API endpoints.""" - api_create_meeting = mocker.patch('indico_vc_zoom.plugin.ZoomIndicoClient.create_meeting') - api_create_meeting.return_value = { - 'id': '12345abc', - 'join_url': 'https://example.com/kitties', - 'start_url': 'https://example.com/puppies', - 'password': '1234', - 'host_id': 'don.orange@megacorp.xyz', - 'topic': 'New Room', - 'agenda': 'something something', - 'settings': { - 'host_video': True, - 'mute_upon_entry': False, - 'participant_video': True, - 'waiting_room': False - } - } - - api_update_meeting = mocker.patch('indico_vc_zoom.plugin.ZoomIndicoClient.update_meeting') - api_update_meeting.return_value = {} - - create_user(1, email='don.orange@megacorp.xyz') - - api_get_user = mocker.patch('indico_vc_zoom.plugin.ZoomIndicoClient.get_user') - api_get_user.return_value = { - 'id': '7890abcd', - 'email': 'don.orange@megacorp.xyz' - } - - api_get_meeting = mocker.patch('indico_vc_zoom.plugin.ZoomIndicoClient.get_meeting') - api_get_meeting.return_value = api_create_meeting.return_value - - return { - 'create_meeting': api_create_meeting, - 'get_meeting': api_get_meeting, - 'update_meeting': api_update_meeting, - 'get_user': api_get_user - } - - -def test_room_creation(create_meeting, zoom_api): - vc_room = create_meeting() +def test_room_creation(create_zoom_meeting, zoom_api, create_event): + event = create_event( + creator=zoom_api['user'], + start_dt=datetime(2024, 3, 1, 16, 0, tzinfo=TZ), + end_dt=datetime(2024, 3, 1, 18, 0, tzinfo=TZ), + title='Test Event #1', + creator_has_privileges=True, + ) + vc_room = create_zoom_meeting(event, 'event') assert vc_room.data['url'] == 'https://example.com/kitties' assert vc_room.data['host'] == 'User:1' assert zoom_api['create_meeting'].called -def test_password_change(create_user, mocker, create_meeting, zoom_plugin, zoom_api): - create_user(2, email='joe.bidon@megacorp.xyz') - vc_room = create_meeting() - vc_room.data['password'] = '1337' +def test_password_change(create_user, mocker, create_event, create_zoom_meeting, zoom_plugin, zoom_api): + event = create_event( + creator=zoom_api['user'], + start_dt=datetime(2024, 3, 1, 16, 0, tzinfo=TZ), + end_dt=datetime(2024, 3, 1, 18, 0, tzinfo=TZ), + title='Test Event #1', + creator_has_privileges=True, + ) + + vc_room = create_zoom_meeting(event, 'event') + vc_room.data['password'] = '12341234' # simulate changes between calls of "GET meeting" def _get_meeting(self, meeting_id): @@ -109,14 +44,14 @@ def _get_meeting(self, meeting_id): 'id': meeting_id, 'join_url': 'https://example.com/llamas' if _get_meeting.called else 'https://example.com/kitties', 'start_url': 'https://example.com/puppies', - 'password': '1337' if _get_meeting.called else '1234', + 'password': '12341234' if _get_meeting.called else '13371337', 'host_id': 'don.orange@megacorp.xyz', - 'topic': 'New Room', - 'agenda': 'something something', + 'topic': 'Zoom Meeting', + 'agenda': 'nothing to add', 'settings': { - 'host_video': True, - 'mute_upon_entry': False, - 'participant_video': True, + 'host_video': False, + 'mute_upon_entry': True, + 'participant_video': False, 'waiting_room': False } } @@ -127,9 +62,9 @@ def _get_meeting(self, meeting_id): zoom_plugin.update_room(vc_room, vc_room.events[0].event) - zoom_api['update_meeting'].assert_called_with('12345abc', { - 'password': '1337' + zoom_api['update_meeting'].assert_called_with('zmeeting1', { + 'password': '12341234', }) - assert vc_room.data['password'] == '1337' + assert vc_room.data['password'] == '12341234' assert vc_room.data['url'] == 'https://example.com/llamas'