mirror of
https://github.com/lucaspalomodevelop/indico-plugins.git
synced 2026-03-20 10:36:13 +00:00
VC/Zoom: Initial Commit
This commit is contained in:
commit
dc91b69c0b
84
.gitignore
vendored
Executable file
84
.gitignore
vendored
Executable file
@ -0,0 +1,84 @@
|
||||
.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
|
||||
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal 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
README.md
Normal file
7
README.md
Normal 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
|
||||
12
indico_vc_zoom/__init__.py
Normal file
12
indico_vc_zoom/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from indico.core import signals
|
||||
from indico.util.i18n import make_bound_gettext
|
||||
|
||||
|
||||
_ = make_bound_gettext('vc_zoom')
|
||||
|
||||
|
||||
@signals.import_tasks.connect
|
||||
def _import_tasks(sender, **kwargs):
|
||||
import indico_vc_zoom.task
|
||||
7
indico_vc_zoom/api/__init__.py
Normal file
7
indico_vc_zoom/api/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
|
||||
from .client import ZoomIndicoClient, APIException, RoomNotFoundAPIException, ZoomClient
|
||||
|
||||
|
||||
__all__ = ['ZoomIndicoClient', 'APIException', 'RoomNotFoundAPIException', 'ZoomClient']
|
||||
487
indico_vc_zoom/api/client.py
Normal file
487
indico_vc_zoom/api/client.py
Normal file
@ -0,0 +1,487 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from requests import Session
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import jwt
|
||||
|
||||
|
||||
class ApiClient(object):
|
||||
"""Simple wrapper for REST API requests"""
|
||||
|
||||
def __init__(self, base_uri=None, timeout=15, **kwargs):
|
||||
"""Setup a new API Client
|
||||
|
||||
:param base_uri: The base URI to the API
|
||||
:param timeout: The timeout to use for requests
|
||||
:param kwargs: Any other attributes. These will be added as
|
||||
attributes to the ApiClient object.
|
||||
"""
|
||||
self.base_uri = base_uri
|
||||
self.timeout = timeout
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
"""The timeout"""
|
||||
return self._timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, value):
|
||||
"""The default timeout"""
|
||||
if value is not None:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValueError("timeout value must be an integer")
|
||||
self._timeout = value
|
||||
|
||||
@property
|
||||
def base_uri(self):
|
||||
"""The base_uri"""
|
||||
return self._base_uri
|
||||
|
||||
@base_uri.setter
|
||||
def base_uri(self, value):
|
||||
"""The default base_uri"""
|
||||
if value and value.endswith("/"):
|
||||
value = value[:-1]
|
||||
self._base_uri = value
|
||||
|
||||
def url_for(self, endpoint):
|
||||
"""Get the URL for the given endpoint
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:return: The full URL for the endpoint
|
||||
"""
|
||||
if not endpoint.startswith("/"):
|
||||
endpoint = "/{}".format(endpoint)
|
||||
if endpoint.endswith("/"):
|
||||
endpoint = endpoint[:-1]
|
||||
return self.base_uri + endpoint
|
||||
|
||||
def get_request(self, endpoint, params=None, headers=None):
|
||||
"""Helper function for GET requests
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:param params: The URL parameters
|
||||
:param headers: request headers
|
||||
:return: The :class:``requests.Response`` object for this request
|
||||
"""
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
|
||||
return requests.get(
|
||||
self.url_for(endpoint), params=params, headers=headers, timeout=self.timeout
|
||||
)
|
||||
|
||||
def post_request(
|
||||
self, endpoint, params=None, data=None, headers=None, cookies=None
|
||||
):
|
||||
"""Helper function for POST requests
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:param params: The URL parameters
|
||||
:param data: The data (either as a dict or dumped JSON string) to
|
||||
include with the POST
|
||||
:param headers: request headers
|
||||
:param cookies: request cookies
|
||||
:return: The :class:``requests.Response`` object for this request
|
||||
"""
|
||||
if data and not is_str_type(data):
|
||||
data = json.dumps(data)
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
|
||||
return requests.post(
|
||||
self.url_for(endpoint),
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
def patch_request(
|
||||
self, endpoint, params=None, data=None, headers=None, cookies=None
|
||||
):
|
||||
"""Helper function for PATCH requests
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:param params: The URL parameters
|
||||
:param data: The data (either as a dict or dumped JSON string) to
|
||||
include with the PATCH
|
||||
:param headers: request headers
|
||||
:param cookies: request cookies
|
||||
:return: The :class:``requests.Response`` object for this request
|
||||
"""
|
||||
if data and not is_str_type(data):
|
||||
data = json.dumps(data)
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
|
||||
return requests.patch(
|
||||
self.url_for(endpoint),
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
def delete_request(
|
||||
self, endpoint, params=None, data=None, headers=None, cookies=None
|
||||
):
|
||||
"""Helper function for DELETE requests
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:param params: The URL parameters
|
||||
:param data: The data (either as a dict or dumped JSON string) to
|
||||
include with the DELETE
|
||||
:param headers: request headers
|
||||
:param cookies: request cookies
|
||||
:return: The :class:``requests.Response`` object for this request
|
||||
"""
|
||||
if data and not is_str_type(data):
|
||||
data = json.dumps(data)
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
|
||||
return requests.delete(
|
||||
self.url_for(endpoint),
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
def put_request(self, endpoint, params=None, data=None, headers=None, cookies=None):
|
||||
"""Helper function for PUT requests
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:param params: The URL paramaters
|
||||
:param data: The data (either as a dict or dumped JSON string) to
|
||||
include with the PUT
|
||||
:param headers: request headers
|
||||
:param cookies: request cookies
|
||||
:return: The :class:``requests.Response`` object for this request
|
||||
"""
|
||||
if data and not is_str_type(data):
|
||||
data = json.dumps(data)
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
|
||||
return requests.put(
|
||||
self.url_for(endpoint),
|
||||
params=params,
|
||||
data=data,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ignored(*exceptions):
|
||||
"""Simple context manager to ignore expected Exceptions
|
||||
|
||||
:param \*exceptions: The exceptions to safely ignore
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
except exceptions:
|
||||
pass
|
||||
|
||||
|
||||
def is_str_type(val):
|
||||
"""Check whether the input is of a string type.
|
||||
|
||||
We use this method to ensure python 2-3 capatibility.
|
||||
|
||||
:param val: The value to check wither it is a string
|
||||
:return: In python2 it will return ``True`` if :attr:`val` is either an
|
||||
instance of str or unicode. In python3 it will return ``True`` if
|
||||
it is an instance of str
|
||||
"""
|
||||
with ignored(NameError):
|
||||
return isinstance(val, basestring)
|
||||
return isinstance(val, str)
|
||||
|
||||
|
||||
def require_keys(d, keys, allow_none=True):
|
||||
"""Require that the object have the given keys
|
||||
|
||||
:param d: The dict the check
|
||||
:param keys: The keys to check :attr:`obj` for. This can either be a single
|
||||
string, or an iterable of strings
|
||||
|
||||
:param allow_none: Whether ``None`` values are allowed
|
||||
:raises:
|
||||
:ValueError: If any of the keys are missing from the obj
|
||||
"""
|
||||
if is_str_type(keys):
|
||||
keys = [keys]
|
||||
for k in keys:
|
||||
if k not in d:
|
||||
raise ValueError("'{}' must be set".format(k))
|
||||
if not allow_none and d[k] is None:
|
||||
raise ValueError("'{}' cannot be None".format(k))
|
||||
return True
|
||||
|
||||
|
||||
def date_to_str_gmt(d):
|
||||
"""Convert date and datetime objects to a string
|
||||
|
||||
Note, this does not do any timezone conversion.
|
||||
|
||||
:param d: The :class:`datetime.date` or :class:`datetime.datetime` to
|
||||
convert to a string
|
||||
:returns: The string representation of the date
|
||||
"""
|
||||
return d.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
def date_to_str_local(d):
|
||||
"""Convert date and datetime objects to a string
|
||||
|
||||
Note, this does not do any timezone conversion.
|
||||
|
||||
:param d: The :class:`datetime.date` or :class:`datetime.datetime` to
|
||||
convert to a string
|
||||
:returns: The string representation of the date
|
||||
"""
|
||||
return d.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
|
||||
def generate_jwt(key, secret):
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
|
||||
payload = {"iss": key, "exp": int(time.time() + 3600)}
|
||||
|
||||
token = jwt.encode(payload, secret, algorithm="HS256", headers=header)
|
||||
return token.decode("utf-8")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class APIException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RoomNotFoundAPIException(APIException):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseComponent(ApiClient):
|
||||
"""A base component"""
|
||||
|
||||
def __init__(self, base_uri=None, config=None, timeout=15, **kwargs):
|
||||
"""Setup a base component
|
||||
|
||||
:param base_uri: The base URI to the API
|
||||
:param config: The config details
|
||||
:param timeout: The timeout to use for requests
|
||||
:param kwargs: Any other attributes. These will be added as
|
||||
attributes to the ApiClient object.
|
||||
"""
|
||||
super(BaseComponent, self).__init__(
|
||||
base_uri=base_uri, timeout=timeout, config=config, **kwargs
|
||||
)
|
||||
|
||||
def post_request(
|
||||
self, endpoint, params=None, data=None, headers=None, cookies=None
|
||||
):
|
||||
"""Helper function for POST requests
|
||||
|
||||
Since the Zoom.us API only uses POST requests and each post request
|
||||
must include all of the config data, this method ensures that all
|
||||
of that data is there
|
||||
|
||||
:param endpoint: The endpoint
|
||||
:param params: The URL parameters
|
||||
:param data: The data (either as a dict or dumped JSON string) to
|
||||
include with the POST
|
||||
:param headers: request headers
|
||||
:param cookies: request cookies
|
||||
:return: The :class:``requests.Response`` object for this request
|
||||
"""
|
||||
params = params or {}
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.config.get("token"))}
|
||||
return super(BaseComponent, self).post_request(
|
||||
endpoint, params=params, data=data, headers=headers, cookies=cookies
|
||||
)
|
||||
|
||||
class MeetingComponent(BaseComponent):
|
||||
def list(self, **kwargs):
|
||||
require_keys(kwargs, "user_id")
|
||||
return self.get_request(
|
||||
"/users/{}/meetings".format(kwargs.get("user_id")), params=kwargs
|
||||
)
|
||||
|
||||
def create(self, **kwargs):
|
||||
require_keys(kwargs, "user_id")
|
||||
if kwargs.get("start_time"):
|
||||
if kwargs.get("timezone"):
|
||||
kwargs["start_time"] = date_to_str_local(kwargs["start_time"])
|
||||
else:
|
||||
kwargs["start_time"] = date_to_str_gmt(kwargs["start_time"])
|
||||
return self.post_request(
|
||||
"/users/{}/meetings".format(kwargs.get("user_id")), params=kwargs['user_id'], data={x: kwargs[x] for x in kwargs.keys() if x not in ['user_id']}
|
||||
)
|
||||
|
||||
def get(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
return self.get_request("/meetings/{}".format(kwargs.get("id")), params=kwargs)
|
||||
|
||||
def update(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
if kwargs.get("start_time"):
|
||||
if kwargs.get("timezone"):
|
||||
kwargs["start_time"] = date_to_str_local(kwargs["start_time"])
|
||||
else:
|
||||
kwargs["start_time"] = date_to_str_gmt(kwargs["start_time"])
|
||||
return self.patch_request(
|
||||
"/meetings/{}".format(kwargs.get("id")), params=kwargs
|
||||
)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
return self.delete_request(
|
||||
"/meetings/{}".format(kwargs.get("id")), params=kwargs
|
||||
)
|
||||
|
||||
def get_invitation(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
return self.get_request("/meetings/{}/invitation".format(kwargs.get("id")), params=kwargs)
|
||||
|
||||
|
||||
class UserComponent(BaseComponent):
|
||||
def list(self, **kwargs):
|
||||
return self.get_request("/users", params=kwargs)
|
||||
|
||||
def create(self, **kwargs):
|
||||
return self.post_request("/users", params=kwargs)
|
||||
|
||||
def update(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
return self.patch_request("/users/{}".format(kwargs.get("id")), params=kwargs)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
return self.delete_request("/users/{}".format(kwargs.get("id")), params=kwargs)
|
||||
|
||||
def get(self, **kwargs):
|
||||
require_keys(kwargs, "id")
|
||||
return self.get_request("/users/{}".format(kwargs.get("id")), params=kwargs)
|
||||
|
||||
|
||||
|
||||
class ZoomClient(ApiClient):
|
||||
"""Zoom.us REST API Python Client"""
|
||||
|
||||
"""Base URL for Zoom API"""
|
||||
|
||||
def __init__(
|
||||
self, api_key, api_secret, data_type="json", timeout=15
|
||||
):
|
||||
"""Create a new Zoom client
|
||||
|
||||
:param api_key: The Zooom.us API key
|
||||
:param api_secret: The Zoom.us API secret
|
||||
:param data_type: The expected return data type. Either 'json' or 'xml'
|
||||
:param timeout: The time out to use for API requests
|
||||
"""
|
||||
|
||||
BASE_URI = "https://api.zoom.us/v2"
|
||||
self.components = {"user": UserComponent,
|
||||
"meeting": MeetingComponent}
|
||||
|
||||
super(ZoomClient, self).__init__(base_uri=BASE_URI, timeout=timeout)
|
||||
|
||||
# Setup the config details
|
||||
self.config = {
|
||||
"api_key": api_key,
|
||||
"api_secret": api_secret,
|
||||
"data_type": data_type,
|
||||
"token": generate_jwt(api_key, api_secret),
|
||||
}
|
||||
|
||||
# Instantiate the components
|
||||
for key in self.components.keys():
|
||||
self.components[key] = self.components[key](
|
||||
base_uri=BASE_URI, config=self.config
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
return
|
||||
|
||||
def refresh_token(self):
|
||||
self.config["token"] = (
|
||||
generate_jwt(self.config["api_key"], self.config["api_secret"]),
|
||||
)
|
||||
|
||||
@property
|
||||
def api_key(self):
|
||||
"""The Zoom.us api_key"""
|
||||
return self.config.get("api_key")
|
||||
|
||||
@api_key.setter
|
||||
def api_key(self, value):
|
||||
"""Set the api_key"""
|
||||
self.config["api_key"] = value
|
||||
self.refresh_token()
|
||||
|
||||
@property
|
||||
def api_secret(self):
|
||||
"""The Zoom.us api_secret"""
|
||||
return self.config.get("api_secret")
|
||||
|
||||
@api_secret.setter
|
||||
def api_secret(self, value):
|
||||
"""Set the api_secret"""
|
||||
self.config["api_secret"] = value
|
||||
self.refresh_token()
|
||||
|
||||
@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")
|
||||
|
||||
|
||||
|
||||
class ZoomIndicoClient(object):
|
||||
def __init__(self, settings):
|
||||
api_key=settings.get('api_key')
|
||||
api_secret=settings.get('api_secret')
|
||||
self.client=ZoomClient(api_key, api_secret)
|
||||
|
||||
def create_meeting(self, **kwargs):
|
||||
return json.loads(self.client.meeting.create(**kwargs).content)
|
||||
|
||||
|
||||
def get_meeting(self, zoom_id):
|
||||
return json.loads(self.client.meeting.get(id=zoom_id).content)
|
||||
|
||||
|
||||
def get_meeting_invitation(self, zoom_id):
|
||||
return json.loads(self.client.meeting.get_invitation(id=zoom_id).content)
|
||||
|
||||
def delete_meeting(self, zoom_id):
|
||||
self.client.meeting.delete(id=zoom_id)
|
||||
|
||||
def check_user_meeting_time(self, userID, start_dt, end_dt):
|
||||
pass
|
||||
|
||||
|
||||
16
indico_vc_zoom/blueprint.py
Normal file
16
indico_vc_zoom/blueprint.py
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from indico.core.plugins import IndicoPluginBlueprint
|
||||
|
||||
from indico_vc_zoom.controllers import RHZoomRoomOwner
|
||||
|
||||
|
||||
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/<any(zoom):service>/<int:event_vc_room_id>/room-owner',
|
||||
'set_room_owner', RHZoomRoomOwner, methods=('POST',))
|
||||
35
indico_vc_zoom/cli.py
Normal file
35
indico_vc_zoom/cli.py
Normal file
@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
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', 'Meeting']]
|
||||
|
||||
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']), unicode(room.zoom_meeting.meeting)])
|
||||
|
||||
table = AsciiTable(table_data)
|
||||
for col in (0, 3, 4):
|
||||
table.justify_columns[col] = 'right'
|
||||
print table.table
|
||||
34
indico_vc_zoom/client/index.js
Normal file
34
indico_vc_zoom/client/index.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
26
indico_vc_zoom/controllers.py
Normal file
26
indico_vc_zoom/controllers.py
Normal 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 RHZoomRoomOwner(RHVCSystemEventBase):
|
||||
def _process(self):
|
||||
result = {}
|
||||
self.vc_room.zoom_meeting.owned_by_user = session.user
|
||||
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 owner of the room '{room.name}'".format(room=self.vc_room)), 'success')
|
||||
result['success'] = True
|
||||
return jsonify(result)
|
||||
67
indico_vc_zoom/forms.py
Normal file
67
indico_vc_zoom/forms.py
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from flask import session
|
||||
|
||||
from wtforms.fields.core import BooleanField
|
||||
from wtforms.fields.simple import TextAreaField, HiddenField
|
||||
from wtforms.validators import DataRequired, Length, Optional, Regexp, ValidationError
|
||||
|
||||
from indico.modules.vc.forms import VCRoomAttachFormBase, VCRoomFormBase
|
||||
from indico.web.forms.base import generated_data
|
||||
from indico.web.forms.fields import IndicoPasswordField, PrincipalField
|
||||
from indico.web.forms.widgets import SwitchWidget
|
||||
|
||||
from indico_vc_zoom import _
|
||||
from indico_vc_zoom.util import iter_user_identities, retrieve_principal
|
||||
|
||||
|
||||
PIN_VALIDATORS = [Optional(), Length(min=3, max=10), Regexp(r'^\d+$', message=_("The PIN must be a number"))]
|
||||
|
||||
|
||||
class ZoomAdvancedFormMixin(object):
|
||||
# Advanced options (per event)
|
||||
|
||||
show_autojoin = BooleanField(_('Show Auto-join URL'),
|
||||
widget=SwitchWidget(),
|
||||
description=_("Show the auto-join URL on the event page"))
|
||||
show_phone_numbers = BooleanField(_('Show Phone Access numbers'),
|
||||
widget=SwitchWidget(),
|
||||
description=_("Show a link to the list of phone access numbers"))
|
||||
|
||||
|
||||
class VCRoomAttachForm(VCRoomAttachFormBase, ZoomAdvancedFormMixin):
|
||||
pass
|
||||
|
||||
|
||||
class VCRoomForm(VCRoomFormBase, ZoomAdvancedFormMixin):
|
||||
"""Contains all information concerning a Zoom booking"""
|
||||
|
||||
advanced_fields = {'show_autojoin', 'show_phone_numbers'} | VCRoomFormBase.advanced_fields
|
||||
skip_fields = advanced_fields | VCRoomFormBase.conditional_fields
|
||||
|
||||
description = TextAreaField(_('Description'), [DataRequired()], description=_('The description of the room'))
|
||||
#owner_user = PrincipalField(_('Owner'), [DataRequired()], description=_('The owner of the room'))
|
||||
#owner_user = HiddenField(default=session.user)
|
||||
#moderation_pin = IndicoPasswordField(_('Moderation PIN'), PIN_VALIDATORS, toggle=True,
|
||||
# description=_('Used to moderate the VC Room. Only digits allowed.'))
|
||||
#room_pin = IndicoPasswordField(_('Room PIN'), PIN_VALIDATORS, toggle=True,
|
||||
# description=_('Used to protect the access to the VC Room (leave blank for open '
|
||||
# 'access). Only digits allowed.'))
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
defaults = kwargs['obj']
|
||||
if defaults.owner_user is None and defaults.owner is not None:
|
||||
defaults.owner_user = retrieve_principal(defaults.owner)
|
||||
super(VCRoomForm, self).__init__(*args, **kwargs)
|
||||
|
||||
#@generated_data
|
||||
#def owner(self):
|
||||
# return self.owner_user.data.default
|
||||
|
||||
def validate_owner_user(self, field):
|
||||
if not field.data:
|
||||
raise ValidationError(_("Unable to find this user in Indico."))
|
||||
#if not next(iter_user_identities(field.data), None):
|
||||
# raise ValidationError(_("This user does not have a suitable account to use Zoom."))
|
||||
51
indico_vc_zoom/http_api.py
Normal file
51
indico_vc_zoom/http_api.py
Normal 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}
|
||||
@ -0,0 +1,52 @@
|
||||
"""crea tabella
|
||||
|
||||
Revision ID: b79318836818
|
||||
Revises:
|
||||
Create Date: 2020-03-12 10:30:09.256157
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from sqlalchemy.sql.ddl import CreateSchema, DropSchema
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b79318836818'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(CreateSchema('plugin_vc_zoom'))
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('zoom_extensions',
|
||||
sa.Column('vc_room_id', sa.Integer(), nullable=False),
|
||||
sa.Column('url_zoom', sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=op.f('fk_zoom_extensions_vc_room_id_vc_rooms')),
|
||||
sa.PrimaryKeyConstraint('vc_room_id', name=op.f('pk_zoom_extensions')),
|
||||
schema='plugin_vc_zoom'
|
||||
)
|
||||
op.create_index(op.f('ix_zoom_extensions_url_zoom'), 'zoom_extensions', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
|
||||
op.create_table('zoom_licenses',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('license_id', sa.Text(), nullable=False),
|
||||
sa.Column('license_name', sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_zoom_licenses')),
|
||||
schema='plugin_vc_zoom'
|
||||
)
|
||||
op.create_index(op.f('ix_uq_zoom_licenses_license_id'), 'zoom_licenses', ['license_id'], unique=True, schema='plugin_vc_zoom')
|
||||
op.create_index(op.f('ix_zoom_licenses_license_name'), 'zoom_licenses', ['license_name'], unique=False, schema='plugin_vc_zoom')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_zoom_licenses_license_name'), table_name='zoom_licenses', schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_uq_zoom_licenses_license_id'), table_name='zoom_licenses', schema='plugin_vc_zoom')
|
||||
op.drop_table('zoom_licenses', schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_zoom_extensions_url_zoom'), table_name='zoom_extensions', schema='plugin_vc_zoom')
|
||||
op.drop_table('zoom_extensions', schema='plugin_vc_zoom')
|
||||
# ### end Alembic commands ###
|
||||
op.execute(DropSchema('plugin_vc_zoom'))
|
||||
@ -0,0 +1,66 @@
|
||||
"""create table
|
||||
|
||||
Revision ID: d86e85a383c1
|
||||
Revises: b79318836818
|
||||
Create Date: 2020-04-03 19:57:35.771416
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd86e85a383c1'
|
||||
down_revision = 'b79318836818'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('zoom_meetings',
|
||||
sa.Column('vc_room_id', sa.Integer(), nullable=False),
|
||||
sa.Column('meeting', sa.BigInteger(), nullable=True),
|
||||
sa.Column('url_zoom', sa.Text(), nullable=False),
|
||||
sa.Column('owned_by_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owned_by_id'], [u'users.users.id'], name=op.f('fk_zoom_meetings_owned_by_id_users')),
|
||||
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=op.f('fk_zoom_meetings_vc_room_id_vc_rooms')),
|
||||
sa.PrimaryKeyConstraint('vc_room_id', name=op.f('pk_zoom_meetings')),
|
||||
schema='plugin_vc_zoom'
|
||||
)
|
||||
op.create_index(op.f('ix_zoom_meetings_meeting'), 'zoom_meetings', ['meeting'], unique=False, schema='plugin_vc_zoom')
|
||||
op.create_index(op.f('ix_zoom_meetings_owned_by_id'), 'zoom_meetings', ['owned_by_id'], unique=False, schema='plugin_vc_zoom')
|
||||
op.create_index(op.f('ix_zoom_meetings_url_zoom'), 'zoom_meetings', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
|
||||
op.drop_index('ix_zoom_extensions_url_zoom', table_name='zoom_extensions', schema='plugin_vc_zoom')
|
||||
op.drop_table('zoom_extensions', schema='plugin_vc_zoom')
|
||||
op.drop_index('ix_uq_zoom_licenses_license_id', table_name='zoom_licenses', schema='plugin_vc_zoom')
|
||||
op.drop_index('ix_zoom_licenses_license_name', table_name='zoom_licenses', schema='plugin_vc_zoom')
|
||||
op.drop_table('zoom_licenses', schema='plugin_vc_zoom')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('zoom_licenses',
|
||||
sa.Column('id', sa.INTEGER(), server_default=sa.text(u"nextval('plugin_vc_zoom.zoom_licenses_id_seq'::regclass)"), autoincrement=True, nullable=False),
|
||||
sa.Column('license_id', sa.TEXT(), autoincrement=False, nullable=False),
|
||||
sa.Column('license_name', sa.TEXT(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=u'pk_zoom_licenses'),
|
||||
schema='plugin_vc_zoom'
|
||||
)
|
||||
op.create_index('ix_zoom_licenses_license_name', 'zoom_licenses', ['license_name'], unique=False, schema='plugin_vc_zoom')
|
||||
op.create_index('ix_uq_zoom_licenses_license_id', 'zoom_licenses', ['license_id'], unique=True, schema='plugin_vc_zoom')
|
||||
op.create_table('zoom_extensions',
|
||||
sa.Column('vc_room_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
||||
sa.Column('url_zoom', sa.TEXT(), autoincrement=False, nullable=False),
|
||||
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=u'fk_zoom_extensions_vc_room_id_vc_rooms'),
|
||||
sa.PrimaryKeyConstraint('vc_room_id', name=u'pk_zoom_extensions'),
|
||||
schema='plugin_vc_zoom'
|
||||
)
|
||||
op.create_index('ix_zoom_extensions_url_zoom', 'zoom_extensions', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_zoom_meetings_url_zoom'), table_name='zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_zoom_meetings_owned_by_id'), table_name='zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_zoom_meetings_meeting'), table_name='zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.drop_table('zoom_meetings', schema='plugin_vc_zoom')
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,46 @@
|
||||
"""create table
|
||||
|
||||
Revision ID: d54585a383v1
|
||||
Revises: b79318836818
|
||||
Create Date: 2020-04-05 15:39:35.771416
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from sqlalchemy.sql.ddl import CreateSchema, DropSchema
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd54585a383v1'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.execute(CreateSchema('plugin_vc_zoom'))
|
||||
op.create_table('zoom_meetings',
|
||||
sa.Column('vc_room_id', sa.Integer(), nullable=False),
|
||||
sa.Column('meeting', sa.BigInteger(), nullable=True),
|
||||
sa.Column('url_zoom', sa.Text(), nullable=False),
|
||||
sa.Column('owned_by_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['owned_by_id'], [u'users.users.id'], name=op.f('fk_zoom_meetings_owned_by_id_users')),
|
||||
sa.ForeignKeyConstraint(['vc_room_id'], [u'events.vc_rooms.id'], name=op.f('fk_zoom_meetings_vc_room_id_vc_rooms')),
|
||||
sa.PrimaryKeyConstraint('vc_room_id', name=op.f('pk_zoom_meetings')),
|
||||
schema='plugin_vc_zoom'
|
||||
)
|
||||
op.create_index(op.f('ix_zoom_meetings_meeting'), 'zoom_meetings', ['meeting'], unique=False, schema='plugin_vc_zoom')
|
||||
op.create_index(op.f('ix_zoom_meetings_owned_by_id'), 'zoom_meetings', ['owned_by_id'], unique=False, schema='plugin_vc_zoom')
|
||||
op.create_index(op.f('ix_zoom_meetings_url_zoom'), 'zoom_meetings', ['url_zoom'], unique=False, schema='plugin_vc_zoom')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_zoom_meetings_url_zoom'), table_name='zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_zoom_meetings_owned_by_id'), table_name='zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.drop_index(op.f('ix_zoom_meetings_meeting'), table_name='zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.drop_table('zoom_meetings', schema='plugin_vc_zoom')
|
||||
op.execute(DropSchema('plugin_vc_zoom'))
|
||||
# ### end Alembic commands ###
|
||||
0
indico_vc_zoom/models/__init__.py
Normal file
0
indico_vc_zoom/models/__init__.py
Normal file
75
indico_vc_zoom/models/zoom_meetings.py
Normal file
75
indico_vc_zoom/models/zoom_meetings.py
Normal file
@ -0,0 +1,75 @@
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import urllib
|
||||
|
||||
from sqlalchemy.event import listens_for
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
|
||||
from indico.core.db.sqlalchemy import db
|
||||
from indico.util.string import return_ascii
|
||||
|
||||
|
||||
class ZoomMeeting(db.Model):
|
||||
__tablename__ = 'zoom_meetings'
|
||||
__table_args__ = {'schema': 'plugin_vc_zoom'}
|
||||
|
||||
#: ID of the videoconference room
|
||||
vc_room_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('events.vc_rooms.id'),
|
||||
primary_key=True
|
||||
)
|
||||
meeting = db.Column(
|
||||
db.BigInteger,
|
||||
index=True
|
||||
)
|
||||
url_zoom = db.Column(
|
||||
db.Text,
|
||||
index=True,
|
||||
nullable=False
|
||||
)
|
||||
owned_by_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('users.users.id'),
|
||||
index=True,
|
||||
nullable=False
|
||||
)
|
||||
vc_room = db.relationship(
|
||||
'VCRoom',
|
||||
lazy=False,
|
||||
backref=db.backref(
|
||||
'zoom_meeting',
|
||||
cascade='all, delete-orphan',
|
||||
uselist=False,
|
||||
lazy=False
|
||||
)
|
||||
)
|
||||
|
||||
#: The user who owns the Zoom room
|
||||
owned_by_user = db.relationship(
|
||||
'User',
|
||||
lazy=True,
|
||||
backref=db.backref(
|
||||
'vc_rooms_zoom',
|
||||
lazy='dynamic'
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def join_url(self):
|
||||
from indico_vc_zoom.plugin import ZoomPlugin
|
||||
url = self.vc_room.data['url']
|
||||
return url
|
||||
|
||||
@return_ascii
|
||||
def __repr__(self):
|
||||
return '<ZoomMeeting({}, {}, {})>'.format(self.vc_room, self.meeting, self.owned_by_user)
|
||||
|
||||
|
||||
@listens_for(ZoomMeeting.owned_by_user, 'set')
|
||||
def _owned_by_user_set(target, user, *unused):
|
||||
if target.vc_room and user.as_principal != tuple(target.vc_room.data['owner']):
|
||||
target.vc_room.data['owner'] = user.as_principal
|
||||
flag_modified(target.vc_room, 'data')
|
||||
260
indico_vc_zoom/plugin.py
Normal file
260
indico_vc_zoom/plugin.py
Normal file
@ -0,0 +1,260 @@
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from flask import session
|
||||
from flask_pluginengine import current_plugin
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
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, BooleanField
|
||||
|
||||
from indico.web.forms.widgets import SwitchWidget
|
||||
|
||||
from indico.web.flask.templating import get_template_module
|
||||
from wtforms.validators import DataRequired, NumberRange
|
||||
from indico.core.notifications import make_email, send_email
|
||||
from indico.core.plugins import get_plugin_template_module
|
||||
from indico.core import signals
|
||||
from indico.core.auth import multipass
|
||||
from indico.core.config import config
|
||||
from indico.core.plugins import IndicoPlugin, 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.views import WPVCEventPage, WPVCManageEvent
|
||||
from indico.web.forms.fields import IndicoPasswordField
|
||||
from indico.web.forms.widgets import CKEditorWidget
|
||||
from indico.web.http_api.hooks.base import HTTPAPIHook
|
||||
from indico.modules.vc.notifications import _send
|
||||
|
||||
from indico_vc_zoom import _
|
||||
from indico_vc_zoom.api import ZoomIndicoClient, APIException, RoomNotFoundAPIException
|
||||
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.models.zoom_meetings import ZoomMeeting
|
||||
from indico_vc_zoom.util import iter_extensions, iter_user_identities, retrieve_principal, update_room_from_obj
|
||||
|
||||
|
||||
class PluginSettingsForm(VCPluginSettingsFormBase):
|
||||
support_email = EmailField(_('Zoom email support'))
|
||||
|
||||
api_key = StringField(_('API KEY'), [DataRequired()])
|
||||
|
||||
api_secret = StringField(_('API SECRET'), [DataRequired()])
|
||||
|
||||
auto_mute = BooleanField(_('Auto mute'),
|
||||
widget=SwitchWidget(_('On'), _('Off')),
|
||||
description=_('The Zoom clients will join the VC room muted by default '))
|
||||
|
||||
host_video = BooleanField(_('Host Video'),
|
||||
widget=SwitchWidget(_('On'), _('Off')),
|
||||
description=_('Start video when the host joins the meeting.'))
|
||||
|
||||
participant_video = BooleanField(_('Participant Video'),
|
||||
widget=SwitchWidget(_('On'), _('Off')),
|
||||
description=_('Start video when participants join the meeting. '))
|
||||
|
||||
join_before_host = BooleanField(_('Join Before Host'),
|
||||
widget=SwitchWidget(_('On'), _('Off')),
|
||||
description=_('Allow participants to join the meeting before the host starts the meeting. Only used for scheduled or recurring meetings.'))
|
||||
|
||||
#indico_room_prefix = IntegerField(_('Indico tenant prefix'), [NumberRange(min=0)],
|
||||
# description=_('The tenant prefix for Indico rooms created on this server'))
|
||||
#room_group_name = StringField(_("Public rooms' group name"), [DataRequired()],
|
||||
# description=_('Group name for public videoconference rooms created by Indico'))
|
||||
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'))
|
||||
|
||||
|
||||
class ZoomPlugin(VCPluginMixin, IndicoPlugin):
|
||||
"""Zoom
|
||||
|
||||
Videoconferencing with Zoom
|
||||
"""
|
||||
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.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,
|
||||
'api_key': '',
|
||||
'api_secret': '',
|
||||
#'indico_room_prefix': 10,
|
||||
'auto_mute':True,
|
||||
'host_video':False,
|
||||
'participant_video':True,
|
||||
'join_before_host':True,
|
||||
#'room_group_name': 'Indico',
|
||||
# we skip identity providers in the default list if they don't support get_identity.
|
||||
# these providers (local accounts, oauth) are unlikely be the correct ones to integrate
|
||||
# with the zoom infrastructure.
|
||||
'num_days_old': 5,
|
||||
'max_rooms_warning': 5000,
|
||||
'zoom_phone_link': None,
|
||||
'creation_email_footer': None
|
||||
})
|
||||
|
||||
@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 _extend_indico_cli(self, sender, **kwargs):
|
||||
return cli
|
||||
|
||||
def update_data_association(self, event, vc_room, event_vc_room, data):
|
||||
super(ZoomPlugin, self).update_data_association(event, vc_room, event_vc_room, data)
|
||||
|
||||
event_vc_room.data.update({key: data.pop(key) for key in [
|
||||
'show_autojoin',
|
||||
'show_phone_numbers'
|
||||
]})
|
||||
|
||||
flag_modified(event_vc_room, 'data')
|
||||
|
||||
def update_data_vc_room(self, vc_room, data):
|
||||
super(ZoomPlugin, self).update_data_vc_room(vc_room, data)
|
||||
|
||||
for key in ['description', 'owner', 'auto_mute', 'host_video', 'participant_video', 'join_before_host']:
|
||||
if key in data:
|
||||
vc_room.data[key] = data.pop(key)
|
||||
|
||||
flag_modified(vc_room, 'data')
|
||||
|
||||
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 do so with
|
||||
all the available identities of the user based on the authenticators
|
||||
defined in Zoom plugin's settings, in that order.
|
||||
|
||||
:param vc_room: VCRoom -- The VC room from which to create the Zoom
|
||||
room
|
||||
:param event: Event -- The event to the Zoom room will be attached
|
||||
"""
|
||||
client = ZoomIndicoClient(self.settings)
|
||||
#owner = retrieve_principal(vc_room.data['owner'])
|
||||
owner= session.user
|
||||
user_id=owner.email
|
||||
topic=vc_room.name
|
||||
time_zone=event.timezone
|
||||
start=event.start_dt_local
|
||||
end=event.end_dt
|
||||
topic=vc_room.data['description']
|
||||
type_meeting=2
|
||||
host_video=self.settings.get('host_video')
|
||||
participant_video=self.settings.get('participant_video')
|
||||
join_before_host=self.settings.get('join_before_host')
|
||||
mute_upon_entry=self.settings.get('auto_mute')
|
||||
|
||||
|
||||
meeting_obj = client.create_meeting(user_id=user_id,
|
||||
type=type_meeting,
|
||||
start_time=start,
|
||||
topic=topic,
|
||||
timezone=time_zone,
|
||||
host_video=host_video,
|
||||
participant_video=participant_video,
|
||||
join_before_host=join_before_host,
|
||||
mute_upon_entry=mute_upon_entry)
|
||||
|
||||
if not meeting_obj:
|
||||
raise VCRoomNotFoundError(_("Could not find newly created room in Zoom"))
|
||||
vc_room.data.update({
|
||||
'zoom_id': unicode(meeting_obj['id']),
|
||||
'url': meeting_obj['join_url'],
|
||||
'start_url':meeting_obj['start_url']
|
||||
})
|
||||
|
||||
flag_modified(vc_room, 'data')
|
||||
vc_room.zoom_meeting = ZoomMeeting(vc_room_id=vc_room.id, meeting=meeting_obj['id'],
|
||||
owned_by_user=owner, url_zoom=meeting_obj['join_url'])
|
||||
self.notify_owner_start_url(vc_room)
|
||||
|
||||
|
||||
def update_room(self, vc_room, event):
|
||||
pass
|
||||
|
||||
def refresh_room(self, vc_room, event):
|
||||
pass
|
||||
|
||||
def delete_room(self, vc_room, event):
|
||||
client = ZoomIndicoClient(self.settings)
|
||||
zoom_id = vc_room.data['zoom_id']
|
||||
client.delete_meeting(zoom_id)
|
||||
|
||||
|
||||
def get_meeting(self, vc_room):
|
||||
client = ZoomIndicoClient(self.settings)
|
||||
return client.get_meeting(vc_room.data['zoom_id'])
|
||||
|
||||
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({
|
||||
'show_autojoin': True,
|
||||
'show_phone_numbers': True,
|
||||
'owner_user': session.user
|
||||
})
|
||||
|
||||
return defaults
|
||||
|
||||
def get_vc_room_attach_form_defaults(self, event):
|
||||
defaults = super(ZoomPlugin, self).get_vc_room_attach_form_defaults(event)
|
||||
defaults.update({
|
||||
'show_autojoin': True,
|
||||
'show_phone_numbers': True
|
||||
})
|
||||
return defaults
|
||||
|
||||
def can_manage_vc_room(self, user, room):
|
||||
return user == room.zoom_meeting.owned_by_user 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 ext in ZoomMeeting.find(owned_by_user=source):
|
||||
ext.owned_by_user = target
|
||||
flag_modified(ext.vc_room, 'data')
|
||||
|
||||
def get_notification_cc_list(self, action, vc_room, event):
|
||||
return {vc_room.zoom_meeting.owned_by_user.email}
|
||||
|
||||
|
||||
def notify_owner_start_url(self, vc_room):
|
||||
user = vc_room.zoom_meeting.owned_by_user
|
||||
to_list = {user.email}
|
||||
|
||||
template_module = get_template_module('vc_zoom:emails/notify_start_url.html', plugin=ZoomPlugin.instance, vc_room=vc_room, event=None,
|
||||
vc_room_event=None, user=user)
|
||||
|
||||
email = make_email(to_list, template=template_module, html=True)
|
||||
send_email(email, None, 'Zoom')
|
||||
BIN
indico_vc_zoom/static/images/zoom_logo.png
Normal file
BIN
indico_vc_zoom/static/images/zoom_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
70
indico_vc_zoom/task.py
Normal file
70
indico_vc_zoom/task.py
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from celery.schedules import crontab
|
||||
|
||||
from indico.core.celery import celery
|
||||
from indico.core.db import db
|
||||
from indico.core.plugins import get_plugin_template_module
|
||||
from indico.modules.events import Event
|
||||
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomStatus
|
||||
from indico.modules.vc.notifications import _send
|
||||
from indico.util.date_time import now_utc
|
||||
from indico.util.struct.iterables import committing_iterator
|
||||
|
||||
from indico_vc_zoom.api import APIException, RoomNotFoundAPIException
|
||||
|
||||
|
||||
def find_old_zoom_rooms(max_room_event_age):
|
||||
"""Finds all Zoom rooms that are:
|
||||
- linked to no events
|
||||
- linked only to events whose start date precedes today - max_room_event_age days
|
||||
"""
|
||||
recently_used = (db.session.query(VCRoom.id)
|
||||
.filter(VCRoom.type == 'zoom',
|
||||
Event.end_dt > (now_utc() - timedelta(days=max_room_event_age)))
|
||||
.join(VCRoom.events)
|
||||
.join(VCRoomEventAssociation.event)
|
||||
.group_by(VCRoom.id))
|
||||
|
||||
# non-deleted rooms with no recent associations
|
||||
return VCRoom.find_all(VCRoom.status != VCRoomStatus.deleted, ~VCRoom.id.in_(recently_used))
|
||||
|
||||
|
||||
def notify_owner(plugin, vc_room):
|
||||
"""Notifies about the deletion of a Zoom room from the Zoom server."""
|
||||
user = vc_room.zoom_meeting.owned_by_user
|
||||
tpl = get_plugin_template_module('emails/remote_deleted.html', plugin=plugin, vc_room=vc_room, event=None,
|
||||
vc_room_event=None, user=user)
|
||||
_send('delete', user, plugin, None, vc_room, tpl)
|
||||
|
||||
|
||||
@celery.periodic_task(run_every=crontab(minute='0', hour='3', day_of_week='monday'), plugin='vc_zoom')
|
||||
def zoom_cleanup(dry_run=False):
|
||||
from indico_vc_zoom.plugin import ZoomPlugin
|
||||
max_room_event_age = ZoomPlugin.settings.get('num_days_old')
|
||||
|
||||
ZoomPlugin.logger.info('Deleting Zoom rooms that are not used or linked to events all older than %d days',
|
||||
max_room_event_age)
|
||||
candidate_rooms = find_old_zoom_rooms(max_room_event_age)
|
||||
ZoomPlugin.logger.info('%d rooms found', len(candidate_rooms))
|
||||
|
||||
if dry_run:
|
||||
for vc_room in candidate_rooms:
|
||||
ZoomPlugin.logger.info('Would delete Zoom room %s from server', vc_room)
|
||||
return
|
||||
|
||||
for vc_room in committing_iterator(candidate_rooms, n=20):
|
||||
try:
|
||||
ZoomPlugin.instance.delete_room(vc_room, None)
|
||||
ZoomPlugin.logger.info('Room %s deleted from Zoom server', vc_room)
|
||||
notify_owner(ZoomPlugin.instance, vc_room)
|
||||
vc_room.status = VCRoomStatus.deleted
|
||||
except RoomNotFoundAPIException:
|
||||
ZoomPlugin.logger.warning('Room %s had been already deleted from the Zoom server', vc_room)
|
||||
vc_room.status = VCRoomStatus.deleted
|
||||
except APIException:
|
||||
ZoomPlugin.logger.exception('Impossible to delete Zoom room %s', vc_room)
|
||||
6
indico_vc_zoom/templates/buttons.html
Normal file
6
indico_vc_zoom/templates/buttons.html
Normal file
@ -0,0 +1,6 @@
|
||||
{% macro render_join_button(vc_room, extra_classes="") %}
|
||||
<a class="i-button highlight {{ extra_classes }}"
|
||||
href="{{ vc_room.zoom_meeting.join_url }}" target="_blank">
|
||||
{% trans %}Join{% endtrans %}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
16
indico_vc_zoom/templates/emails/created.html
Normal file
16
indico_vc_zoom/templates/emails/created.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends 'vc/emails/created.html' %}
|
||||
|
||||
{% block plugin_specific_info %}
|
||||
<li>
|
||||
<strong>Zoom URL</strong>:
|
||||
<a href="{{ vc_room.zoom_meeting.join_url }}">{{ vc_room.zoom_meeting.join_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 %}
|
||||
1
indico_vc_zoom/templates/emails/deleted.html
Normal file
1
indico_vc_zoom/templates/emails/deleted.html
Normal file
@ -0,0 +1 @@
|
||||
{% extends 'vc/emails/deleted.html' %}
|
||||
18
indico_vc_zoom/templates/emails/notify_start_url.html
Normal file
18
indico_vc_zoom/templates/emails/notify_start_url.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'emails/base.html' %}
|
||||
|
||||
{% block subject -%}
|
||||
[DON'T SHARE] Zoom Host URL - {{ vc_room.name }}
|
||||
{%- endblock %}
|
||||
|
||||
{% block header -%}
|
||||
|
||||
<li>
|
||||
<strong> HOST Zoom URL</strong>:
|
||||
<a href="{{ vc_room.data.start_url }}">{{ vc_room.data.start_url }}</a>
|
||||
</li>
|
||||
|
||||
Please don't share this link.
|
||||
|
||||
{% block custom_footer %}{% endblock %}
|
||||
|
||||
{%- endblock %}
|
||||
16
indico_vc_zoom/templates/emails/remote_deleted.html
Normal file
16
indico_vc_zoom/templates/emails/remote_deleted.html
Normal 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 %}
|
||||
6
indico_vc_zoom/templates/event_buttons.html
Normal file
6
indico_vc_zoom/templates/event_buttons.html
Normal 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, "i-button-small event-service-right-button join-button") }}
|
||||
{% endblock %}
|
||||
37
indico_vc_zoom/templates/info_box.html
Normal file
37
indico_vc_zoom/templates/info_box.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% from '_clipboard_input.html' import clipboard_input %}
|
||||
{% set owner = vc_room.zoom_meeting.owned_by_user %}
|
||||
{% set phone_link = settings.get('zoom_phone_link') %}
|
||||
<dl>
|
||||
<dt>{% trans %}Name{% endtrans %}</dt>
|
||||
<dd>{{ vc_room.name }}</dd>
|
||||
<dt>{% trans %}Meeting Topic{% endtrans %}</dt>
|
||||
<dd>{{ vc_room.data.description }}</dd>
|
||||
{% if vc_room.zoom_meeting %}
|
||||
<dt>{% trans %}Meeting ID{% endtrans %}</dt>
|
||||
<dd>{{ vc_room.zoom_meeting.meeting }}</dd>
|
||||
{% endif %}
|
||||
{% if owner %}
|
||||
<dt>{% trans %}Owner{% endtrans %}</dt>
|
||||
<dd>{{ owner.full_name }}</dd>
|
||||
{% endif %}
|
||||
{% if event_vc_room.data.show_pin and vc_room.data.room_pin %}
|
||||
<dt>{% trans %}Room PIN{% endtrans %}</dt>
|
||||
<dd>{{ vc_room.data.room_pin }}</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.zoom_meeting.join_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>
|
||||
55
indico_vc_zoom/templates/manage_event_info_box.html
Normal file
55
indico_vc_zoom/templates/manage_event_info_box.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% from '_password.html' import password %}
|
||||
{% from '_clipboard_input.html' import clipboard_input %}
|
||||
{% set owner = vc_room.zoom_meeting.owned_by_user %}
|
||||
{% set phone_link = settings.get('zoom_phone_link') %}
|
||||
<dl class="details-container">
|
||||
<dt>{% trans %}Meeting Topic{% endtrans %}</dt>
|
||||
<dd>{{ vc_room.data.description }}</dd>
|
||||
<dt>{% trans %}Meeting ID{% endtrans %}</dt>
|
||||
<dd>{{ vc_room.zoom_meeting.meeting }}</dd>
|
||||
<dt>{% trans %}Owner{% endtrans %}</dt>
|
||||
<dd>
|
||||
{% if owner %}
|
||||
{{ owner.full_name }}
|
||||
{% else %}
|
||||
{{ vc_room.data.owner_account }} <em>(deleted)</em>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% if vc_room.data.room_pin %}
|
||||
<dt>{% trans %}Room PIN{% endtrans %}</dt>
|
||||
<dd>
|
||||
{{ password('vc-room-pin-%s'|format(event_vc_room.id), value=vc_room.data.room_pin, toggle=True,
|
||||
readonly=true) }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if vc_room.data.moderation_pin %}
|
||||
<dt>{% trans %}Moderation PIN{% endtrans %}</dt>
|
||||
<dd>
|
||||
{{ password('vc-moderation-pin-%s'|format(event_vc_room.id), value=vc_room.data.moderation_pin,
|
||||
toggle=True, readonly=true) }}
|
||||
</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans %}Zoom URL{% endtrans %}</dt>
|
||||
<dd>
|
||||
{{ clipboard_input(vc_room.zoom_meeting.join_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>
|
||||
6
indico_vc_zoom/templates/management_buttons.html
Normal file
6
indico_vc_zoom/templates/management_buttons.html
Normal 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, extra_classes="icon-play") }}
|
||||
{% endblock %}
|
||||
7
indico_vc_zoom/templates/vc_room_timetable_buttons.html
Normal file
7
indico_vc_zoom/templates/vc_room_timetable_buttons.html
Normal 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, "i-button-small event-service-right-button join-button") }}
|
||||
{% endblock %}
|
||||
23
indico_vc_zoom/translations/fr_FR/LC_MESSAGES/messages-js.po
Normal file
23
indico_vc_zoom/translations/fr_FR/LC_MESSAGES/messages-js.po
Normal 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"
|
||||
289
indico_vc_zoom/translations/fr_FR/LC_MESSAGES/messages.po
Normal file
289
indico_vc_zoom/translations/fr_FR/LC_MESSAGES/messages.po
Normal 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 "
|
||||
90
indico_vc_zoom/util.py
Normal file
90
indico_vc_zoom/util.py
Normal file
@ -0,0 +1,90 @@
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from flask_multipass import IdentityRetrievalFailed
|
||||
|
||||
from indico.core.auth import multipass
|
||||
from indico.core.db import db
|
||||
from indico.modules.auth import Identity
|
||||
from indico.modules.users import User
|
||||
|
||||
|
||||
authenticators_re = re.compile(r'\s*,\s*')
|
||||
|
||||
|
||||
def iter_user_identities(user):
|
||||
"""Iterates over all existing user identities that can be used with Zoom"""
|
||||
from indico_vc_zoom.plugin import ZoomPlugin
|
||||
providers = authenticators_re.split(ZoomPlugin.settings.get('authenticators'))
|
||||
done = set()
|
||||
for provider in providers:
|
||||
for _, identifier in user.iter_identifiers(check_providers=True, providers={provider}):
|
||||
if identifier in done:
|
||||
continue
|
||||
done.add(identifier)
|
||||
yield identifier
|
||||
|
||||
|
||||
def get_user_from_identifier(settings, identifier):
|
||||
"""Get an actual User object from an identifier"""
|
||||
providers = list(auth.strip() for auth in settings.get('authenticators').split(','))
|
||||
identities = Identity.find_all(Identity.provider.in_(providers), Identity.identifier == identifier)
|
||||
if identities:
|
||||
return sorted(identities, key=lambda x: providers.index(x.provider))[0].user
|
||||
for provider in providers:
|
||||
try:
|
||||
identity_info = multipass.get_identity(provider, identifier)
|
||||
except IdentityRetrievalFailed:
|
||||
continue
|
||||
if identity_info is None:
|
||||
continue
|
||||
if not identity_info.provider.settings.get('trusted_email'):
|
||||
continue
|
||||
emails = {email.lower() for email in identity_info.data.getlist('email') if email}
|
||||
if not emails:
|
||||
continue
|
||||
user = User.find_first(~User.is_deleted, User.all_emails.in_(list(emails)))
|
||||
if user:
|
||||
return user
|
||||
|
||||
|
||||
def iter_extensions(prefix, event_id):
|
||||
"""Return extension (prefix + event_id) with an optional suffix which is
|
||||
incremented step by step in case of collision
|
||||
"""
|
||||
extension = '{prefix}{event_id}'.format(prefix=prefix, event_id=event_id)
|
||||
yield extension
|
||||
suffix = 1
|
||||
while True:
|
||||
yield '{extension}{suffix}'.format(extension=extension, suffix=suffix)
|
||||
suffix += 1
|
||||
|
||||
|
||||
def update_room_from_obj(settings, vc_room, room_obj):
|
||||
"""Updates a VCRoom DB object using a SOAP room object returned by the API"""
|
||||
vc_room.name = room_obj.name
|
||||
if room_obj.ownerName != vc_room.data['owner_identity']:
|
||||
owner = get_user_from_identifier(settings, room_obj.ownerName) or User.get_system_user()
|
||||
vc_room.zoom_meeting.owned_by_user = owner
|
||||
|
||||
vc_room.data.update({
|
||||
'description': room_obj.description,
|
||||
'zoom_id': unicode(room_obj.roomID),
|
||||
'url': room_obj.RoomMode.roomURL,
|
||||
'owner_identity': room_obj.ownerName,
|
||||
'room_pin': room_obj.RoomMode.roomPIN if room_obj.RoomMode.hasPIN else "",
|
||||
'moderation_pin': room_obj.RoomMode.moderatorPIN if room_obj.RoomMode.hasModeratorPIN else "",
|
||||
})
|
||||
vc_room.zoom_meeting.extension = int(room_obj.extension)
|
||||
|
||||
|
||||
def retrieve_principal(principal):
|
||||
from indico.modules.users import User
|
||||
type_, id_ = principal
|
||||
if type_ in {'Avatar', 'User'}:
|
||||
return User.get(int(id_))
|
||||
else:
|
||||
raise ValueError('Unexpected type: {}'.format(type_))
|
||||
29
setup.py
Normal file
29
setup.py
Normal file
@ -0,0 +1,29 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
setup(
|
||||
name='indico-plugin-vc-zoom',
|
||||
version='0.2-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',
|
||||
'requests',
|
||||
'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'}}
|
||||
)
|
||||
24
tests/task_test.py
Normal file
24
tests/task_test.py
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from pytz import utc
|
||||
|
||||
from indico.modules.vc.models.vc_rooms import VCRoom, VCRoomEventAssociation, VCRoomStatus
|
||||
|
||||
from indico_vc_zoom.models.zoom_meetings import ZoomMeeting
|
||||
|
||||
|
||||
@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}
|
||||
30163
url_map.json
Normal file
30163
url_map.json
Normal file
File diff suppressed because it is too large
Load Diff
5
webpack-bundles.json
Normal file
5
webpack-bundles.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"entry": {
|
||||
"main": "./index.js"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user