diff --git a/owncloud/MANIFEST.in b/owncloud/MANIFEST.in new file mode 100644 index 0000000..3665b82 --- /dev/null +++ b/owncloud/MANIFEST.in @@ -0,0 +1,6 @@ +graft indico_owncloud/client +graft indico_owncloud/static +graft indico_owncloud/templates +graft indico_owncloud/translations + +global-exclude *.pyc __pycache__ .keep diff --git a/owncloud/README.md b/owncloud/README.md new file mode 100644 index 0000000..2d96e65 --- /dev/null +++ b/owncloud/README.md @@ -0,0 +1,13 @@ +# OwnCloud integration plugin + +This plugin enables integration with an ownCloud server, initially for the +purpose of attaching materials to an event or category. A new "from the cloud" +button shows up in the materials section which enables a manager to log into his +ownCloud account and pick files from his personal storage. Those files will be +copied as attachments. + +## Changelog + +### 3.2 + +- Initial release for Indico 3.2 diff --git a/owncloud/indico_owncloud/__init__.py b/owncloud/indico_owncloud/__init__.py new file mode 100644 index 0000000..053c5d7 --- /dev/null +++ b/owncloud/indico_owncloud/__init__.py @@ -0,0 +1,11 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +from indico.util.i18n import make_bound_gettext + + +_ = make_bound_gettext('owncloud') diff --git a/owncloud/indico_owncloud/blueprint.py b/owncloud/indico_owncloud/blueprint.py new file mode 100644 index 0000000..1d0c517 --- /dev/null +++ b/owncloud/indico_owncloud/blueprint.py @@ -0,0 +1,17 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +from indico.core.plugins import IndicoPluginBlueprint + +from indico_owncloud.controllers import RHAddCategoryAttachmentOwncloud, RHAddEventAttachmentOwncloud + + +blueprint = IndicoPluginBlueprint('owncloud', 'indico_owncloud') +blueprint.add_url_rule('/category//manage/attachments/add/owncloud', 'owncloud_category', + RHAddCategoryAttachmentOwncloud, methods=('GET', 'POST'), defaults={'object_type': 'category'}) +blueprint.add_url_rule('/event//manage/attachments/add/owncloud', 'owncloud_event', + RHAddEventAttachmentOwncloud, methods=('GET', 'POST'), defaults={'object_type': 'event'}) diff --git a/owncloud/indico_owncloud/client/index.js b/owncloud/indico_owncloud/client/index.js new file mode 100644 index 0000000..3856bf1 --- /dev/null +++ b/owncloud/indico_owncloud/client/index.js @@ -0,0 +1,21 @@ +// This file is part of the Indico plugins. +// Copyright (C) 2002 - 2022 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. + +// eslint-disable-next-line import/unambiguous +window.setupOwncloudFilePickerWidget = ({filepickerUrl, fieldId}) => { + window.addEventListener('message', message => { + const iframe = document.querySelector('#owncloud_filepicker-file-picker'); + if ( + message.origin === filepickerUrl && + message.source === iframe.contentWindow && + message.data && + message.data.files + ) { + document.getElementById(`${fieldId}-files`).value = message.data.files.join('\n'); + } + }); +}; diff --git a/owncloud/indico_owncloud/client/style.scss b/owncloud/indico_owncloud/client/style.scss new file mode 100644 index 0000000..678e672 --- /dev/null +++ b/owncloud/indico_owncloud/client/style.scss @@ -0,0 +1,23 @@ +// This file is part of the Indico plugins. +// Copyright (C) 2002 - 2022 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. + +@import 'base/palette'; + +form#attachment-owncloudfilepicker-form.full-width .owncloudfilepicker { + width: 770px; + height: 280px; + border: 1px solid $light-gray; +} + +.owncloudfilepicker { + border: 0px none; + width: 100%; +} + +.button-label { + margin-left: 0.4rem; +} diff --git a/owncloud/indico_owncloud/controllers.py b/owncloud/indico_owncloud/controllers.py new file mode 100644 index 0000000..495f956 --- /dev/null +++ b/owncloud/indico_owncloud/controllers.py @@ -0,0 +1,64 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +import mimetypes + +import requests +from flask import flash, session +from flask_pluginengine import render_plugin_template + +from indico.core.db import db +from indico.modules.attachments.controllers.management.base import (_get_folders_protection_info, + _render_attachment_list, _render_protection_message) +from indico.modules.attachments.controllers.management.category import RHCategoryAttachmentManagementBase +from indico.modules.attachments.controllers.management.event import RHEventAttachmentManagementBase +from indico.modules.attachments.models.attachments import Attachment, AttachmentFile, AttachmentType +from indico.modules.attachments.models.folders import AttachmentFolder +from indico.util.fs import secure_client_filename +from indico.web.util import jsonify_data, jsonify_template + +from indico_owncloud import _ +from indico_owncloud.forms import AddAttachmentOwncloudForm + + +class AddAttachmentOwncloudMixin: + """Add cloud storage attachment.""" + + def _process(self): + form = AddAttachmentOwncloudForm(linked_object=self.object) + if form.validate_on_submit(): + files = form.data['owncloud_filepicker']['files'] + folder = form.data['folder'] or AttachmentFolder.get_or_create_default(linked_object=self.object) + for f in files: + filename = f.split('/')[-1].split('?')[0] + local_filename = secure_client_filename(filename) + attachment = Attachment(folder=folder, user=session.user, title=local_filename, + type=AttachmentType.file, protection_mode=form.data['protection_mode']) + if attachment.is_self_protected: + attachment.acl = form.data['acl'] + content_type = mimetypes.guess_type(local_filename)[0] or 'application/octet-stream' + attachment.file = AttachmentFile(user=session.user, filename=local_filename, content_type=content_type) + + file_response = requests.get(f) + file_response.raise_for_status() + attachment.file.save(file_response.content) + + db.session.add(attachment) + db.session.flush() + flash(_('Attachment added')) + return jsonify_data(attachment_list=_render_attachment_list(self.object)) + return jsonify_template('add_owncloud_files.html', _render_func=render_plugin_template, form=form, + protection_message=_render_protection_message(self.object), + folders_protection_info=_get_folders_protection_info(self.object)) + + +class RHAddCategoryAttachmentOwncloud(AddAttachmentOwncloudMixin, RHCategoryAttachmentManagementBase): + pass + + +class RHAddEventAttachmentOwncloud(AddAttachmentOwncloudMixin, RHEventAttachmentManagementBase): + pass diff --git a/owncloud/indico_owncloud/forms.py b/owncloud/indico_owncloud/forms.py new file mode 100644 index 0000000..de33eab --- /dev/null +++ b/owncloud/indico_owncloud/forms.py @@ -0,0 +1,45 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +from wtforms import Field +from wtforms.validators import DataRequired, ValidationError + +from indico.core.config import config +from indico.modules.attachments.forms import AttachmentFormBase +from indico.util.i18n import _ +from indico.web.forms.widgets import JinjaWidget + +from indico_owncloud.util import get_filepicker_url + + +class IndicoOwncloudField(Field): + widget = JinjaWidget('owncloud_filepicker_widget.html', plugin='owncloud', single_line=True, single_kwargs=True) + + def __init__(self, label=None, validators=None, **kwargs): + super().__init__(label, validators, **kwargs) + self.filepicker_url = get_filepicker_url() + self.origin = config.BASE_URL + + def process_formdata(self, valuelist): + if valuelist: + self.data = {'files': [f.rstrip() for f in valuelist[0].split('\n')]} + + +class AttachmentOwncloudFormMixin: + owncloud_filepicker = IndicoOwncloudField(_('Files'), [DataRequired()]) + + def validate_owncloud_filepicker(self, field): + if self.owncloud_filepicker.data and self.owncloud_filepicker.data['files'] == ['']: + raise ValidationError("Select files to add and click 'Select resources'") + + +class AddAttachmentOwncloudForm(AttachmentOwncloudFormMixin, AttachmentFormBase): + pass + + +class EditAttachmentOwncloudForm(AttachmentOwncloudFormMixin, AttachmentFormBase): + pass diff --git a/owncloud/indico_owncloud/plugin.py b/owncloud/indico_owncloud/plugin.py new file mode 100644 index 0000000..55f004c --- /dev/null +++ b/owncloud/indico_owncloud/plugin.py @@ -0,0 +1,66 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +from flask_pluginengine.plugin import render_plugin_template +from wtforms.fields import StringField, URLField +from wtforms.validators import DataRequired + +from indico.core.plugins import IndicoPlugin +from indico.modules.attachments.views import WPEventAttachments +from indico.modules.categories.views import WPCategoryManagement +from indico.modules.events.views import WPConferenceDisplay, WPSimpleEventDisplay +from indico.web.forms.base import IndicoForm + +from indico_owncloud import _ +from indico_owncloud.blueprint import blueprint +from indico_owncloud.util import is_configured + + +class PluginSettingsForm(IndicoForm): + filepicker_url = URLField(_('File-picker URL'), [DataRequired()]) + service_name = StringField(_('Service name'), [], + description=_('A customized name for the cloud service. If empty, the default ' + "'The cloud' will be used.")) + button_icon_url = URLField(_('Custom icon URL'), [], + description=_('URL for a customized icon to show in the add attachment button. If ' + 'empty, the default cloud icon will be used')) + + +class OwncloudPlugin(IndicoPlugin): + """OwnCloud integration + + Provides an integration with OwnCloud storage servers, enabling managers + to attach files to categories/events from their cloud storage. + """ + configurable = True + settings_form = PluginSettingsForm + default_settings = { + 'filepicker_url': '', + 'service_name': '', + 'button_icon_url': '', + } + + def init(self): + super().init() + self.template_hook('attachment-sources', self._inject_owncloud_button) + self.inject_bundle('owncloud.js', WPEventAttachments) + self.inject_bundle('owncloud.js', WPSimpleEventDisplay) + self.inject_bundle('owncloud.js', WPConferenceDisplay) + self.inject_bundle('owncloud.js', WPCategoryManagement) + self.inject_bundle('main.css', WPEventAttachments) + self.inject_bundle('main.css', WPSimpleEventDisplay) + self.inject_bundle('main.css', WPConferenceDisplay) + self.inject_bundle('main.css', WPCategoryManagement) + + def get_blueprints(self): + return blueprint + + def _inject_owncloud_button(self, linked_object=None, **kwargs): + if is_configured(): + return render_plugin_template('owncloud_button.html', id=linked_object.id, linked_object=linked_object, + service_name=self.settings.get('service_name'), + button_icon_url=self.settings.get('button_icon_url')) diff --git a/owncloud/indico_owncloud/templates/add_owncloud_files.html b/owncloud/indico_owncloud/templates/add_owncloud_files.html new file mode 100644 index 0000000..9351d31 --- /dev/null +++ b/owncloud/indico_owncloud/templates/add_owncloud_files.html @@ -0,0 +1,34 @@ +{% from 'forms/_form.html' import form_header, form_footer, form_rows, form_row %} + +{%- block content %} + {{ form_header(form, id='attachment-owncloudfilepicker-form', classes=('full-width')) }} + {{ form_row(form.owncloud_filepicker, skip_label=true) }} + {{ form_rows(form, fields=('folder', 'protected', 'acl')) }} + {{ protection_message | safe }} + {% call form_footer(attach_form) %} + + + {% endcall %} + +{%- endblock %} diff --git a/owncloud/indico_owncloud/templates/owncloud_button.html b/owncloud/indico_owncloud/templates/owncloud_button.html new file mode 100644 index 0000000..b9bec45 --- /dev/null +++ b/owncloud/indico_owncloud/templates/owncloud_button.html @@ -0,0 +1,10 @@ +{% set service_name = service_name or _('the cloud') %} + + + {%- if button_icon_url -%} + + {%- endif -%} + {%- trans -%}From {{ service_name }}{%- endtrans -%} + diff --git a/owncloud/indico_owncloud/templates/owncloud_filepicker_widget.html b/owncloud/indico_owncloud/templates/owncloud_filepicker_widget.html new file mode 100644 index 0000000..c04ce5d --- /dev/null +++ b/owncloud/indico_owncloud/templates/owncloud_filepicker_widget.html @@ -0,0 +1,20 @@ +{% extends 'forms/base_widget.html' %} + +{% block html %} + {% set src = field.filepicker_url + '?origin=' + field.origin %} +
+ + + + +
+{% endblock %} + +{% block javascript %} + +{% endblock %} diff --git a/owncloud/indico_owncloud/translations/messages.pot b/owncloud/indico_owncloud/translations/messages.pot new file mode 100644 index 0000000..d888a8c --- /dev/null +++ b/owncloud/indico_owncloud/translations/messages.pot @@ -0,0 +1,73 @@ +# Translations template for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-03-16 15:30+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.1\n" + +#: indico_owncloud/controllers.py:51 +msgid "Attachment added" +msgstr "" + +#: indico_owncloud/forms.py:34 +msgid "Files" +msgstr "" + +#: indico_owncloud/plugin.py:24 +msgid "File-picker URL" +msgstr "" + +#: indico_owncloud/plugin.py:25 +msgid "Service name" +msgstr "" + +#: indico_owncloud/plugin.py:26 +msgid "" +"A customized name for the cloud service. If empty, the default 'The " +"cloud' will be used." +msgstr "" + +#: indico_owncloud/plugin.py:28 +msgid "Custom icon URL" +msgstr "" + +#: indico_owncloud/plugin.py:29 +msgid "" +"URL for a customized icon to show in the add attachment button. If empty," +" the default cloud icon will be used" +msgstr "" + +#: indico_owncloud/templates/add_owncloud_files.html:9 +msgid "Add" +msgstr "" + +#: indico_owncloud/templates/add_owncloud_files.html:10 +msgid "Cancel" +msgstr "" + +#: indico_owncloud/templates/owncloud_button.html:1 +msgid "the cloud" +msgstr "" + +#: indico_owncloud/templates/owncloud_button.html:4 +#, python-format +msgid "Add files from %(service_name)s" +msgstr "" + +#: indico_owncloud/templates/owncloud_button.html:9 +#, python-format +msgid "From %(service_name)s" +msgstr "" + diff --git a/owncloud/indico_owncloud/util.py b/owncloud/indico_owncloud/util.py new file mode 100644 index 0000000..d9e2026 --- /dev/null +++ b/owncloud/indico_owncloud/util.py @@ -0,0 +1,18 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +def is_configured(): + """Check whether the plugin is properly configured.""" + from indico_owncloud.plugin import OwncloudPlugin + + return bool(OwncloudPlugin.settings.get('filepicker_url')) + + +def get_filepicker_url(): + from indico_owncloud.plugin import OwncloudPlugin + + return OwncloudPlugin.settings.get('filepicker_url').rstrip('/') diff --git a/owncloud/setup.cfg b/owncloud/setup.cfg new file mode 100644 index 0000000..684c4ec --- /dev/null +++ b/owncloud/setup.cfg @@ -0,0 +1,30 @@ +[metadata] +name = indico-plugin-owncloud +version = 3.2-dev +description = Integrates ownCloud storage as a source for materials +long_description = file: README.md +long_description_content_type = text/markdown; charset=UTF-8; variant=GFM +url = https://github.com/indico/indico-plugins +license = MIT +author = Indico Team +author_email = indico-team@cern.ch +classifiers = + Environment :: Plugins + Environment :: Web Environment + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3.9 + +[options] +packages = find: +zip_safe = false +include_package_data = true +python_requires = ~=3.9.0 +install_requires = + indico>=3.2.dev0 + +[options.entry_points] +indico.plugins = + owncloud = indico_owncloud.plugin:OwncloudPlugin + +[pydocstyle] +ignore = D100,D101,D102,D103,D104,D105,D107,D203,D213 diff --git a/owncloud/setup.py b/owncloud/setup.py new file mode 100644 index 0000000..a98c3c3 --- /dev/null +++ b/owncloud/setup.py @@ -0,0 +1,11 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 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. + +from setuptools import setup + + +setup() diff --git a/owncloud/webpack-bundles.json b/owncloud/webpack-bundles.json new file mode 100644 index 0000000..8640d70 --- /dev/null +++ b/owncloud/webpack-bundles.json @@ -0,0 +1,6 @@ +{ + "entry": { + "owncloud": "./index.js", + "main": "./style.scss" + } +}