From 4e9d22d376ec5548d14ffa9c866077a66fda379b Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 21 Aug 2020 19:49:24 +0530 Subject: [PATCH] apps: Remove Coquelicot Bepasty is the replacement file-sharing app. Signed-off-by: Joseph Nuthalapati Reviewed-by: Sunil Mohan Adapa --- actions/coquelicot | 109 --- debian/copyright | 3 +- debian/freedombox.maintscript | 1 + doc/manual/en/Coquelicot.raw.xml | 1 - doc/manual/es/Coquelicot.raw.xml | 1 - plinth/modules/coquelicot/__init__.py | 86 --- .../conf-available/coquelicot-freedombox.conf | 5 - .../etc/plinth/modules-enabled/coquelicot | 1 - plinth/modules/coquelicot/forms.py | 20 - plinth/modules/coquelicot/manifest.py | 24 - plinth/modules/coquelicot/tests/__init__.py | 0 .../coquelicot/tests/coquelicot.feature | 57 -- .../coquelicot/tests/test_functional.py | 126 ---- plinth/modules/coquelicot/urls.py | 12 - plinth/modules/coquelicot/views.py | 52 -- pytest.ini | 1 - setup.py | 4 + static/themes/default/icons/coquelicot.png | Bin 27771 -> 0 bytes static/themes/default/icons/coquelicot.svg | 648 ------------------ 19 files changed, 6 insertions(+), 1145 deletions(-) delete mode 100755 actions/coquelicot delete mode 100644 doc/manual/en/Coquelicot.raw.xml delete mode 100644 doc/manual/es/Coquelicot.raw.xml delete mode 100644 plinth/modules/coquelicot/__init__.py delete mode 100644 plinth/modules/coquelicot/data/etc/apache2/conf-available/coquelicot-freedombox.conf delete mode 100644 plinth/modules/coquelicot/data/etc/plinth/modules-enabled/coquelicot delete mode 100644 plinth/modules/coquelicot/forms.py delete mode 100644 plinth/modules/coquelicot/manifest.py delete mode 100644 plinth/modules/coquelicot/tests/__init__.py delete mode 100644 plinth/modules/coquelicot/tests/coquelicot.feature delete mode 100644 plinth/modules/coquelicot/tests/test_functional.py delete mode 100644 plinth/modules/coquelicot/urls.py delete mode 100644 plinth/modules/coquelicot/views.py delete mode 100644 static/themes/default/icons/coquelicot.png delete mode 100644 static/themes/default/icons/coquelicot.svg diff --git a/actions/coquelicot b/actions/coquelicot deleted file mode 100755 index aa46115c6..000000000 --- a/actions/coquelicot +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/python3 -# -*- mode: python -*- -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Configuration helper for coquelicot. -""" - -import argparse -import hashlib -import os -import sys - -import yaml - -from plinth import action_utils - -SETTINGS_FILE = '/etc/coquelicot/settings.yml' - - -def parse_arguments(): - """Return parsed command line arguments as dictionary.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - - subparsers.add_parser('setup', - help='Post-installation operations for coquelicot') - - subparsers.add_parser( - 'set-upload-password', - help='Set a new global, pre-shared password for uploading files') - - max_file_size = subparsers.add_parser( - 'set-max-file-size', - help='Change the maximum size of the files that can be uploaded to ' - 'Coquelicot') - max_file_size.add_argument('size', type=int, help='upload file size in MB') - - subparsers.add_parser( - 'get-max-file-size', - help='Print the maximum size of the files that can be uploaded to ' - 'Coquelicot') - - subparsers.required = True - return parser.parse_args() - - -def subcommand_setup(_): - """Perform post-installation operations for coquelicot.""" - settings = read_settings() - settings['path'] = "/coquelicot" - settings['max_file_size'] = mebibytes(1024) - write_settings(settings) - action_utils.service_restart('coquelicot') - - -def subcommand_set_upload_password(arguments): - """Set a new upload password for Coquelicot.""" - upload_password = ''.join(sys.stdin) - settings = read_settings() - hashed_pw = hashlib.sha1(upload_password.encode()).hexdigest() - settings['authentication_method']['upload_password'] = hashed_pw - write_settings(settings) - action_utils.service_try_restart('coquelicot') - - -def subcommand_set_max_file_size(arguments): - """Set a new maximum file size for Coquelicot.""" - size_in_bytes = mebibytes(arguments.size) - settings = read_settings() - settings['max_file_size'] = size_in_bytes - write_settings(settings) - action_utils.service_try_restart('coquelicot') - - -def subcommand_get_max_file_size(_): - """Print the maximum file size to stdout.""" - if os.path.exists(SETTINGS_FILE): - settings = read_settings() - print(int(settings['max_file_size'] / (1024 * 1024))) - else: - print(-1) - - -def read_settings(): - with open(SETTINGS_FILE, 'rb') as settings_file: - return yaml.load(settings_file) - - -def write_settings(settings): - with open(SETTINGS_FILE, 'w') as settings_file: - yaml.dump(settings, settings_file) - - -def main(): - """Parse arguments and perform all duties.""" - arguments = parse_arguments() - - subcommand = arguments.subcommand.replace('-', '_') - subcommand_method = globals()['subcommand_' + subcommand] - subcommand_method(arguments) - - -def mebibytes(size): - """Return the given size of mebibytes in bytes.""" - return size * 1024 * 1024 - - -if __name__ == '__main__': - main() diff --git a/debian/copyright b/debian/copyright index 64e4126f9..f12e00e7a 100644 --- a/debian/copyright +++ b/debian/copyright @@ -5,8 +5,7 @@ Files: * Copyright: 2011-2020 FreedomBox Authors License: AGPL-3+ -Files: static/themes/default/icons/coquelicot.svg - static/themes/default/icons/jsxc.png +Files: static/themes/default/icons/jsxc.png static/themes/default/icons/jsxc.svg static/themes/default/icons/mldonkey.svg Copyright: 2011-2019 FreedomBox Authors diff --git a/debian/freedombox.maintscript b/debian/freedombox.maintscript index aea3d40da..d9b3a7b5e 100644 --- a/debian/freedombox.maintscript +++ b/debian/freedombox.maintscript @@ -12,3 +12,4 @@ rm_conffile /etc/plinth/modules-enabled/repro 20.1~ rm_conffile /etc/apt/preferences.d/50freedombox3.pref 20.5~ rm_conffile /etc/plinth/plinth.config 20.12~ rm_conffile /etc/plinth/custom-shortcuts.json 20.12~ +rm_conffile /etc/plinth/modules-enabled/coquelicot 20.14~ diff --git a/doc/manual/en/Coquelicot.raw.xml b/doc/manual/en/Coquelicot.raw.xml deleted file mode 100644 index 15658957c..000000000 --- a/doc/manual/en/Coquelicot.raw.xml +++ /dev/null @@ -1 +0,0 @@ -
FreedomBox/Manual/Coquelicot112020-05-30 17:57:45SunilMohanAdapaUpdate the title to emphasize app name over its generic name102020-05-23 19:51:30JamesValleroyadd TableOfContents92020-05-23 17:03:10JamesValleroyrename plinth -> freedombox82020-04-12 16:01:36JamesValleroyadd links back to top level pages72019-09-11 09:45:09fioddorCategory Deduplicated62018-12-30 19:59:56DrahtseilBasic priniciple52018-03-05 09:15:01JosephNuthalapaticoquelicot: Fix broken links42018-02-26 17:14:51JamesValleroyincluded in 0.2432018-02-12 23:48:10JamesValleroybump version22018-02-12 23:47:14JamesValleroyreplace fancy quote characters with plain quote characters12018-02-10 03:14:55JosephNuthalapatiCreate new page for Coquelicot
Coquelicot (File Sharing)
About CoquelicotCoquelicot is a "one-click" file sharing web application with a focus on protecting users' privacy. The basic principle is simple: users can upload a file to the server, in return they get a unique URL which can be shared with others in order to download the file. A download password can be defined. After the upload you get a unique link that can be shared to your partners in order to Read more about Coquelicot at the Coquelicot README Available since: version 0.24.0
When to use CoquelicotCoquelicot is best used to quickly share a single file. If you want to share a folder, for a single use, compress the folder and share it over Coquelicot which must be kept synchronized between computers, use Syncthing instead Coquelicot can only provide a reasonable degree of privacy. If anonymity is required, you should consider using the desktop application Onionshare instead. Since Coquelicot fully uploads the file to the server, your FreedomBox will incur both upload and download bandwidth costs. For very large files, consider sharing them using BitTorrent by creating a private torrent file. If anonymity is required, use Onionshare. It is P2P and doesn't require a server.
Coquelicot on FreedomBoxWith Coquelicot installed, you can upload files to your FreedomBox server and privately share them. Post installation, the Coquelicot page offers two settings. Upload Password: Coquelicot on FreedomBox is currently configured to use simple password authentication for ease of use. Remember that it's one global password for this Coquelicot instance and not your user password for FreedomBox. You need not remember this password. You can set a new one from the FreedomBox interface anytime. Maximum File Size: You can alter the maximum size of the file that can be transferred through Coquelicot using this setting. The size is in Mebibytes. The maximum file size is only limited by the disk size of your FreedomBox.
PrivacySomeone monitoring your network traffic might find out that some file is being transferred through your FreedomBox and also possibly its size, but will not know the file name. Coquelicot encrypts files on the server and also fills the file contents with 0s when deleting them. This eliminates the risk of file contents being revealed in the event of your FreedomBox being confiscated or stolen. The real risk to mitigate here is a third-party also downloading your file along with the intended recipient.
Sharing over instant messengersSome instant messengers which have previews for websites might download your file in order to show a preview in the conversation. If you set the option of one-time download on a file, you might notice that the one download will be used up by the instant messenger. If sharing over such messengers, please use a download password in combination with a one-time download option.
Sharing download links privatelyIt is recommended to share your file download links and download passwords over encrypted channels. You can simply avoid all the above problems with instant messenger previews by using instant messengers that support encrypted conversations like Riot with Matrix Synapse or XMPP (ejabberd server on FreedomBox) with clients that support end-to-end encryption. Send the download link and the download password in two separate messages (helps if your messenger supports perfect forward secrecy like XMPP with OTR). You can also share your links over PGP-encrypted email using Thunderbird. Back to Features introduction or manual pages. InformationSupportContributeReportsPromoteOverview Hardware Live Help Where To Start Translate Calls Talks Features Vision Q&A Design To Do Releases Press Download Manual Code Contributors Blog FreedomBox for Communities FreedomBox Developer Manual HELP & DISCUSSIONS: Discussion Forum - Mailing List - #freedombox irc.debian.org | CONTACT Foundation | JOIN Project Next call: Sunday, July 26th at 17:00 UTC Latest news: Announcing Pioneer FreedomBox Kits - 2019-03-26 This page is copyright its contributors and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. CategoryFreedomBox
\ No newline at end of file diff --git a/doc/manual/es/Coquelicot.raw.xml b/doc/manual/es/Coquelicot.raw.xml deleted file mode 100644 index ce8731dd7..000000000 --- a/doc/manual/es/Coquelicot.raw.xml +++ /dev/null @@ -1 +0,0 @@ -
es/FreedomBox/Manual/Coquelicot52020-05-30 19:32:53SunilMohanAdapaUpdate the title to emphasize app name over its generic name42020-05-24 06:45:44fioddorSe alinea con la versión 10 en inglés del 23 de mayo de 202032020-04-13 16:17:26fioddorSe alinea con la versión 08 en inglés del 12 de abril de 202022019-09-11 10:34:42fioddorCorrecciones menores.12019-09-11 10:27:55fioddorSe crea la versión española.
Coquelicot (Compartición de Archivos)
Acerca de CoquelicotCoquelicot es aplicación web para compartir archivos enfocada a proteger la privacidad de sus usuarios. El principio básico es simple: los usuarios pueden subir un archivo al servidor y a cambio reciben una URL única para descargarlo que se puede compartir con terceros. Además se puede establecer una contraseña para reforzar el acceso. Más información acerca de Coquelicot en su LEEME Disponible desde: versión 0.24.0
Cuando usar CoquelicotEl mejor uso de Coquelicot es para compartir rápidamente un archivo suelto. Si quieres compartir una carpeta... ...para usar y tirar, comprime la carpeta y compartela como archivo con Coquelicot ...que deba mantenerse sincronizada entre ordenadores usa mejor Syncthing Coquelicot también puede proporcionar un grado de privacidad razonable. Si se necesita anonimato mejor sopesas emplear la aplicación de escritorio Onionshare. Como Coquelicot carga todo el archivo al servidor tu FreedomBox consumirá ancho de banda tanto para la subida como para la descarga. Para archivos muy grandes sopesa compartirlos creando un fichero BitTorrent privado. Si se necesita anonimato usa Onionshare. Es P2P y no necesita servidor.
Coquelicot en FreedomBoxCon Coquelicot instalado puedes subir archivos a tu servidor FreedomBox y compartirlos en privado. Tras la instalación la página de Coquelicot ofrece 2 preferencias. Contraseña de Subida: Actualmente y por facilidad de uso Coquelicot está configurado en FreedomBox para usar autenticación simple por contraseña. Recuerda que se trata de una contraseña global para esta instancia de Coquelicot y no tu contraseña de usuario para FreedomBox. Tienes que acordarte de esta contraseña. Puedes establecer otra en cualquier momento desde el interfaz de FreedomBox. Tamaño Máximo de Archivo: Puedes alterar el tamaño máximo de los archivos a transferir mediante Coquelicot usando esta preferencia. El tamaño se expresa en Mebibytes y el máximo solo está limitado por el espacio en disco de tu FreedomBox.
PrivacidadAlguien que monitorice tu tráfico de red podría averiguar que se está transfiriendo un archivo en tu FreedomBox y posiblemente también su tamaño pero no sabrá su nombre. Coquelicot cifra los archivos en el servidor y sobrescribe los contenidos con 0s al borrarlos, eliminando el riesgo de que se desvelen los contenidos del fichero si tu FreedomBox resultara confiscada o robada. El riesgo real que hay que mitigar es que además del destinatario legítimo un tercero también descargue tu fichero.
Compartir mediante mensajería instantáneaAlgunas aplicaciones de mensajería instantánea con vista previa de sitios web podrían descargar tu fichero para mostrarla (su vista previa) en la conversación. Si configuras la opción de descarga única para un archivo podrías notar que la aplicación de mensajería consume la única descarga. Si compartes mediante estas aplicaciones usa una contraseña de descarga en combinación con la opción de descarga única.
Compartir en privado enlaces de descargaSe recomienda compartir las contraseñas y los enlaces de descarga de tus archivos por canales cifrados. Puedes evitar todos los problemas anteriores con las vistas previas de la mensajería instantánea símplemente empleando aplicaciones de mensajería que soporten conversaciones cifradas como Riot con Matrix Synapse o XMPP (servidor ejabberd en FreedomBox) con clientes que soporten cifrado punto a punto. Envía la contraseña y el enlace de descarga separados en 2 mensajes distintos (ayuda que tu aplicación de mensajería soporte perfect forward secrecy como XMPP con OTR). También puedes compartir tus enlaces por correo electrónico cifrado con PGP usando Thunderbird. Volver a la descripción de Funcionalidades o a las páginas del manual. InformaciónSoporteContribuyeInformesPromueveIntroducción Hardware Ayuda en línea Dónde empezar Traduce Reuniones Charlas Funcionalidades Visión Preguntas y Respuestas Diseño Por hacer Releases Prensa Descargas Manual Codigo Fuente Contribuyentes Blog FreedomBox para Comunidades Manual del Desarrolador de FreedomBox AYUDA y DEBATES: Foro de Debate - Lista de Correo - #freedombox irc.debian.org | CONTACTO Fundación | PARTICIPA Proyecto Next call: Sunday, July 26th at 17:00 UTC Latest news: Announcing Pioneer FreedomBox Kits - 2019-03-26 Esta página está sujeta a copyright y sus autores la publican bajo la licencia pública Creative Commons Atribución-CompartirIgual 4.0 Internacional (CC BY-SA 4.0). CategoryFreedomBox
\ No newline at end of file diff --git a/plinth/modules/coquelicot/__init__.py b/plinth/modules/coquelicot/__init__.py deleted file mode 100644 index 291e0bf95..000000000 --- a/plinth/modules/coquelicot/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Plinth module to configure coquelicot. -""" - -from django.utils.translation import ugettext_lazy as _ - -from plinth import actions -from plinth import app as app_module -from plinth import frontpage, menu -from plinth.daemon import Daemon -from plinth.modules.apache.components import Webserver -from plinth.modules.firewall.components import Firewall - -from .manifest import backup, clients # noqa, pylint: disable=unused-import - -version = 1 - -managed_services = ['coquelicot'] - -managed_packages = ['coquelicot'] - -_description = [ - _('Coquelicot is a "one-click" file sharing web application with a focus ' - 'on protecting users\' privacy. It is best used for quickly sharing a ' - 'single file. '), - _('This Coquelicot instance is exposed to the public but requires an ' - 'upload password to prevent unauthorized access. You can set a new ' - 'upload password in the form that will appear below after installation. ' - 'The default upload password is "test".') -] - -app = None - - -class CoquelicotApp(app_module.App): - """FreedomBox app for Coquelicot.""" - - app_id = 'coquelicot' - - def __init__(self): - """Create components for the app.""" - super().__init__() - info = app_module.Info(app_id=self.app_id, version=version, - name=_('Coquelicot'), - icon_filename='coquelicot', - short_description=_('File Sharing'), - description=_description, - manual_page='Coquelicot', clients=clients) - self.add(info) - - menu_item = menu.Menu('menu-coquelicot', info.name, - info.short_description, info.icon_filename, - 'coquelicot:index', parent_url_name='apps') - self.add(menu_item) - - shortcut = frontpage.Shortcut('shortcut-coquelicot', info.name, - short_description=info.short_description, - icon='coquelicot', url='/coquelicot', - clients=info.clients, - login_required=True) - self.add(shortcut) - - firewall = Firewall('firewall-coquelicot', info.name, - ports=['http', 'https'], is_external=True) - self.add(firewall) - - webserver = Webserver('webserver-coquelicot', 'coquelicot-freedombox', - urls=['https://{host}/coquelicot']) - self.add(webserver) - - daemon = Daemon('daemon-coquelicot', managed_services[0]) - self.add(daemon) - - -def setup(helper, old_version=None): - """Install and configure the module.""" - helper.install(managed_packages) - helper.call('post', actions.superuser_run, 'coquelicot', ['setup']) - helper.call('post', app.enable) - - -def get_current_max_file_size(): - """Get the current value of maximum file size.""" - size = actions.superuser_run('coquelicot', ['get-max-file-size']) - return int(size.strip()) diff --git a/plinth/modules/coquelicot/data/etc/apache2/conf-available/coquelicot-freedombox.conf b/plinth/modules/coquelicot/data/etc/apache2/conf-available/coquelicot-freedombox.conf deleted file mode 100644 index 0fe851e83..000000000 --- a/plinth/modules/coquelicot/data/etc/apache2/conf-available/coquelicot-freedombox.conf +++ /dev/null @@ -1,5 +0,0 @@ - - ProxyPass http://127.0.0.1:51161/coquelicot - SetEnv proxy-sendchunks 1 - RequestHeader set X-Forwarded-SSL "on" - diff --git a/plinth/modules/coquelicot/data/etc/plinth/modules-enabled/coquelicot b/plinth/modules/coquelicot/data/etc/plinth/modules-enabled/coquelicot deleted file mode 100644 index fa55f19dc..000000000 --- a/plinth/modules/coquelicot/data/etc/plinth/modules-enabled/coquelicot +++ /dev/null @@ -1 +0,0 @@ -#plinth.modules.coquelicot diff --git a/plinth/modules/coquelicot/forms.py b/plinth/modules/coquelicot/forms.py deleted file mode 100644 index b418b844c..000000000 --- a/plinth/modules/coquelicot/forms.py +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Plinth form for configuring Coquelicot. -""" - -from django import forms -from django.utils.translation import ugettext_lazy as _ - - -class CoquelicotForm(forms.Form): # pylint: disable=W0232 - """Coquelicot configuration form.""" - upload_password = forms.CharField( - label=_('Upload Password'), - help_text=_('Set a new upload password for Coquelicot. ' - 'Leave this field blank to keep the current password.'), - required=False, widget=forms.PasswordInput) - max_file_size = forms.IntegerField( - label=_("Maximum File Size (in MiB)"), help_text=_( - 'Set the maximum size of the files that can be uploaded to ' - 'Coquelicot.'), required=False, min_value=0) diff --git a/plinth/modules/coquelicot/manifest.py b/plinth/modules/coquelicot/manifest.py deleted file mode 100644 index c128a3fbf..000000000 --- a/plinth/modules/coquelicot/manifest.py +++ /dev/null @@ -1,24 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later - -from django.utils.translation import ugettext_lazy as _ - -from plinth.clients import validate -from plinth.modules.backups.api import validate as validate_backups - -clients = validate([{ - 'name': _('coquelicot'), - 'platforms': [{ - 'type': 'web', - 'url': '/coquelicot' - }] -}]) - -backup = validate_backups({ - 'data': { - 'directories': ['/var/lib/coquelicot'] - }, - 'secrets': { - 'files': ['/etc/coquelicot/settings.yml'] - }, - 'services': ['coquelicot'] -}) diff --git a/plinth/modules/coquelicot/tests/__init__.py b/plinth/modules/coquelicot/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/plinth/modules/coquelicot/tests/coquelicot.feature b/plinth/modules/coquelicot/tests/coquelicot.feature deleted file mode 100644 index f15c4788c..000000000 --- a/plinth/modules/coquelicot/tests/coquelicot.feature +++ /dev/null @@ -1,57 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later - -@apps @coquelicot @backups @skip -Feature: Coquelicot File Sharing - Run Coquelicot File Sharing server. - -Background: - Given I'm a logged in user - Given the coquelicot application is installed - -Scenario: Enable coquelicot application - Given the coquelicot application is disabled - When I enable the coquelicot application - Then the coquelicot service should be running - -Scenario: Modify maximum upload size - Given the coquelicot application is enabled - When I modify the maximum file size of coquelicot to 256 - Then the maximum file size of coquelicot should be 256 - -Scenario: Modify upload password - Given the coquelicot application is enabled - When I modify the coquelicot upload password to whatever123 - Then I should be able to login to coquelicot with password whatever123 - -Scenario: Modify maximum upload size in disabled case - Given the coquelicot application is disabled - When I modify the maximum file size of coquelicot to 123 - Then the coquelicot service should not be running - -Scenario: Upload a file to coquelicot - Given the coquelicot application is enabled - And a sample local file - When I modify the coquelicot upload password to whatever123 - And I upload the sample local file to coquelicot with password whatever123 - And I download the uploaded file from coquelicot - Then contents of downloaded sample file should be same as sample local file - -Scenario: Backup and restore coquelicot - Given the coquelicot application is enabled - When I modify the coquelicot upload password to beforebackup123 - And I modify the maximum file size of coquelicot to 128 - And I upload the sample local file to coquelicot with password beforebackup123 - And I create a backup of the coquelicot app data with name test_coquelicot - And I modify the coquelicot upload password to afterbackup123 - And I modify the maximum file size of coquelicot to 64 - And I restore the coquelicot app data backup with name test_coquelicot - And I download the uploaded file from coquelicot - Then the coquelicot service should be running - And I should be able to login to coquelicot with password beforebackup123 - And the maximum file size of coquelicot should be 128 - And contents of downloaded sample file should be same as sample local file - -Scenario: Disable coquelicot application - Given the coquelicot application is enabled - When I disable the coquelicot application - Then the coquelicot service should not be running diff --git a/plinth/modules/coquelicot/tests/test_functional.py b/plinth/modules/coquelicot/tests/test_functional.py deleted file mode 100644 index e8c8a08fe..000000000 --- a/plinth/modules/coquelicot/tests/test_functional.py +++ /dev/null @@ -1,126 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Functional, browser based tests for coquelicot app. -""" - -import random -import tempfile - -from pytest_bdd import given, parsers, scenarios, then, when -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.keys import Keys - -from plinth.tests import functional - -scenarios('coquelicot.feature') - - -@given('a sample local file') -def sample_local_file(): - file_path, contents = _create_sample_local_file() - return dict(file_path=file_path, contents=contents) - - -@when(parsers.parse('I modify the maximum file size of coquelicot to {size:d}') - ) -def modify_max_file_size(session_browser, size): - _modify_max_file_size(session_browser, size) - - -@then(parsers.parse('the maximum file size of coquelicot should be {size:d}')) -def assert_max_file_size(session_browser, size): - assert _get_max_file_size(session_browser) == size - - -@when(parsers.parse('I modify the coquelicot upload password to {password:w}')) -def modify_upload_password(session_browser, password): - _modify_upload_password(session_browser, password) - - -@then( - parsers.parse( - 'I should be able to login to coquelicot with password {password:w}')) -def verify_upload_password(session_browser, password): - _verify_upload_password(session_browser, password) - - -@when( - parsers.parse('I upload the sample local file to coquelicot with password ' - '{password:w}')) -def coquelicot_upload_file(session_browser, sample_local_file, password): - url = _upload_file(session_browser, sample_local_file['file_path'], - password) - sample_local_file['upload_url'] = url - - -@when('I download the uploaded file from coquelicot') -def coquelicot_download_file(sample_local_file): - file_path = functional.download_file_outside_browser( - sample_local_file['upload_url']) - sample_local_file['download_path'] = file_path - - -@then('contents of downloaded sample file should be same as sample local file') -def coquelicot_compare_upload_download_files(sample_local_file): - _compare_files(sample_local_file['file_path'], - sample_local_file['download_path']) - - -def _create_sample_local_file(): - """Create a sample file for upload using browser.""" - contents = bytearray(random.getrandbits(8) for _ in range(64)) - with tempfile.NamedTemporaryFile(delete=False) as temp_file: - temp_file.write(contents) - - return temp_file.name, contents - - -def _verify_upload_password(browser, password): - functional.visit(browser, '/coquelicot') - # ensure the password form is scrolled into view - browser.execute_script('window.scrollTo(100, 0)') - browser.find_by_id('upload_password').fill(password) - actions = ActionChains(browser.driver) - actions.send_keys(Keys.RETURN) - actions.perform() - assert functional.eventually(browser.is_element_present_by_css, - args=['div[style*="display: none;"]']) - - -def _upload_file(browser, file_path, password): - """Upload a local file from disk to coquelicot.""" - _verify_upload_password(browser, password) - browser.attach_file('file', file_path) - functional.submit(browser) - assert functional.eventually(browser.is_element_present_by_css, - args=['#content .url']) - url_textarea = browser.find_by_css('#content .url textarea').first - return url_textarea.value - - -def _modify_max_file_size(browser, size): - """Change the maximum file size of coquelicot to the given value""" - functional.visit(browser, '/plinth/apps/coquelicot/') - browser.find_by_id('id_max_file_size').fill(size) - functional.submit(browser, form_class='form-configuration') - - -def _get_max_file_size(browser): - """Get the maximum file size of coquelicot""" - functional.visit(browser, '/plinth/apps/coquelicot/') - return int(browser.find_by_id('id_max_file_size').value) - - -def _modify_upload_password(browser, password): - """Change the upload password for coquelicot to the given value""" - functional.visit(browser, '/plinth/apps/coquelicot/') - browser.find_by_id('id_upload_password').fill(password) - functional.submit(browser, form_class='form-configuration') - - -def _compare_files(file1, file2): - """Assert that the contents of two files are the same.""" - file1_contents = open(file1, 'rb').read() - file2_contents = open(file2, 'rb').read() - - assert file1_contents == file2_contents diff --git a/plinth/modules/coquelicot/urls.py b/plinth/modules/coquelicot/urls.py deleted file mode 100644 index a08863c94..000000000 --- a/plinth/modules/coquelicot/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -URLs for the coquelicot module. -""" - -from django.conf.urls import url - -from .views import CoquelicotAppView - -urlpatterns = [ - url(r'^apps/coquelicot/$', CoquelicotAppView.as_view(), name='index'), -] diff --git a/plinth/modules/coquelicot/views.py b/plinth/modules/coquelicot/views.py deleted file mode 100644 index 46d9aed0e..000000000 --- a/plinth/modules/coquelicot/views.py +++ /dev/null @@ -1,52 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -Plinth views for Coquelicot. -""" - -from django.contrib import messages -from django.utils.translation import ugettext as _ - -from plinth import actions, views -from plinth.errors import ActionError -from plinth.modules.coquelicot import get_current_max_file_size - -from .forms import CoquelicotForm - - -class CoquelicotAppView(views.AppView): - """Serve configuration page.""" - app_id = 'coquelicot' - form_class = CoquelicotForm - - def get_initial(self): - """Return the status of the service to fill in the form.""" - initial = super().get_initial() - initial['max_file_size'] = get_current_max_file_size() - return initial - - def form_valid(self, form): - """Apply the changes submitted in the form.""" - form_data = form.cleaned_data - - if form_data['upload_password']: - try: - actions.superuser_run( - 'coquelicot', ['set-upload-password'], - input=form_data['upload_password'].encode()) - messages.success(self.request, _('Upload password updated')) - except ActionError: - messages.error(self.request, - _('Failed to update upload password')) - - max_file_size = form_data['max_file_size'] - if max_file_size and max_file_size != get_current_max_file_size(): - try: - actions.superuser_run( - 'coquelicot', ['set-max-file-size', - str(max_file_size)]) - messages.success(self.request, _('Maximum file size updated')) - except ActionError: - messages.error(self.request, - _('Failed to update maximum file size')) - - return super().form_valid(form) diff --git a/pytest.ini b/pytest.ini index f557054cf..e95adc2ab 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,7 +5,6 @@ markers = functional backups bind configuration - coquelicot date_and_time deluge dynamicdns diff --git a/setup.py b/setup.py index 1eaead35e..52e46fd7f 100755 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ ENABLED_APPS_PATH = "/etc/plinth/modules-enabled/" DISABLED_APPS_TO_REMOVE = [ 'apps', + 'coquelicot', 'diaspora', 'owncloud', 'system', @@ -110,6 +111,7 @@ class CustomBuild(build): class CustomClean(clean): """Override clean command to clean doc, locales, and egg-info.""" + def run(self): """Execute clean command""" subprocess.check_call(['rm', '-rf', 'Plinth.egg-info/']) @@ -127,6 +129,7 @@ class CustomClean(clean): class CustomInstall(install): """Override install command.""" + def run(self): for app in DISABLED_APPS_TO_REMOVE: file_path = pathlib.Path(ENABLED_APPS_PATH) / app @@ -144,6 +147,7 @@ class CustomInstall(install): class CustomInstallData(install_data): """Override install command to allow directory creation and copy""" + def _run_doc_install(self): """Install documentation""" command = ['make', '-j', '8', '-C', 'doc', 'install'] diff --git a/static/themes/default/icons/coquelicot.png b/static/themes/default/icons/coquelicot.png deleted file mode 100644 index 9bb173f8329a5efa0e5c7cf477329ab3682fcd3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27771 zcmV(004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 S&CD - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -