Merge branch 'version_2' of ../../indico_zoom_plugin into vc-vidyo-room-name-sanitization

This commit is contained in:
Pedro Ferreira 2020-11-10 11:25:03 +01:00
commit f820fc75ba
37 changed files with 1745 additions and 0 deletions

37
vc_zoom/.flake8 Normal file
View File

@ -0,0 +1,37 @@
[flake8]
max-line-length = 120
# colored output
format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
# decent quote styles
inline-quotes = single
multiline-quotes = single
docstring-quotes = double
avoid-escape = true
exclude =
build
dist
docs
ext_modules
htmlcov
indico.egg-info
node_modules
.*/
# TODO: remove the next two entries and use extend-exclude once flake8 3.8.0 is out
.git
__pycache__
ignore =
# allow omitting whitespace around arithmetic operators
E226
# don't require specific wrapping before/after binary operators
W503
W504
# allow assigning lambdas (it's useful for single-line functions defined inside other functions)
E731
# while single quotes are nicer, we have double quotes in way too many places
Q000
# for non-docstring multiline strings we don't really enforce a quote style
Q001

85
vc_zoom/.gitignore vendored Executable file
View File

@ -0,0 +1,85 @@
.vscode/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
venv/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/build/
docs/source/generated/
# pytest
.pytest_cache/
# PyBuilder
target/
# Editor files
#mac
.DS_Store
*~
#vim
*.swp
*.swo
#pycharm
.idea/*
#Ipython Notebook
.ipynb_checkpoints
webpack-build-config.json
url_map.json

6
vc_zoom/MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
graft indico_vc_zoom/static
graft indico_vc_zoom/migrations
graft indico_vc_zoom/templates
graft indico_vc_zoom/translations
global-exclude *.pyc __pycache__ .keep

7
vc_zoom/README.md Normal file
View File

@ -0,0 +1,7 @@
Indico Plugin for Zoom based on Vidyo plugin.
To obtain Api key and api secret, please visit [https://marketplace.zoom.us/docs/guides/auth/jwt](https://marketplace.zoom.us/docs/guides/auth/jwt)
Not ready for production.
Developed by Giovanni Mariano @ ENEA Frascati

1
vc_zoom/conftest.py Normal file
View File

@ -0,0 +1 @@
pytest_plugins = 'indico'

View File

@ -0,0 +1,6 @@
from __future__ import unicode_literals
from indico.util.i18n import make_bound_gettext
_ = make_bound_gettext('vc_zoom')

View File

@ -0,0 +1,4 @@
from .client import ZoomIndicoClient, ZoomClient
__all__ = ['ZoomIndicoClient', 'ZoomClient']

View File

@ -0,0 +1,236 @@
from __future__ import absolute_import, unicode_literals
import time
import jwt
from requests import Session
from requests.exceptions import HTTPError
from pytz import utc
def format_iso_dt(d):
"""Convertdatetime objects to a UTC-based string.
:param d: The :class:`datetime.datetime` to convert to a string
:returns: The string representation of the date
"""
return d.astimezone(utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def _handle_response(resp, expected_code=200, expects_json=True):
resp.raise_for_status()
if resp.status_code != expected_code:
raise HTTPError("Unexpected status code {}".format(resp.status_code), response=resp)
if expects_json:
return resp.json()
else:
return resp
class APIException(Exception):
pass
class BaseComponent(object):
def __init__(self, base_uri, config, timeout):
self.base_uri = base_uri
self.config = config
self.timeout = timeout
@property
def token(self):
header = {"alg": "HS256", "typ": "JWT"}
payload = {"iss": self.config['api_key'], "exp": int(time.time() + 3600)}
token = jwt.encode(payload, self.config['api_secret'], algorithm="HS256", headers=header)
return token.decode("utf-8")
@property
def session(self):
session = Session()
session.headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {}'.format(self.token)
}
return session
class MeetingComponent(BaseComponent):
def list(self, user_id, **kwargs):
return self.get(
"{}/users/{}/meetings".format(self.base_uri, user_id), params=kwargs
)
def create(self, user_id, **kwargs):
if kwargs.get("start_time"):
kwargs["start_time"] = format_iso_dt(kwargs["start_time"])
return self.session.post(
"{}/users/{}/meetings".format(self.base_uri, user_id),
json=kwargs
)
def get(self, meeting_id, **kwargs):
return self.session.get("{}/meetings/{}".format(self.base_uri, meeting_id), json=kwargs)
def update(self, meeting_id, **kwargs):
if kwargs.get("start_time"):
kwargs["start_time"] = format_iso_dt(kwargs["start_time"])
return self.session.patch(
"{}/meetings/{}".format(self.base_uri, meeting_id), json=kwargs
)
def delete(self, meeting_id, **kwargs):
return self.session.delete(
"{}/meetings/{}".format(self.base_uri, meeting_id), json=kwargs
)
class WebinarComponent(BaseComponent):
def list(self, user_id, **kwargs):
return self.get(
"{}/users/{}/webinars".format(self.base_uri, user_id), params=kwargs
)
def create(self, user_id, **kwargs):
if kwargs.get("start_time"):
kwargs["start_time"] = format_iso_dt(kwargs["start_time"])
return self.session.post(
"{}/users/{}/webinars".format(self.base_uri, user_id),
json=kwargs
)
def get(self, meeting_id, **kwargs):
return self.session.get("{}/webinars/{}".format(self.base_uri, meeting_id), json=kwargs)
def update(self, meeting_id, **kwargs):
if kwargs.get("start_time"):
kwargs["start_time"] = format_iso_dt(kwargs["start_time"])
return self.session.patch(
"{}/webinars/{}".format(self.base_uri, meeting_id), json=kwargs
)
def delete(self, meeting_id, **kwargs):
return self.session.delete(
"{}/webinars/{}".format(self.base_uri, meeting_id), json=kwargs
)
class UserComponent(BaseComponent):
def me(self):
return self.get('me')
def list(self, **kwargs):
return self.session.get("{}/users".format(self.base_uri), params=kwargs)
def create(self, **kwargs):
return self.session.post("{}/users".format(self.base_uri), params=kwargs)
def update(self, user_id, **kwargs):
return self.session.patch("{}/users/{}".format(self.base_uri, user_id), params=kwargs)
def delete(self, user_id, **kwargs):
return self.session.delete("{}/users/{}".format(self.base_uri, user_id), params=kwargs)
def add_assistant(self, user_id, **kwargs):
return self.session.post("{}/users/{}/assistants".format(self.base_uri, user_id), json=kwargs)
def get_assistants(self, user_id, **kwargs):
return self.session.get("{}/users/{}/assistants".format(self.base_uri, user_id), params=kwargs)
def get(self, user_id, **kwargs):
return self.session.get("{}/users/{}".format(self.base_uri, user_id), params=kwargs)
class ZoomClient(object):
"""Zoom REST API Python Client."""
_components = {
'user': UserComponent,
'meeting': MeetingComponent,
'webinar': WebinarComponent
}
def __init__(self, api_key, api_secret, timeout=15):
"""Create a new Zoom client.
:param api_key: the Zoom JWT API key
:param api_secret: the Zoom JWT API Secret
:param timeout: the time out to use for API requests
"""
BASE_URI = "https://api.zoom.us/v2"
# Setup the config details
config = {
"api_key": api_key,
"api_secret": api_secret
}
# Instantiate the components
self.components = {
key: component(base_uri=BASE_URI, config=config, timeout=timeout)
for key, component in self._components.viewitems()
}
@property
def meeting(self):
"""Get the meeting component."""
return self.components.get("meeting")
@property
def user(self):
"""Get the user component."""
return self.components.get("user")
@property
def webinar(self):
"""Get the user component."""
return self.components.get("webinar")
class ZoomIndicoClient(object):
def __init__(self):
self._refresh_client()
def _refresh_client(self):
from indico_vc_zoom.plugin import ZoomPlugin
settings = ZoomPlugin.settings
self.client = ZoomClient(
settings.get('api_key'),
settings.get('api_secret')
)
def create_meeting(self, user_id, **kwargs):
return _handle_response(self.client.meeting.create(user_id, **kwargs), 201)
def get_meeting(self, meeting_id):
return _handle_response(self.client.meeting.get(meeting_id))
def update_meeting(self, meeting_id, data):
return _handle_response(self.client.meeting.update(meeting_id, **data), 204, expects_json=False)
def delete_meeting(self, meeting_id):
return _handle_response(self.client.meeting.delete(meeting_id), 204, expects_json=False)
def create_webinar(self, user_id, **kwargs):
return _handle_response(self.client.webinar.create(user_id, **kwargs), 201)
def get_webinar(self, webinar_id):
return _handle_response(self.client.webinar.get(webinar_id))
def update_webinar(self, webinar_id, data):
return _handle_response(self.client.webinar.update(webinar_id, **data), 204, expects_json=False)
def delete_webinar(self, webinar_id):
return _handle_response(self.client.webinar.delete(webinar_id), 204, expects_json=False)
def check_user_meeting_time(self, user_id, start_dt, end_dt):
pass
def get_user(self, user_id):
return _handle_response(self.client.user.get(user_id))
def get_assistants_for_user(self, user_id):
return _handle_response(self.client.user.get_assistants(user_id))
def add_assistant_to_user(self, user_id, assistant_email):
return _handle_response(self.client.user.add_assistant(user_id, assistants=[{'email': assistant_email}]), 201)

View File

@ -0,0 +1,14 @@
from __future__ import unicode_literals
from indico.core.plugins import IndicoPluginBlueprint
from indico_vc_zoom.controllers import RHRoomHost
blueprint = IndicoPluginBlueprint('vc_zoom', 'indico_vc_zoom')
# Room management
# using any(zoom) instead of defaults since the event vc room locator
# includes the service and normalization skips values provided in 'defaults'
blueprint.add_url_rule('/event/<confId>/manage/videoconference/zoom/<int:event_vc_room_id>/room-host',
'set_room_host', RHRoomHost, methods=('POST',))

View File

@ -0,0 +1,34 @@
from __future__ import unicode_literals
import click
from terminaltables import AsciiTable
from indico.cli.core import cli_group
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomStatus
@cli_group(name='zoom')
def cli():
"""Manage the Zoom plugin."""
@cli.command()
@click.option('--status', type=click.Choice(['deleted', 'created']))
def rooms(status=None):
"""Lists all Zoom rooms"""
room_query = VCRoom.find(type='zoom')
table_data = [['ID', 'Name', 'Status', 'Zoom ID']]
if status:
room_query = room_query.filter(VCRoom.status == VCRoomStatus.get(status))
for room in room_query:
table_data.append([unicode(room.id), room.name, room.status.name, unicode(room.data['zoom_id'])])
table = AsciiTable(table_data)
for col in (0, 3, 4):
table.justify_columns[col] = 'right'
print table.table

View File

@ -0,0 +1,34 @@
// This file is part of the Indico plugins.
// Copyright (C) 2002 - 2019 CERN
//
// 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.
$(function() {
$('.vc-toolbar').dropdown({
positioning: {
level1: {my: 'right top', at: 'right bottom', offset: '0px 0px'},
},
});
$('.vc-toolbar .action-make-owner').click(function() {
const $this = $(this);
$.ajax({
url: $this.data('href'),
method: 'POST',
complete: IndicoUI.Dialogs.Util.progress(),
})
.done(function(result) {
if (handleAjaxError(result)) {
return;
} else {
location.reload();
}
})
.fail(function(error) {
handleAjaxError(error);
});
});
});

View File

@ -0,0 +1,26 @@
from __future__ import unicode_literals
from flask import flash, jsonify, session
from indico.core.db import db
from indico.modules.vc.controllers import RHVCSystemEventBase
from indico.modules.vc.exceptions import VCRoomError
from indico.util.i18n import _
class RHRoomHost(RHVCSystemEventBase):
def _process(self):
result = {}
self.vc_room.data['host'] = session.user.identifier
try:
self.plugin.update_room(self.vc_room, self.event)
except VCRoomError as err:
result['error'] = {'message': err.message}
result['success'] = False
db.session.rollback()
else:
flash(_("You are now the host of room '{room.name}'".format(room=self.vc_room)), 'success')
result['success'] = True
return jsonify(result)

View File

@ -0,0 +1,93 @@
from __future__ import unicode_literals
from flask import session
from wtforms.fields.core import BooleanField
from wtforms.fields.simple import TextAreaField
from wtforms.validators import DataRequired, ValidationError
from indico.modules.vc.forms import VCRoomAttachFormBase, VCRoomFormBase
from indico.util.user import principal_from_identifier
from indico.web.forms.base import generated_data
from indico.web.forms.fields import IndicoRadioField, PrincipalField
from indico.web.forms.validators import HiddenUnless
from indico.web.forms.widgets import SwitchWidget
from indico_vc_zoom import _
class VCRoomAttachForm(VCRoomAttachFormBase):
password_visibility = IndicoRadioField(_("Password visibility"),
description=_("Who should be able to know this meeting's password"),
orientation='horizontal',
choices=[
('everyone', _('Everyone')),
('logged_in', _('Logged-in users')),
('no_one', _("No one"))])
class VCRoomForm(VCRoomFormBase):
"""Contains all information concerning a Zoom booking."""
advanced_fields = {'mute_audio', 'mute_host_video', 'mute_participant_video'} | VCRoomFormBase.advanced_fields
skip_fields = advanced_fields | VCRoomFormBase.conditional_fields
meeting_type = IndicoRadioField(_("Meeting Type"),
description=_("The type of Zoom meeting ot be created"),
orientation='horizontal',
choices=[
('regular', _('Regular Meeting')),
('webinar', _('Webinar'))])
host_choice = IndicoRadioField(_("Meeting Host"), [DataRequired()],
choices=[('myself', _("Myself")), ('someone_else', _("Someone else"))])
host_user = PrincipalField(_("User"),
[HiddenUnless('host_choice', 'someone_else'), DataRequired()])
password_visibility = IndicoRadioField(_("Password visibility"),
description=_("Who should be able to know this meeting's password"),
orientation='horizontal',
choices=[
('everyone', _('Everyone')),
('logged_in', _('Logged-in users')),
('no_one', _("No one"))])
mute_audio = BooleanField(_('Mute audio'),
[HiddenUnless('meeting_type', 'regular')],
widget=SwitchWidget(),
description=_('Participants will join the VC room muted by default '))
mute_host_video = BooleanField(_('Mute video (host)'),
widget=SwitchWidget(),
description=_('The host will join the VC room with video disabled'))
mute_participant_video = BooleanField(_('Mute video (participants)'),
[HiddenUnless('meeting_type', 'regular')],
widget=SwitchWidget(),
description=_('Participants will join the VC room with video disabled'))
waiting_room = BooleanField(_('Waiting room'),
[HiddenUnless('meeting_type', 'regular')],
widget=SwitchWidget(),
description=_('Participants may be kept in a waiting room by the host'))
description = TextAreaField(_('Description'), description=_('Optional description for this room'))
def __init__(self, *args, **kwargs):
defaults = kwargs['obj']
if defaults.host_user is None and defaults.host is not None:
host = principal_from_identifier(defaults.host)
defaults.host_choice = 'myself' if host == session.user else 'someone_else'
defaults.host_user = None if host == session.user else host
super(VCRoomForm, self).__init__(*args, **kwargs)
@generated_data
def host(self):
if self.host_choice is None:
return None
return session.user.identifier if self.host_choice.data == 'myself' else self.host_user.data.identifier
def validate_host_user(self, field):
if not field.data:
raise ValidationError(_("Unable to find this user in Indico."))

View File

@ -0,0 +1,51 @@
from flask import request
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomStatus
from indico.web.http_api.hooks.base import HTTPAPIHook
class DeleteVCRoomAPI(HTTPAPIHook):
PREFIX = 'api'
TYPES = ('deletevcroom',)
RE = r'zoom'
GUEST_ALLOWED = False
VALID_FORMATS = ('json',)
COMMIT = True
HTTP_POST = True
def _has_access(self, user):
from indico_vc_zoom.plugin import ZoomPlugin
return user in ZoomPlugin.settings.acls.get('managers')
def _getParams(self):
super(DeleteVCRoomAPI, self)._getParams()
self._room_ids = map(int, request.form.getlist('rid'))
def api_deletevcroom(self, user):
from indico_vc_zoom.plugin import ZoomPlugin
from indico_vc_zoom.api import APIException
success = []
failed = []
not_in_db = []
for rid in self._room_ids:
room = VCRoom.query.filter(VCRoom.type == 'zoom',
VCRoom.status == VCRoomStatus.created,
VCRoom.data.contains({'zoom_id': str(rid)})).first()
if not room:
not_in_db.append(rid)
continue
try:
room.plugin.delete_meeting(room, None)
except APIException:
failed.append(rid)
ZoomPlugin.logger.exception('Could not delete VC room %s', room)
else:
room.status = VCRoomStatus.deleted
success.append(rid)
ZoomPlugin.logger.info('%s deleted', room)
return {'success': success, 'failed': failed, 'missing': not_in_db}

View File

@ -0,0 +1,37 @@
from __future__ import unicode_literals
from indico.web.flask.templating import get_template_module
from indico.core.notifications import make_email, send_email
from indico.util.user import principal_from_identifier
def notify_host_start_url(vc_room):
from indico_vc_zoom.plugin import ZoomPlugin
user = principal_from_identifier(vc_room.data['host'])
to_list = {user.email}
template_module = get_template_module(
'vc_zoom:emails/notify_start_url.html',
plugin=ZoomPlugin.instance,
vc_room=vc_room,
user=user
)
email = make_email(to_list, template=template_module, html=True)
send_email(email, None, 'Zoom')
def notify_new_host(actor, vc_room):
from indico_vc_zoom.plugin import ZoomPlugin
template_module = get_template_module(
'vc_zoom:emails/notify_new_host.html',
plugin=ZoomPlugin.instance,
vc_room=vc_room,
actor=actor
)
new_host = principal_from_identifier(vc_room.data['host'])
email = make_email({new_host.email}, cc_list={actor.email}, template=template_module, html=True)
send_email(email, None, 'Zoom')

View File

@ -0,0 +1,478 @@
from __future__ import unicode_literals
import random
import string
from flask import flash, session
from requests.exceptions import HTTPError
from sqlalchemy.orm.attributes import flag_modified
from werkzeug.exceptions import Forbidden, NotFound
from wtforms.fields.core import BooleanField
from wtforms.fields import IntegerField, TextAreaField
from wtforms.fields.html5 import EmailField, URLField
from wtforms.fields.simple import StringField
from wtforms.validators import DataRequired, NumberRange
from indico.core import signals
from indico.core.config import config
from indico.core.plugins import IndicoPlugin, render_plugin_template, url_for_plugin
from indico.modules.events.views import WPSimpleEventDisplay
from indico.modules.vc import VCPluginMixin, VCPluginSettingsFormBase
from indico.modules.vc.exceptions import VCRoomError, VCRoomNotFoundError
from indico.modules.vc.models.vc_rooms import VCRoom
from indico.modules.vc.views import WPVCEventPage, WPVCManageEvent
from indico.util.date_time import now_utc
from indico.util.user import principal_from_identifier
from indico.web.forms.fields.simple import IndicoPasswordField
from indico.web.forms.widgets import CKEditorWidget, SwitchWidget
from indico.web.http_api.hooks.base import HTTPAPIHook
from indico_vc_zoom import _
from indico_vc_zoom.api import ZoomIndicoClient
from indico_vc_zoom.blueprint import blueprint
from indico_vc_zoom.cli import cli
from indico_vc_zoom.forms import VCRoomAttachForm, VCRoomForm
from indico_vc_zoom.http_api import DeleteVCRoomAPI
from indico_vc_zoom.notifications import notify_new_host, notify_host_start_url
from indico_vc_zoom.util import find_enterprise_email
def _gen_random_password():
return ''.join(random.sample(string.ascii_lowercase + string.ascii_uppercase + string.digits, 10))
def _fetch_zoom_meeting(vc_room, client=None, is_webinar=False):
try:
client = client or ZoomIndicoClient()
if is_webinar:
return client.get_webinar(vc_room.data['zoom_id'])
return client.get_meeting(vc_room.data['zoom_id'])
except HTTPError as e:
if e.response.status_code in {400, 404}:
# Indico will automatically mark this room as deleted
raise VCRoomNotFoundError(_("This room has been deleted from Zoom"))
else:
ZoomPlugin.logger.exception('Error getting Zoom Room: %s', e.response.content)
raise VCRoomError(_("Problem fetching room from Zoom. Please contact support if the error persists."))
def _update_zoom_meeting(zoom_id, changes, is_webinar=False):
client = ZoomIndicoClient()
try:
if is_webinar:
client.update_webinar(zoom_id, changes)
else:
client.update_meeting(zoom_id, changes)
except HTTPError as e:
ZoomPlugin.logger.exception("Error updating meeting '%s': %s", zoom_id, e.response.content)
raise VCRoomError(_("Can't update meeting. Please contact support if the error persists."))
def _get_schedule_args(obj):
duration = obj.end_dt - obj.start_dt
if obj.start_dt < now_utc():
return {}
return {
'start_time': obj.start_dt,
'duration': duration.total_seconds() / 60,
}
class PluginSettingsForm(VCPluginSettingsFormBase):
support_email = EmailField(_('Zoom email support'))
api_key = StringField(_('API Key'), [DataRequired()])
api_secret = IndicoPasswordField(_('API Secret'), [DataRequired()], toggle=True)
email_domains = StringField(_('E-mail domains'), [DataRequired()],
description=_("Comma-separated list of e-mail domains which can use the Zoom API."))
assistant_id = StringField(_('Assistant Zoom ID'), [DataRequired()],
description=_('Account to be used as owner of all rooms. It will get "assistant" '
'privileges on all accounts for which it books rooms'))
mute_audio = BooleanField(_('Mute audio'),
widget=SwitchWidget(),
description=_('Participants will join the VC room muted by default '))
mute_host_video = BooleanField(_('Mute video (host)'),
widget=SwitchWidget(),
description=_('The host will join the VC room with video disabled'))
mute_participant_video = BooleanField(_('Mute video (participants)'),
widget=SwitchWidget(),
description=_('Participants will join the VC room with video disabled'))
join_before_host = BooleanField(_('Join Before Host'),
widget=SwitchWidget(),
description=_('Allow participants to join the meeting before the host starts the '
'meeting. Only used for scheduled or recurring meetings.'))
waiting_room = BooleanField(_('Waiting room'),
widget=SwitchWidget(),
description=_('Participants may be kept in a waiting room by the host'))
num_days_old = IntegerField(_('VC room age threshold'), [NumberRange(min=1), DataRequired()],
description=_('Number of days after an Indico event when a videoconference room is '
'considered old'))
max_rooms_warning = IntegerField(_('Max. num. VC rooms before warning'), [NumberRange(min=1), DataRequired()],
description=_('Maximum number of rooms until a warning is sent to the managers'))
zoom_phone_link = URLField(_('ZoomVoice phone number'),
description=_('Link to the list of ZoomVoice phone numbers'))
creation_email_footer = TextAreaField(_('Creation email footer'), widget=CKEditorWidget(),
description=_('Footer to append to emails sent upon creation of a VC room'))
send_host_url = BooleanField(_('Send host URL'),
widget=SwitchWidget(),
description=_('Whether to send an e-mail with the Host URL to the meeting host upon '
'creation of a meeting'))
class ZoomPlugin(VCPluginMixin, IndicoPlugin):
"""Zoom
Zoom Plugin for Indico."""
configurable = True
settings_form = PluginSettingsForm
vc_room_form = VCRoomForm
vc_room_attach_form = VCRoomAttachForm
friendly_name = 'Zoom'
def init(self):
super(ZoomPlugin, self).init()
self.connect(signals.plugin.cli, self._extend_indico_cli)
self.connect(signals.event.times_changed, self._times_changed)
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)
HTTPAPIHook.register(DeleteVCRoomAPI)
@property
def default_settings(self):
return dict(VCPluginMixin.default_settings, **{
'support_email': config.SUPPORT_EMAIL,
'assistant_id': config.SUPPORT_EMAIL,
'api_key': '',
'api_secret': '',
'email_domains': '',
'mute_host_video': True,
'mute_audio': True,
'mute_participant_video': True,
'join_before_host': True,
'waiting_room': False,
'num_days_old': 5,
'max_rooms_warning': 5000,
'zoom_phone_link': None,
'creation_email_footer': None,
'send_host_url': False
})
@property
def logo_url(self):
return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png')
@property
def icon_url(self):
return url_for_plugin(self.name + '.static', filename='images/zoom_logo.png')
def create_form(self, event, existing_vc_room=None, existing_event_vc_room=None):
"""Override the default room form creation mechanism."""
form = super(ZoomPlugin, self).create_form(
event,
existing_vc_room=existing_vc_room,
existing_event_vc_room=existing_event_vc_room
)
if existing_vc_room:
# if we're editing a VC room, we will not allow the meeting type to be changed
form.meeting_type.render_kw = {'disabled': True}
if form.data['meeting_type'] == 'webinar':
# webinar hosts cannot be changed through the API
form.host_choice.render_kw = {'disabled': True}
form.host_user.render_kw = {'disabled': True}
return form
def _extend_indico_cli(self, sender, **kwargs):
return cli
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
super(ZoomPlugin, self).update_data_association(event, vc_room, room_assoc, data)
if vc_room.data:
# this is not a new room
if association_is_new:
# this means we are updating an existing meeting with a new vc_room-event association
_update_zoom_meeting(vc_room.data['zoom_id'], {
'start_time': None,
'duration': None,
'type': 3
})
elif room_assoc.link_object != old_link:
# the booking should now be linked to something else
new_schedule_args = _get_schedule_args(room_assoc.link_object)
meeting = _fetch_zoom_meeting(vc_room)
current_schedule_args = {k: meeting[k] for k in {'start_time', 'duration'}}
# check whether the start time / duration of the scheduled meeting differs
if new_schedule_args != current_schedule_args:
_update_zoom_meeting(vc_room.data['zoom_id'], new_schedule_args)
room_assoc.data['password_visibility'] = data.pop('password_visibility')
flag_modified(room_assoc, 'data')
def update_data_vc_room(self, vc_room, data, is_new=False):
super(ZoomPlugin, self).update_data_vc_room(vc_room, data)
fields = {'description'}
if data['meeting_type'] == 'webinar':
fields |= {'mute_host_video'}
if is_new:
fields |= {'host', 'meeting_type'}
else:
fields |= {
'meeting_type', 'host', 'mute_audio', 'mute_participant_video', 'mute_host_video', 'join_before_host',
'waiting_room'
}
for key in fields:
if key in data:
vc_room.data[key] = data.pop(key)
flag_modified(vc_room, 'data')
def _check_indico_is_assistant(self, user_id):
client = ZoomIndicoClient()
assistant_id = self.settings.get('assistant_id')
if user_id != assistant_id:
try:
assistants = {assist['email'] for assist in client.get_assistants_for_user(user_id)['assistants']}
except HTTPError as e:
if e.response.status_code == 404:
raise NotFound(_("No Zoom account found for this user"))
self.logger.exception('Error getting assistants for account %s: %s', user_id, e.response.content)
raise VCRoomError(_("Problem getting information about Zoom account"))
if assistant_id not in assistants:
client.add_assistant_to_user(user_id, assistant_id)
def create_room(self, vc_room, event):
"""Create a new Zoom room for an event, given a VC room.
In order to create the Zoom room, the function will try to get
a valid e-mail address for the user in question, which can be
use with the Zoom API.
:param vc_room: the VC room from which to create the Zoom room
:param event: the event to the Zoom room will be attached
"""
client = ZoomIndicoClient()
host = principal_from_identifier(vc_room.data['host'])
host_email = find_enterprise_email(host)
# get the object that this booking is linked to
vc_room_assoc = vc_room.events[0]
link_obj = vc_room_assoc.link_object
is_webinar = vc_room.data['meeting_type'] == 'webinar'
scheduling_args = _get_schedule_args(link_obj) if link_obj.start_dt else {}
self._check_indico_is_assistant(host_email)
try:
settings = {
'host_video': vc_room.data['mute_host_video'],
}
kwargs = {}
if is_webinar:
kwargs = {
'type': 5 if scheduling_args else 6,
'host_email': host_email
}
else:
kwargs = {
'type': 2 if scheduling_args else 3,
'schedule_for': host_email
}
settings.update({
'mute_upon_entry': vc_room.data['mute_audio'],
'participant_video': not vc_room.data['mute_participant_video'],
'waiting_room': vc_room.data['waiting_room'],
'join_before_host': self.settings.get('join_before_host'),
})
kwargs.update({
'topic': vc_room.name,
'password': _gen_random_password(),
'timezone': event.timezone,
'settings': settings
})
kwargs.update(scheduling_args)
if is_webinar:
meeting_obj = client.create_webinar(self.settings.get('assistant_id'), **kwargs)
else:
meeting_obj = client.create_meeting(self.settings.get('assistant_id'), **kwargs)
except HTTPError as e:
self.logger.exception('Error creating Zoom Room: %s', e.response.content)
raise VCRoomError(_("Could not create the room in Zoom. Please contact support if the error persists"))
vc_room.data.update({
'zoom_id': unicode(meeting_obj['id']),
'url': meeting_obj['join_url'],
'public_url': meeting_obj['join_url'].split('?')[0],
'start_url': meeting_obj['start_url'],
'password': meeting_obj['password'],
'host': host.identifier
})
flag_modified(vc_room, 'data')
# e-mail Host URL to meeting host
if self.settings.get('send_host_url'):
notify_host_start_url(vc_room)
def update_room(self, vc_room, event):
client = ZoomIndicoClient()
is_webinar = vc_room.data['meeting_type'] == 'webinar'
zoom_meeting = _fetch_zoom_meeting(vc_room, client=client, is_webinar=is_webinar)
changes = {}
host = principal_from_identifier(vc_room.data['host'])
host_id = zoom_meeting['host_id']
try:
host_data = client.get_user(host_id)
except HTTPError as e:
self.logger.exception("Error retrieving user '%s': %s", host_id, e.response.content)
raise VCRoomError(_("Can't get information about user. Please contact support if the error persists."))
# host changed
if host_data['email'] not in host.all_emails:
email = find_enterprise_email(host)
if not email:
raise Forbidden(_("This user doesn't seem to have an associated Zoom account"))
changes['host_email' if is_webinar else 'schedule_for'] = email
self._check_indico_is_assistant(email)
notify_new_host(session.user, vc_room)
if vc_room.name != zoom_meeting['topic']:
changes['topic'] = vc_room.name
zoom_meeting_settings = zoom_meeting['settings']
if vc_room.data['mute_host_video'] == zoom_meeting_settings['host_video']:
changes.setdefault('settings', {})['host_video'] = not vc_room.data['mute_host_video']
if not is_webinar:
if vc_room.data['mute_audio'] != zoom_meeting_settings['mute_upon_entry']:
changes.setdefault('settings', {})['mute_upon_entry'] = vc_room.data['mute_audio']
if vc_room.data['mute_participant_video'] == zoom_meeting_settings['participant_video']:
changes.setdefault('settings', {})['participant_video'] = not vc_room.data['mute_participant_video']
if vc_room.data['waiting_room'] != zoom_meeting_settings['waiting_room']:
changes.setdefault('settings', {})['waiting_room'] = vc_room.data['waiting_room']
if changes:
_update_zoom_meeting(vc_room.data['zoom_id'], changes, is_webinar=is_webinar)
def refresh_room(self, vc_room, event):
is_webinar = vc_room.data['meeting_type'] == 'webinar'
zoom_meeting = _fetch_zoom_meeting(vc_room, is_webinar=is_webinar)
vc_room.name = zoom_meeting['topic']
vc_room.data.update({
'url': zoom_meeting['join_url'],
'public_url': zoom_meeting['join_url'].split('?')[0],
'zoom_id': zoom_meeting['id']
})
flag_modified(vc_room, 'data')
def delete_room(self, vc_room, event):
client = ZoomIndicoClient()
zoom_id = vc_room.data['zoom_id']
is_webinar = vc_room.data['meeting_type'] == 'webinar'
try:
if is_webinar:
client.delete_webinar(zoom_id)
else:
client.delete_meeting(zoom_id)
except HTTPError as e:
# if there's a 404, there is no problem, since the room is supposed to be gone anyway
if not e.response.status_code == 404:
self.logger.exception('Error getting Zoom Room: %s', e.response.content)
raise VCRoomError(_("Problem fetching room from Zoom. Please contact support if the error persists."))
def get_blueprints(self):
return blueprint
def get_vc_room_form_defaults(self, event):
defaults = super(ZoomPlugin, self).get_vc_room_form_defaults(event)
defaults.update({
'meeting_type': 'regular',
'mute_audio': self.settings.get('mute_audio'),
'mute_host_video': self.settings.get('mute_host_video'),
'mute_participant_video': self.settings.get('mute_participant_video'),
'waiting_room': self.settings.get('waiting_room'),
'host_choice': 'myself',
'host_user': None,
'password_visibility': 'logged_in'
})
return defaults
def get_vc_room_attach_form_defaults(self, event):
defaults = super(ZoomPlugin, self).get_vc_room_attach_form_defaults(event)
defaults['password_visibility'] = 'logged_in'
return defaults
def can_manage_vc_room(self, user, room):
return (
user == principal_from_identifier(room.data['host']) or
super(ZoomPlugin, self).can_manage_vc_room(user, room)
)
def _merge_users(self, target, source, **kwargs):
super(ZoomPlugin, self)._merge_users(target, source, **kwargs)
for room in VCRoom.query.filter(
VCRoom.type == self.service_name, VCRoom.data.contains({'host': source.identifier})
):
room.data['host'] = target.id
flag_modified(room, 'data')
def get_notification_cc_list(self, action, vc_room, event):
return {principal_from_identifier(vc_room.data['host']).email}
def _render_vc_room_labels(self, event, vc_room, **kwargs):
if vc_room.plugin != self:
return
return render_plugin_template('room_labels.html', vc_room=vc_room)
def _times_changed(self, sender, obj, **kwargs):
from indico.modules.events.models.events import Event
from indico.modules.events.contributions.models.contributions import Contribution
from indico.modules.events.sessions.models.blocks import SessionBlock
if not hasattr(obj, 'vc_room_associations'):
return
if any(assoc.vc_room.type == 'zoom' and len(assoc.vc_room.events) == 1 for assoc in obj.vc_room_associations):
if sender == Event:
message = _("There are one or more scheduled Zoom meetings associated with this event which were not "
"automatically updated.")
elif sender == Contribution:
message = _("There are one or more scheduled Zoom meetings associated with contribution '{}' which "
" were not automatically updated.").format(obj.title)
elif sender == SessionBlock:
message = _("There are one or more scheduled Zoom meetings associated with this session block which "
"were not automatically updated.")
else:
return
flash(message, 'warning')

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@ -0,0 +1,21 @@
{% macro render_join_button(vc_room, event_vc_room, extra_classes="", is_manager=false) %}
{% if event_vc_room.data.password_visibility == 'everyone' or is_manager or
(session.user and event_vc_room.data.password_visibility == 'logged_in') %}
<a class="i-button icon-play highlight {{ extra_classes }}"
href="{{ vc_room.data.url }}" target="_blank">
{% trans %}Join{% endtrans %}
</a>
{% elif event_vc_room.data.password_visibility == 'no_one' %}
<a class="i-button icon-lock-center warning {{ extra_classes }}"
title="{% trans %}You will need a password to join this Zoom meeting{% endtrans %}"
href="{{ vc_room.data.public_url }}">
{% trans %}Join{% endtrans %}
</a>
{% else %}
<a class="i-button icon-lock-center {{ extra_classes }}"
title="{% trans %}This Zoom Meeting can only be seen by logged in users{% endtrans %}"
href="{{ url_for_login(request.relative_url) }}">
{% trans %}Please log in{% endtrans %}
</a>
{% endif %}
{% endmacro %}

View File

@ -0,0 +1,20 @@
{% extends 'vc/emails/created.html' %}
{% block plugin_specific_info %}
<li>
<strong>Host</strong>:
<span>{{ (vc_room.data.host|decodeprincipal).full_name }}</span>
</li>
<li>
<strong>Zoom URL</strong>:
<a href="{{ vc_room.data.url }}">{{ vc_room.data.url }}</a>
</li>
{% endblock %}
{% block custom_footer %}
{% if plugin.settings.get('creation_email_footer') %}
<hr>
{{ plugin.settings.get('creation_email_footer') | sanitize_html }}
{% endif %}
{% endblock %}

View File

@ -0,0 +1 @@
{% extends 'vc/emails/deleted.html' %}

View File

@ -0,0 +1,25 @@
{% extends 'emails/base.html' %}
{% block subject -%}
[{{ plugin.friendly_name }}] You are now hosting '{{ vc_room.name }}'
{%- endblock %}
{% block header -%}
<p>
<strong>{{ actor.full_name }}</strong> has just made you the host of Zoom Meeting '{{ vc_room.name }}'.
</p>
{% if plugin.settings.get('send_host_url') %}
<p>
<strong>ATTENTION:</strong>
You should not share this URL with anyone, since it will allow them to become meeting hosts!
</p>
<ul>
<li>
<strong>Host URL</strong>:
<a href="{{ vc_room.data.start_url }}">{{ vc_room.data.start_url }}</a>
</li>
</ul>
{% endif %}
{% block custom_footer %}{% endblock %}
{%- endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'emails/base.html' %}
{% block subject -%}
[{{ plugin.friendly_name }}] Host URL - {{ vc_room.name }}
{%- endblock %}
{% block header -%}
<p>
<strong>ATTENTION:</strong>
You should not share this URL with anyone, since it will allow them to become meeting hosts!
</p>
<ul>
<li>
<strong>Host URL</strong>:
<a href="{{ vc_room.data.start_url }}">{{ vc_room.data.start_url }}</a>
</li>
</ul>
{% block custom_footer %}{% endblock %}
{%- endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'emails/base.html' %}
{% block subject -%}
[{{ plugin.friendly_name }}] Room deleted from server: {{ vc_room.name }}
{%- endblock %}
{% block header -%}
<p>
The Zoom room "{{ vc_room.name }}" has been deleted from the Zoom server since it has not been used by any recent event.
</p>
<p>
You won't be able to attach it to any future events. If you need to do so, please create a new room.
</p>
{% block custom_footer %}{% endblock %}
{%- endblock %}

View File

@ -0,0 +1,6 @@
{% extends 'vc/event_buttons.html' %}
{% 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") }}
{% endblock %}

View File

@ -0,0 +1,32 @@
{% from '_clipboard_input.html' import clipboard_input %}
{% set host = vc_room.data.host %}
{% set phone_link = settings.get('zoom_phone_link') %}
<dl>
<dt>{% trans %}Zoom Meeting ID{% endtrans %}</dt>
<dd>{{ vc_room.data.zoom_id }}</dd>
{% if host %}
<dt>{% trans %}Host{% endtrans %}</dt>
<dd>{{ (host|decodeprincipal).full_name }}</dd>
{% endif %}
{% if event_vc_room.data.password_visibility == 'everyone' or is_manager or
(session.user and event_vc_room.data.password_visibility == 'logged_in') %}
<dt>{% trans %}Password{% endtrans %}</dt>
<dd>{{ vc_room.data.password }}</dd>
{% endif %}
{% if event_vc_room.data.show_autojoin %}
<dt class="large-row">{% trans %}Zoom URL{% endtrans %}</dt>
<dd class="large-row">
{{ clipboard_input(vc_room.data.url, name="vc-room-url-%s"|format(event_vc_room.id)) }}
</dd>
{% endif %}
{% if event_vc_room.data.show_phone_numbers and phone_link %}
<dt>
{% trans %}Useful links{% endtrans %}
</dt>
<dd>
<a href="{{ phone_link }}" target="_blank">
{% trans %}Phone numbers{% endtrans %}
</a>
</dd>
{% endif %}
</dl>

View File

@ -0,0 +1,37 @@
{% from '_password.html' import password %}
{% from '_clipboard_input.html' import clipboard_input %}
{% set host = vc_room.data.host %}
{% set phone_link = settings.get('zoom_phone_link') %}
<dl class="details-container">
<dt>{% trans %}Zoom Meeting ID{% endtrans %}</dt>
<dd>{{ vc_room.data.zoom_id }}</dd>
<dt>{% trans %}Host{% endtrans %}</dt>
<dd>
{{ (host|decodeprincipal).full_name }}
</dd>
<dt>{% trans %}Linked to{% endtrans %}</dt>
<dd>
{% set obj = event_vc_room.link_object %}
{% if obj is none %}
<em>(missing {{ event_vc_room.link_type.name }})</em>
{% elif event_vc_room.link_type.name == 'event' %}
{% trans %}the whole event{% endtrans %}
{% elif event_vc_room.link_type.name == 'contribution' %}
{% trans %}Contribution{% endtrans %}: {{ obj.title }}
{% elif event_vc_room.link_type.name == 'block' %}
{% trans %}Session{% endtrans %}: {{ obj.full_title }}
{% endif %}
</dd>
<dt>{% trans %}Password{% endtrans %}</dt>
<dd>{{ vc_room.data.password }}</dd>
<dt>{% trans %}Zoom URL{% endtrans %}</dt>
<dd>
{{ clipboard_input(vc_room.data.url, name="vc-room-url") }}
</dd>
<dt>{% trans %}Created on{% endtrans %}</dt>
<dd>{{ vc_room.created_dt | format_datetime(timezone=event.tzinfo) }}</dd>
{% if vc_room.modified_dt %}
<dt>{% trans %}Modified on{% endtrans %}</dt>
<dd>{{ vc_room.modified_dt | format_datetime(timezone=event.tzinfo) }}</dd>
{% endif %}
</dl>

View File

@ -0,0 +1,6 @@
{% extends 'vc/management_buttons.html' %}
{% from 'vc_zoom:buttons.html' import render_join_button %}
{% block buttons %}
{{ render_join_button(vc_room, event_vc_room, extra_classes="icon-play", is_manager=true) }}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% if vc_room.data.meeting_type == 'webinar' %}
<div class="i-label color-teal text-color outline"
title="{% trans %}This is a Zoom webinar{% endtrans %}">
{% trans %}Webinar{% endtrans %}
</div>
{% endif %}

View File

@ -0,0 +1,7 @@
{% extends 'vc/vc_room_timetable_buttons.html' %}
{% 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") }}
{% endblock %}

View File

@ -0,0 +1,23 @@
# Translations template for PROJECT.
# Copyright (C) 2015 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
#
# Translators:
# Thomas Baron <thomas.baron@cern.ch>, 2015
msgid ""
msgstr ""
"Project-Id-Version: Indico\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2015-03-11 16:21+0100\n"
"PO-Revision-Date: 2015-03-12 12:52+0000\n"
"Last-Translator: Thomas Baron <thomas.baron@cern.ch>\n"
"Language-Team: French (France) (http://www.transifex.com/projects/p/indico/language/fr_FR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
"Language: fr_FR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Indico"
msgstr "Indico"

View File

@ -0,0 +1,289 @@
# Translations template for PROJECT.
# Copyright (C) 2017 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
#
# Translators:
# Thomas Baron <thomas.baron@cern.ch>, 2015,2017
msgid ""
msgstr ""
"Project-Id-Version: Indico\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-10-18 11:55+0200\n"
"PO-Revision-Date: 2017-10-30 11:04+0000\n"
"Last-Translator: Thomas Baron <thomas.baron@cern.ch>\n"
"Language-Team: French (France) (http://www.transifex.com/indico/indico/language/fr_FR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.1\n"
"Language: fr_FR\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: indico_vc_zoom/controllers.py:38
msgid "You are now the owner of the room '{room.name}'"
msgstr "Vous êtes maintenant responsable de la salle '{room.name}'"
#: indico_vc_zoom/forms.py:32
msgid "The PIN must be a number"
msgstr "Le code confidentiel doit être un nombre entier"
#: indico_vc_zoom/forms.py:37
msgid "Show PIN"
msgstr "Afficher le code confidentiel"
#: indico_vc_zoom/forms.py:39
msgid "Show the VC Room PIN on the event page (insecure!)"
msgstr "Afficher le code confidentiel de la salle Vidyo sur la page de l'événement (peu sûr!)"
#: indico_vc_zoom/forms.py:40
msgid "Show Auto-join URL"
msgstr "Afficher l'URL de connexion"
#: indico_vc_zoom/forms.py:42
msgid "Show the auto-join URL on the event page"
msgstr "Afficher l'URL de connexion sur la page de l'événement"
#: indico_vc_zoom/forms.py:43
msgid "Show Phone Access numbers"
msgstr "Afficher les numéros d'accès téléphonique"
#: indico_vc_zoom/forms.py:45
msgid "Show a link to the list of phone access numbers"
msgstr "Afficher un lien vers la liste des numéros d'accès téléphonique"
#: indico_vc_zoom/forms.py:58 indico_vc_zoom/templates/info_box.html:7
#: indico_vc_zoom/templates/manage_event_info_box.html:6
msgid "Description"
msgstr "Description"
#: indico_vc_zoom/forms.py:58
msgid "The description of the room"
msgstr "La description de la salle"
#: indico_vc_zoom/forms.py:59 indico_vc_zoom/templates/info_box.html:14
#: indico_vc_zoom/templates/manage_event_info_box.html:10
msgid "Owner"
msgstr "Responsable"
#: indico_vc_zoom/forms.py:59
msgid "The owner of the room"
msgstr "Le responsable de la salle"
#: indico_vc_zoom/forms.py:60
#: indico_vc_zoom/templates/manage_event_info_box.html:39
msgid "Moderation PIN"
msgstr "Code confidentiel de modération"
#: indico_vc_zoom/forms.py:61
msgid "Used to moderate the VC Room. Only digits allowed."
msgstr "Utilisé pour modérer la salle de VC. Seuls les chiffres sont autorisés."
#: indico_vc_zoom/forms.py:62 indico_vc_zoom/templates/info_box.html:18
#: indico_vc_zoom/templates/manage_event_info_box.html:32
msgid "Room PIN"
msgstr "Code confidentiel de la salle"
#: indico_vc_zoom/forms.py:63
msgid ""
"Used to protect the access to the VC Room (leave blank for open access). "
"Only digits allowed."
msgstr "Utilisé pour protéger l'accès à la salle de VC (laisser vide pour un accès ouvert). Seuls les chiffres sont autorisés."
#: indico_vc_zoom/forms.py:65
msgid "Auto mute"
msgstr "Coupure automatique des périphériques d'entrée"
#: indico_vc_zoom/forms.py:66
msgid "On"
msgstr "Activé"
#: indico_vc_zoom/forms.py:66
msgid "Off"
msgstr "Désactivé"
#: indico_vc_zoom/forms.py:67
msgid ""
"The VidyoDesktop clients will join the VC room muted by default (audio and "
"video)"
msgstr "Les clients VidyoDesktop rejoindront la salle Vidyo avec le micro et la caméra coupés par défaut"
#: indico_vc_zoom/forms.py:82
msgid "Unable to find this user in Indico."
msgstr "Impossible de trouver cet utilisateur dans Indico."
#: indico_vc_zoom/forms.py:84
msgid "This user does not have a suitable account to use Vidyo."
msgstr "Cet utilisateur n'a pas de compte qui lui permet d'utiliser Vidyo."
#: indico_vc_zoom/plugin.py:49
msgid "Vidyo email support"
msgstr "Adresse électronique de l'assistance Vidyo"
#: indico_vc_zoom/plugin.py:50
msgid "Username"
msgstr "Identifiant"
#: indico_vc_zoom/plugin.py:50
msgid "Indico username for Vidyo"
msgstr "Identifiant Indico pour Vidyo"
#: indico_vc_zoom/plugin.py:51
msgid "Password"
msgstr "Mot de passe"
#: indico_vc_zoom/plugin.py:52
msgid "Indico password for Vidyo"
msgstr "Mot de passe utilisateur pour Vidyo"
#: indico_vc_zoom/plugin.py:53
msgid "Admin API WSDL URL"
msgstr "URL WSDL pour l'API admin"
#: indico_vc_zoom/plugin.py:54
msgid "User API WSDL URL"
msgstr "URL WSDL pour l'API utilisateur"
#: indico_vc_zoom/plugin.py:55
msgid "Indico tenant prefix"
msgstr "Préfixe de tenant pour Indico"
#: indico_vc_zoom/plugin.py:56
msgid "The tenant prefix for Indico rooms created on this server"
msgstr "Le préfixe de tenant pour les salles Vidyo créées sur ce serveur Indico"
#: indico_vc_zoom/plugin.py:57
msgid "Public rooms' group name"
msgstr "Nom du groupe Vidyo pour les salles Vidyo"
#: indico_vc_zoom/plugin.py:58
msgid "Group name for public videoconference rooms created by Indico"
msgstr "Nom du groupe pour les salles publiques de visioconférence créées par Indico"
#: indico_vc_zoom/plugin.py:59
msgid "Authenticators"
msgstr "Services d'authentification"
#: indico_vc_zoom/plugin.py:60
msgid "Identity providers to convert Indico users to Vidyo accounts"
msgstr "Fournisseurs d'identité pour convertir des utilisateurs Indico en comptes Vidyo"
#: indico_vc_zoom/plugin.py:61
msgid "VC room age threshold"
msgstr "Limite d'âge pour les salles Vidyo"
#: indico_vc_zoom/plugin.py:62
msgid ""
"Number of days after an Indico event when a videoconference room is "
"considered old"
msgstr "Nombre de jours à partir de la fin d'un événement dans Indico après lesquels une salle de visioconférence est considérée comme agée"
#: indico_vc_zoom/plugin.py:64
msgid "Max. num. VC rooms before warning"
msgstr "Nombre maximum de salles Vidyo avant un message d'alerte"
#: indico_vc_zoom/plugin.py:65
msgid "Maximum number of rooms until a warning is sent to the managers"
msgstr "Nombre maximum de salles Vidyo créées sur ce serveur avant qu'un message d'alerte soit envoyé aux administrateurs"
#: indico_vc_zoom/plugin.py:66
msgid "VidyoVoice phone number"
msgstr "Numéros de téléphone VidyoVoice"
#: indico_vc_zoom/plugin.py:67
msgid "Link to the list of VidyoVoice phone numbers"
msgstr "Lien vers la liste des numéros de téléphones VidyoVoice"
#: indico_vc_zoom/plugin.py:68
msgid "Client Chooser URL"
msgstr "URL de sélection du client"
#: indico_vc_zoom/plugin.py:69
msgid ""
"URL for client chooser interface. The room key will be passed as a 'url' GET"
" query argument"
msgstr "L'URL pour l'interface de sélection du client. Le code de la salle sera passé comme argument de requête GET."
#: indico_vc_zoom/plugin.py:71
msgid "Creation email footer"
msgstr "Pied de page du courriel de création"
#: indico_vc_zoom/plugin.py:72
msgid "Footer to append to emails sent upon creation of a VC room"
msgstr "Pied de page ajouté au courriel envoyé à la création d'une nouvelle salle Vidyo"
#: indico_vc_zoom/plugin.py:162 indico_vc_zoom/plugin.py:202
#: indico_vc_zoom/plugin.py:240 indico_vc_zoom/plugin.py:269
msgid "No valid Vidyo account found for this user"
msgstr "Pas de compte Vidyo valide pour cet utilisateur"
#: indico_vc_zoom/plugin.py:198 indico_vc_zoom/plugin.py:263
msgid "Room name already in use"
msgstr "Ce nom de salle est déjà utilisé"
#: indico_vc_zoom/plugin.py:213
msgid "Could not find newly created room in Vidyo"
msgstr "Impossible de trouver la nouvelle salle dans Vidyo"
#: indico_vc_zoom/plugin.py:232 indico_vc_zoom/plugin.py:259
#: indico_vc_zoom/plugin.py:288
msgid "This room has been deleted from Vidyo"
msgstr "Cette salle a été supprimée de Vidyo"
#: indico_vc_zoom/templates/buttons.html:7
#, python-format
msgid "You will be the owner of this Vidyo room, replacing %(name)s."
msgstr "Vous deviendrez le responsable de cette salle Vidyo, à la place de %(name)s."
#: indico_vc_zoom/templates/buttons.html:9
msgid "Make me owner"
msgstr "Nommez moi responsable"
#: indico_vc_zoom/templates/buttons.html:19
msgid "Join"
msgstr "Rejoindre"
#: indico_vc_zoom/templates/info_box.html:5
msgid "Name"
msgstr "Nom"
#: indico_vc_zoom/templates/info_box.html:10
#: indico_vc_zoom/templates/manage_event_info_box.html:8
msgid "Extension"
msgstr "Extension"
#: indico_vc_zoom/templates/info_box.html:22
#: indico_vc_zoom/templates/manage_event_info_box.html:45
msgid "Auto-join URL"
msgstr "URL pour connexion à la salle"
#: indico_vc_zoom/templates/info_box.html:29
msgid "Useful links"
msgstr "Liens utiles"
#: indico_vc_zoom/templates/info_box.html:33
msgid "Phone numbers"
msgstr "Numéros de téléphone"
#: indico_vc_zoom/templates/manage_event_info_box.html:18
msgid "Linked to"
msgstr "attachée à"
#: indico_vc_zoom/templates/manage_event_info_box.html:24
msgid "the whole event"
msgstr "l'événement entier"
#: indico_vc_zoom/templates/manage_event_info_box.html:26
msgid "Contribution"
msgstr "La contribution"
#: indico_vc_zoom/templates/manage_event_info_box.html:28
msgid "Session"
msgstr "La session"
#: indico_vc_zoom/templates/manage_event_info_box.html:49
msgid "Created on"
msgstr "Créée le "
#: indico_vc_zoom/templates/manage_event_info_box.html:52
msgid "Modified on"
msgstr "Modifiée le "

View File

@ -0,0 +1,23 @@
from __future__ import unicode_literals
from indico.core.db import db
from indico.modules.users.models.emails import UserEmail
from indico.modules.users.models.users import User
def find_enterprise_email(user):
"""Find a user's first e-mail address which can be used by the Zoom API.
:param user: the `User` in question
:return: the e-mail address if it exists, otherwise `None`
"""
from indico_vc_zoom.plugin import ZoomPlugin
providers = [auth.strip() for auth in ZoomPlugin.settings.get('email_domains').split(',')]
result = UserEmail.query.filter(
UserEmail.user == user,
~User.is_blocked,
~User.is_deleted,
db.or_(UserEmail.email.ilike("%%@{}".format(provider)) for provider in providers)
).join(User).first()
return result.email if result else None

7
vc_zoom/pytest.ini Normal file
View File

@ -0,0 +1,7 @@
[pytest]
; more verbose summary (include skip/fail/error/warning), coverage
addopts = -rsfEw --cov . --cov-report html --no-cov-on-fail
; only check for tests in suffixed files
python_files = *_test.py
; we need the livesync plugin to be loaded
indico_plugins = vc_zoom

2
vc_zoom/setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[pydocstyle]
ignore = D100,D101,D102,D103,D104,D105,D107,D203,D213

28
vc_zoom/setup.py Normal file
View File

@ -0,0 +1,28 @@
from __future__ import unicode_literals
from setuptools import find_packages, setup
setup(
name='indico-plugin-vc-zoom',
version='0.3-dev',
description='Zoom video-conferencing plugin for Indico',
url='',
license='MIT',
author='Giovanni Mariano - ENEA',
author_email='giovanni.mariano@enea.it',
packages=find_packages(),
zip_safe=False,
include_package_data=True,
install_requires=[
'indico>=2',
'PyJWT'
],
classifiers=[
'Environment :: Plugins',
'Environment :: Web Environment',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.7'
],
entry_points={'indico.plugins': {'vc_zoom = indico_vc_zoom.plugin:ZoomPlugin'}}
)

View File

@ -0,0 +1,22 @@
from datetime import datetime
import pytest
from pytz import utc
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomStatus
@pytest.fixture
def create_dummy_room(db, dummy_user):
"""Returns a callable which lets you create dummy Zoom room occurrences"""
pass
def test_room_cleanup(create_event, create_dummy_room, freeze_time, db):
"""Test that 'old' Zoom rooms are correctly detected"""
freeze_time(datetime(2015, 2, 1))
pass
assert {r.id for r in find_old_zoom_rooms(180)} == {2, 3, 5}

View File

@ -0,0 +1,5 @@
{
"entry": {
"main": "./index.js"
}
}