mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
kiwix: Add app for Kiwix offline Wikipedia reader
Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
16993e0b34
commit
34976ac4b0
@ -9,7 +9,7 @@ This document provides reference information for FreedomBox **contribution** hac
|
|||||||
1. [Submitting your changes](#submitting-your-changes)
|
1. [Submitting your changes](#submitting-your-changes)
|
||||||
1. [Other related stuff](#miscelanea)
|
1. [Other related stuff](#miscelanea)
|
||||||
|
|
||||||
It doesn't cover arquitecture, design choices, or other product internals.
|
It doesn't cover architecture, design choices or other product internals.
|
||||||
|
|
||||||
## Picking a task to work on
|
## Picking a task to work on
|
||||||
|
|
||||||
@ -20,8 +20,6 @@ Newcomers will find easy, self-contained tasks tagged as "beginner".
|
|||||||
Source code for FreedomBox Service is available from
|
Source code for FreedomBox Service is available from
|
||||||
[salsa.debian.org](https://salsa.debian.org/freedombox-team/freedombox).
|
[salsa.debian.org](https://salsa.debian.org/freedombox-team/freedombox).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Development environments: setting up and their usage
|
## Development environments: setting up and their usage
|
||||||
|
|
||||||
### Requirements for Development OS
|
### Requirements for Development OS
|
||||||
@ -30,9 +28,9 @@ FreedomBox is built as part of Debian GNU/Linux. However, you don't need to
|
|||||||
install Debian to do development for FreedomBox. FreedomBox development is
|
install Debian to do development for FreedomBox. FreedomBox development is
|
||||||
typically done with a container or a Virtual Machine.
|
typically done with a container or a Virtual Machine.
|
||||||
|
|
||||||
* For running a container, you need systemd containers, Git, Python3 and a
|
* To run a container, you need systemd containers, Git, Python3 and a
|
||||||
sudo-enabled user. This approach is recommended.
|
sudo-enabled user. This approach is recommended.
|
||||||
* For running a VM, you can work on any operating system that can install latest
|
* To run a VM, you can work on any operating system that can install latest
|
||||||
versions of Git, Vagrant and VirtualBox.
|
versions of Git, Vagrant and VirtualBox.
|
||||||
|
|
||||||
In addition:
|
In addition:
|
||||||
|
|||||||
6
debian/copyright
vendored
6
debian/copyright
vendored
@ -132,6 +132,12 @@ Files: plinth/modules/janus/static/icons/janus.png
|
|||||||
Copyright: 2014-2022 Meetecho
|
Copyright: 2014-2022 Meetecho
|
||||||
License: GPL-3 with OpenSSL exception
|
License: GPL-3 with OpenSSL exception
|
||||||
|
|
||||||
|
Files: static/themes/default/icons/kiwix.png
|
||||||
|
static/themes/default/icons/kiwix.svg
|
||||||
|
Copyright: 2020 The other Kiwix guy
|
||||||
|
Comment: https://commons.wikimedia.org/wiki/File:Kiwix_logo_v3.svg
|
||||||
|
License: CC-BY-SA-4.0
|
||||||
|
|
||||||
Files: static/themes/default/icons/macos.png
|
Files: static/themes/default/icons/macos.png
|
||||||
static/themes/default/icons/macos.svg
|
static/themes/default/icons/macos.svg
|
||||||
Copyright: Vectors Market (https://thenounproject.com/vectorsmarket/)
|
Copyright: Vectors Market (https://thenounproject.com/vectorsmarket/)
|
||||||
|
|||||||
121
plinth/modules/kiwix/__init__.py
Normal file
121
plinth/modules/kiwix/__init__.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
FreedomBox app for Kiwix content server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth import app as app_module, frontpage, menu, package
|
||||||
|
from plinth.config import DropinConfigs
|
||||||
|
from plinth.daemon import Daemon
|
||||||
|
from plinth.modules.apache.components import Webserver
|
||||||
|
from plinth.modules.backups.components import BackupRestore
|
||||||
|
from plinth.modules.kiwix import manifest
|
||||||
|
from plinth.modules.firewall.components import Firewall, FirewallLocalProtection
|
||||||
|
from plinth.modules.users.components import UsersAndGroups
|
||||||
|
|
||||||
|
from . import manifest, privileged
|
||||||
|
|
||||||
|
_description = [
|
||||||
|
_('Kiwix is an offline reader for web content. It is software intended '
|
||||||
|
'to make Wikipedia available without using the internet, but it is '
|
||||||
|
'potentially suitable for all HTML content. Kiwix packages are in the '
|
||||||
|
'ZIM file format.'),
|
||||||
|
_('''Kiwix can host various kinds of content:
|
||||||
|
<ul>
|
||||||
|
<li>Offline versions of websites: Wikimedia projects, Stack Exchange</li>
|
||||||
|
<li>Video content: Khan Academy, TED Talks, Crash Course</li>
|
||||||
|
<li>Educational materials: PHET, TED Ed, Vikidia</li>
|
||||||
|
<li>eBooks: Project Gutenberg</li>
|
||||||
|
<li>Magazines: Low-tech Magazine</li>
|
||||||
|
</ul>''')
|
||||||
|
]
|
||||||
|
|
||||||
|
SYSTEM_USER = 'kiwix'
|
||||||
|
|
||||||
|
|
||||||
|
class KiwixApp(app_module.App):
|
||||||
|
"""FreedomBox app for Kiwix."""
|
||||||
|
|
||||||
|
app_id = 'kiwix'
|
||||||
|
|
||||||
|
_version = 1
|
||||||
|
|
||||||
|
DAEMON = 'kiwix-server-freedombox'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Create components for the app."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
groups = {'kiwix': _('Manage Kiwix content server')}
|
||||||
|
|
||||||
|
info = app_module.Info(
|
||||||
|
app_id=self.app_id, version=self._version, name=_('Kiwix'),
|
||||||
|
icon_filename='kiwix', short_description=_('Offline Wikipedia'),
|
||||||
|
description=_description, manual_page='Kiwix',
|
||||||
|
clients=manifest.clients,
|
||||||
|
donation_url='https://www.kiwix.org/en/support-us/')
|
||||||
|
self.add(info)
|
||||||
|
|
||||||
|
menu_item = menu.Menu('menu-kiwix', info.name, info.short_description,
|
||||||
|
info.icon_filename, 'kiwix:index',
|
||||||
|
parent_url_name='apps')
|
||||||
|
self.add(menu_item)
|
||||||
|
|
||||||
|
shortcut = frontpage.Shortcut('shortcut-kiwix', info.name,
|
||||||
|
short_description=info.short_description,
|
||||||
|
icon=info.icon_filename, url='/kiwix',
|
||||||
|
clients=info.clients,
|
||||||
|
login_required=True,
|
||||||
|
allowed_groups=list(groups))
|
||||||
|
self.add(shortcut)
|
||||||
|
|
||||||
|
packages = package.Packages('packages-kiwix', ['kiwix-tools'])
|
||||||
|
self.add(packages)
|
||||||
|
|
||||||
|
dropin_configs = DropinConfigs('dropin-configs-kiwix', [
|
||||||
|
'/etc/apache2/conf-available/kiwix-freedombox.conf',
|
||||||
|
])
|
||||||
|
self.add(dropin_configs)
|
||||||
|
|
||||||
|
firewall = Firewall('firewall-kiwix', info.name,
|
||||||
|
ports=['http', 'https'], is_external=True)
|
||||||
|
self.add(firewall)
|
||||||
|
|
||||||
|
firewall_local_protection = FirewallLocalProtection(
|
||||||
|
'firewall-local-protection-kiwix', ['4201'])
|
||||||
|
self.add(firewall_local_protection)
|
||||||
|
|
||||||
|
webserver = Webserver('webserver-kiwix', 'kiwix-freedombox',
|
||||||
|
urls=['https://{host}/kiwix'])
|
||||||
|
self.add(webserver)
|
||||||
|
|
||||||
|
daemon = Daemon('daemon-kiwix', self.DAEMON,
|
||||||
|
listen_ports=[(4201, 'tcp4')])
|
||||||
|
self.add(daemon)
|
||||||
|
|
||||||
|
users_and_groups = UsersAndGroups('users-and-groups-kiwix',
|
||||||
|
reserved_usernames=['kiwix'],
|
||||||
|
groups=groups)
|
||||||
|
self.add(users_and_groups)
|
||||||
|
|
||||||
|
backup_restore = BackupRestore('backup-restore-kiwix',
|
||||||
|
**manifest.backup)
|
||||||
|
self.add(backup_restore)
|
||||||
|
|
||||||
|
def setup(self, old_version=None):
|
||||||
|
"""Install and configure the app."""
|
||||||
|
super().setup(old_version)
|
||||||
|
if not old_version:
|
||||||
|
self.enable()
|
||||||
|
|
||||||
|
def uninstall(self):
|
||||||
|
"""De-configure and uninstall the app."""
|
||||||
|
super().uninstall()
|
||||||
|
privileged.uninstall()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_file_name(file_name: str):
|
||||||
|
"""Check if the content archive file has a valid extension."""
|
||||||
|
if not file_name.endswith(".zim"):
|
||||||
|
raise ValueError(f"Expected a ZIM file. Found {file_name}")
|
||||||
@ -0,0 +1 @@
|
|||||||
|
plinth.modules.kiwix
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Kiwix Content Server
|
||||||
|
Documentation=man:kiwix-serve(1)
|
||||||
|
After=network.target
|
||||||
|
ConditionPathExists=/usr/bin/kiwix-serve
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER CAP_NET_ADMIN CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_AUDIT_WRITE CAP_KILL CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_LINUX_IMMUTABLE CAP_IPC_LOCK CAP_SYS_CHROOT CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_PACCT CAP_SYS_TTY_CONFIG CAP_SYS_BOOT CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYS_NICE CAP_SYS_RESOURCE
|
||||||
|
DevicePolicy=closed
|
||||||
|
Environment=HOME="/var/lib/kiwix-server-freedombox"
|
||||||
|
Environment=LIBRARY_PATH="/var/lib/kiwix-server-freedombox/library_zim.xml"
|
||||||
|
Environment=ARGS="--library --port=4201 --urlRootLocation=kiwix"
|
||||||
|
ExecStartPre=sh -e -c "mkdir -p $HOME/content; library=$$(ls ${LIBRARY_PATH} 2>/dev/null || true); [ \"x$${library}\" = \"x\" ] && (mkdir -p \"${HOME}\" && echo '<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<library version=\"20110515\">\n</library>' > \"${LIBRARY_PATH}\") || true"
|
||||||
|
ExecStart=sh -e -c "exec /usr/bin/kiwix-serve $ARGS $LIBRARY_PATH"
|
||||||
|
Restart=on-failure
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
DynamicUser=yes
|
||||||
|
LockPersonality=yes
|
||||||
|
NoNewPrivileges=yes
|
||||||
|
PrivateDevices=yes
|
||||||
|
PrivateMounts=yes
|
||||||
|
PrivateTmp=yes
|
||||||
|
PrivateUsers=yes
|
||||||
|
ProtectControlGroups=yes
|
||||||
|
ProtectClock=yes
|
||||||
|
ProtectHome=yes
|
||||||
|
ProtectHostname=yes
|
||||||
|
ProtectKernelLogs=yes
|
||||||
|
ProtectKernelModules=yes
|
||||||
|
ProtectKernelTunables=yes
|
||||||
|
ProtectSystem=strict
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||||
|
RestrictNamespaces=yes
|
||||||
|
RestrictRealtime=yes
|
||||||
|
RestrictSUIDSGID=yes
|
||||||
|
StateDirectory=kiwix-server-freedombox
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
SystemCallFilter=@system-service
|
||||||
|
SystemCallFilter=~@resources
|
||||||
|
SystemCallFilter=~@privileged
|
||||||
|
SystemCallErrorNumber=EPERM
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
##
|
||||||
|
## On all sites, provide kiwix web interface on a path: /kiwix
|
||||||
|
##
|
||||||
|
<Location /kiwix>
|
||||||
|
ProxyPass http://localhost:4201/kiwix
|
||||||
|
</Location>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
plinth.modules.kiwix
|
||||||
25
plinth/modules/kiwix/forms.py
Normal file
25
plinth/modules/kiwix/forms.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Forms for the Kiwix module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.core import validators
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth import cfg
|
||||||
|
|
||||||
|
from .privileged import KIWIX_HOME
|
||||||
|
|
||||||
|
|
||||||
|
class AddContentForm(forms.Form):
|
||||||
|
"""Form to create an empty library."""
|
||||||
|
|
||||||
|
# Would be nice to have a progress bar when uploading large files.
|
||||||
|
file = forms.FileField(
|
||||||
|
label=_('Upload File'), required=True, validators=[
|
||||||
|
validators.FileExtensionValidator(
|
||||||
|
['zim'], _('Content packages have to be in .zim format'))
|
||||||
|
], help_text=_(f'''Uploaded ZIM files will be stored under
|
||||||
|
{KIWIX_HOME}/content on your {cfg.box_name}. If Kiwix fails to add the file,
|
||||||
|
it will be deleted immediately to save disk space.'''))
|
||||||
20
plinth/modules/kiwix/manifest.py
Normal file
20
plinth/modules/kiwix/manifest.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth.clients import validate
|
||||||
|
|
||||||
|
clients = validate([{
|
||||||
|
'name': _('kiwix'),
|
||||||
|
'platforms': [{
|
||||||
|
'type': 'web',
|
||||||
|
'url': '/kiwix'
|
||||||
|
}]
|
||||||
|
}])
|
||||||
|
|
||||||
|
backup = {
|
||||||
|
'data': {
|
||||||
|
'directories': ['/var/lib/private/kiwix-server-freedombox/']
|
||||||
|
},
|
||||||
|
'services': ['kiwix-server-freedombox']
|
||||||
|
}
|
||||||
87
plinth/modules/kiwix/privileged.py
Normal file
87
plinth/modules/kiwix/privileged.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Privileged actions for Kiwix content server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from plinth import action_utils
|
||||||
|
from plinth.actions import privileged
|
||||||
|
from plinth.modules import kiwix
|
||||||
|
|
||||||
|
# Only one central library is supported.
|
||||||
|
KIWIX_HOME = pathlib.Path('/var/lib/kiwix-server-freedombox')
|
||||||
|
LIBRARY_FILE = KIWIX_HOME / 'library_zim.xml'
|
||||||
|
CONTENT_DIR = KIWIX_HOME / 'content'
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def add_content(file_name: str):
|
||||||
|
"""Adds a content package to Kiwix.
|
||||||
|
|
||||||
|
Adding packages is idempotent.
|
||||||
|
|
||||||
|
Users can add content to Kiwix in multiple ways:
|
||||||
|
- Upload a ZIM file
|
||||||
|
- Provide a link to the ZIM file
|
||||||
|
- Provide a magnet link to the ZIM file
|
||||||
|
|
||||||
|
The commandline download manager aria2c is a dependency of kiwix-tools.
|
||||||
|
aria2c is used for both HTTP and Magnet downloads.
|
||||||
|
"""
|
||||||
|
kiwix.validate_file_name(file_name)
|
||||||
|
|
||||||
|
# Moving files to the Kiwix library path ensures that
|
||||||
|
# they can't be removed by other apps or users.
|
||||||
|
zim_file_name = pathlib.Path(file_name).name
|
||||||
|
CONTENT_DIR.mkdir(exist_ok=True)
|
||||||
|
zim_file_dest = str(CONTENT_DIR / zim_file_name)
|
||||||
|
shutil.move(file_name, zim_file_dest)
|
||||||
|
|
||||||
|
_kiwix_manage_add(zim_file_dest)
|
||||||
|
|
||||||
|
|
||||||
|
def _kiwix_manage_add(zim_file: str):
|
||||||
|
subprocess.check_call(['kiwix-manage', LIBRARY_FILE, 'add', zim_file])
|
||||||
|
|
||||||
|
# kiwix-serve doesn't read the library file unless it is restarted.
|
||||||
|
action_utils.service_restart('kiwix-server-freedombox')
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def uninstall():
|
||||||
|
"""Remove all content during uninstall."""
|
||||||
|
shutil.rmtree(str(CONTENT_DIR))
|
||||||
|
LIBRARY_FILE.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def list_content_packages() -> dict[str, dict]:
|
||||||
|
library = ET.parse(LIBRARY_FILE).getroot()
|
||||||
|
|
||||||
|
# Relying on the fact that Python dictionaries maintain order of insertion.
|
||||||
|
return {
|
||||||
|
book.attrib['id']: {
|
||||||
|
'title': book.attrib['title'],
|
||||||
|
'description': book.attrib['description'],
|
||||||
|
# strip '.zim' from the path
|
||||||
|
'path': book.attrib['path'].split('/')[-1][:-4].lower()
|
||||||
|
}
|
||||||
|
for book in library
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def delete_content_package(zim_id: str):
|
||||||
|
library = ET.parse(LIBRARY_FILE).getroot()
|
||||||
|
|
||||||
|
for book in library:
|
||||||
|
if book.attrib['id'] == zim_id:
|
||||||
|
subprocess.check_call(
|
||||||
|
['kiwix-manage', LIBRARY_FILE, 'remove', zim_id])
|
||||||
|
(KIWIX_HOME / book.attrib['path']).unlink()
|
||||||
|
action_utils.service_restart('kiwix-server-freedombox')
|
||||||
|
return
|
||||||
BIN
plinth/modules/kiwix/static/icons/kiwix.png
Normal file
BIN
plinth/modules/kiwix/static/icons/kiwix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
20
plinth/modules/kiwix/static/icons/kiwix.svg
Normal file
20
plinth/modules/kiwix/static/icons/kiwix.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1"
|
||||||
|
id="Layer_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1256 1256"
|
||||||
|
style="enable-background:new 0 0 1256 1256;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#010101;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M1165,764.1c-8.3-36.4-68.5-141.3-191.6-234.4c-22.5-17.1-42.8-31.3-59.7-42.6
|
||||||
|
c24.6-105.3-103.3-232.3-228.1-172.5C596,230.3,496.1,195.9,404.2,197.3c-243.3,3.4-431,256.9-229.1,498.8c0.1,0.1,0.2,0.2,0.4,0.4
|
||||||
|
c3.1,3.7,6.3,7.4,9.5,11.1c13.1,15.7,21.8,29.6,29.2,54.1L274.4,959h-21.3c-19.6,0-35.6,15.9-35.6,35.6h80.8l135.8,64.2
|
||||||
|
c8.4-17.8,0.8-39-16.9-47.3l-35.6-16.8H484c0-19.6-15.9-35.6-35.6-35.6h-92.8c-16.2,0-30.6-10.6-35.3-26.1l-47.7-156.7
|
||||||
|
c-11.9-41.2,15.4-68.1,41.1-71.3c23.4-2.9,35.2,12.2,46.2,48.8l42.4,139h-21.3c-19.6,0-35.6,15.9-35.6,35.6h80.8l135.8,64.2
|
||||||
|
c8.4-17.8,0.8-39-16.9-47.3l-35.6-16.8h75.1c7.6,12.9,16.9,25.1,28,36.1c70,70,183.7,70,253.7,0s70-183.7,0-253.7s-183.7-70-253.7,0
|
||||||
|
c-49.2,49.2-63.9,120-43.9,182h-85c-16.2,0-30.6-10.6-35.3-26.1L378,635.4l12-6.4c167.1-70.1,345.8,55.1,470.2-65.2
|
||||||
|
c0.3-0.3,0.6-0.6,0.8-0.8c15.4-14,30.8-28.3,76.3,0.2c49,30.7,157.1,110.8,206.1,247.8C1143.5,811,1173.2,800.4,1165,764.1z
|
||||||
|
M821.2,460.6c-0.4-18.7-15.6-33.7-34.5-33.7c-19,0-34.5,15.4-34.5,34.5c0,10.4,4.6,19.6,11.8,25.9c-25-4.8-43.8-26.6-43.8-52.9
|
||||||
|
c0-29.8,24.1-53.9,53.9-53.9c29.8,0,53.9,24.1,53.9,53.9C828,443.9,825.5,452.8,821.2,460.6z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
58
plinth/modules/kiwix/templates/add-content-package.html
Normal file
58
plinth/modules/kiwix/templates/add-content-package.html
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load bootstrap %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
You can <a href="https://library.kiwix.org"
|
||||||
|
target="_blank" rel="noopener noreferrer">download</a>
|
||||||
|
content packages from the Kiwix project or
|
||||||
|
<a href="https://openzim.org/wiki/Build_your_ZIM_file"
|
||||||
|
target="_blank" rel="noopener noreferrer">create</a> your own.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
Content packages can be added in the following ways:
|
||||||
|
<ul>
|
||||||
|
<li>upload a ZIM file</li>
|
||||||
|
<!-- <li>upload a BitTorrent file to download a ZIM file</li> -->
|
||||||
|
<!-- <li>provide a download link to a ZIM file</li> -->
|
||||||
|
<!-- <li>provide a magnet link to download a ZIM file</li> -->
|
||||||
|
<!-- <li>provide a file path to a ZIM file</li> -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- TODO Add this somewhere -->
|
||||||
|
<!-- The Kiwix project recommends using BitTorrent for downloads. -->
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if max_filesize %}
|
||||||
|
<div class="alert alert-warning" role="alert">
|
||||||
|
<!-- <span class="fa fa-exclamation-triangle" aria-hidden="true"></span> -->
|
||||||
|
<!-- <span class="sr-only">{% trans "Caution:" %}</span> -->
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You have {{ max_filesize }} of free disk space available.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="form form-kiwix" enctype="multipart/form-data" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{{ form|bootstrap }}
|
||||||
|
|
||||||
|
<input type="submit" class="btn btn-primary"
|
||||||
|
value="{% trans "Upload file" %}"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
29
plinth/modules/kiwix/templates/delete-content-package.html
Normal file
29
plinth/modules/kiwix/templates/delete-content-package.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Delete content package <em>{{ name }}</em>
|
||||||
|
{% endblocktrans %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Delete this package permanently? You may add it back later if you have a copy of the ZIM file.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="form form-delete" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<input type="submit" class="btn btn-md btn-danger"
|
||||||
|
value="{% blocktrans %}Delete {{ name }}{% endblocktrans %}"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
48
plinth/modules/kiwix/templates/kiwix.html
Normal file
48
plinth/modules/kiwix/templates/kiwix.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{% extends "app.html" %}
|
||||||
|
{% comment %}
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block configuration %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
<h3>{% trans "Manage Content" %}</h3>
|
||||||
|
|
||||||
|
<div class="btn-toolbar">
|
||||||
|
<a href="{% url 'kiwix:add-content' %}" class="btn btn-default"
|
||||||
|
role="button" title="{% trans 'Add a content package' %}">
|
||||||
|
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||||
|
{% trans 'Add' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{% if not packages %}
|
||||||
|
<p>{% trans 'No content available.' %}</p>
|
||||||
|
{% else %}
|
||||||
|
<div id="kiwix-packages" class="list-group list-group-two-column">
|
||||||
|
{% for id, package in packages.items %}
|
||||||
|
<div class="list-group-item">
|
||||||
|
<a id="{{ id }}"
|
||||||
|
class="primary"
|
||||||
|
href="/kiwix/viewer#{{ package.path }}"
|
||||||
|
title="{{ package.description }}">
|
||||||
|
{{ package.title }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'kiwix:delete-content' id %}"
|
||||||
|
class="btn btn-default btn-sm secondary" role="button"
|
||||||
|
title="{% blocktrans with title=package.title %}Delete package {{ title }}{% endblocktrans %}">
|
||||||
|
<span class="fa fa-trash-o" aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
0
plinth/modules/kiwix/tests/__init__.py
Normal file
0
plinth/modules/kiwix/tests/__init__.py
Normal file
BIN
plinth/modules/kiwix/tests/data/FreedomBox.zim
Normal file
BIN
plinth/modules/kiwix/tests/data/FreedomBox.zim
Normal file
Binary file not shown.
1
plinth/modules/kiwix/tests/data/invalid.zim
Normal file
1
plinth/modules/kiwix/tests/data/invalid.zim
Normal file
@ -0,0 +1 @@
|
|||||||
|
Nothing to see here.
|
||||||
4
plinth/modules/kiwix/tests/data/sample_library_zim.xml
Normal file
4
plinth/modules/kiwix/tests/data/sample_library_zim.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<library version="20110515">
|
||||||
|
<book id="bc4f8cdf-5626-2b13-3860-0033deddfbea" path="content/FreedomBox.zim" title="FreedomBox" description="A sample content archive" language="eng" creator="FreedomBox" publisher="FreedomBox" tags="_ftindex:yes;_ftindex:yes;_pictures:yes;_videos:yes;_details:yes" date="2022-11-29" articleCount="1" mediaCount="1" size="88" />
|
||||||
|
</library>
|
||||||
98
plinth/modules/kiwix/tests/test_functional.py
Normal file
98
plinth/modules/kiwix/tests/test_functional.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Functional, browser based tests for Kiwix app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pkg_resources
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
from plinth.modules.kiwix.tests.test_privileged import ZIM_ID
|
||||||
|
|
||||||
|
from plinth.tests import functional
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.apps, pytest.mark.sso, pytest.mark.kiwix]
|
||||||
|
|
||||||
|
_default_url = functional.config['DEFAULT']['url']
|
||||||
|
|
||||||
|
ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea'
|
||||||
|
|
||||||
|
|
||||||
|
class TestKiwixApp(functional.BaseAppTests):
|
||||||
|
app_name = 'kiwix'
|
||||||
|
has_service = True
|
||||||
|
has_web = True
|
||||||
|
|
||||||
|
def test_add_delete_content_package(self, session_browser):
|
||||||
|
"""Test adding/deleting content package to the library."""
|
||||||
|
functional.app_enable(session_browser, 'kiwix')
|
||||||
|
|
||||||
|
zim_file = pkg_resources.resource_filename(
|
||||||
|
'plinth.modules.kiwix.tests', 'data/FreedomBox.zim')
|
||||||
|
_add_content_package(session_browser, zim_file)
|
||||||
|
assert _is_content_package_listed(session_browser, 'freedombox')
|
||||||
|
assert _is_content_package_available(session_browser, 'FreedomBox')
|
||||||
|
|
||||||
|
_delete_content_package(session_browser, ZIM_ID)
|
||||||
|
assert not _is_content_package_listed(session_browser, 'freedombox')
|
||||||
|
assert not _is_content_package_available(session_browser, 'FreedomBox')
|
||||||
|
|
||||||
|
@pytest.mark.backups
|
||||||
|
def test_backup_restore(self, session_browser):
|
||||||
|
"""Test backing up and restoring."""
|
||||||
|
functional.app_enable(session_browser, 'kiwix')
|
||||||
|
|
||||||
|
zim_file = pkg_resources.resource_filename(
|
||||||
|
'plinth.modules.kiwix.tests', 'data/FreedomBox.zim')
|
||||||
|
_add_content_package(session_browser, zim_file)
|
||||||
|
functional.backup_create(session_browser, 'kiwix', 'test_kiwix')
|
||||||
|
|
||||||
|
_delete_content_package(session_browser, ZIM_ID)
|
||||||
|
functional.backup_restore(session_browser, 'kiwix', 'test_kiwix')
|
||||||
|
|
||||||
|
assert _is_content_package_listed(session_browser, 'freedombox')
|
||||||
|
assert _is_content_package_available(session_browser, 'FreedomBox')
|
||||||
|
|
||||||
|
def test_add_invalid_zim_file(self, session_browser):
|
||||||
|
"""Test handling of invalid zim files."""
|
||||||
|
functional.app_enable(session_browser, 'kiwix')
|
||||||
|
|
||||||
|
zim_file = pkg_resources.resource_filename(
|
||||||
|
'plinth.modules.kiwix.tests', 'data/invalid.zim')
|
||||||
|
_add_content_package(session_browser, zim_file)
|
||||||
|
|
||||||
|
assert not _is_content_package_listed(session_browser, 'invalid')
|
||||||
|
|
||||||
|
|
||||||
|
def _add_content_package(browser, file_name):
|
||||||
|
browser.links.find_by_href('/plinth/apps/kiwix/content/add/').first.click()
|
||||||
|
browser.attach_file('kiwix-file', file_name)
|
||||||
|
functional.submit(browser, form_class='form-kiwix')
|
||||||
|
|
||||||
|
|
||||||
|
def _is_content_package_available(browser, title) -> bool:
|
||||||
|
browser.visit(f'{_default_url}/kiwix')
|
||||||
|
sleep(1) # Allow time for the books to appear
|
||||||
|
titles = browser.find_by_id('book__title')
|
||||||
|
print(len(titles))
|
||||||
|
print([title.value for title in titles])
|
||||||
|
return any(map(lambda e: e.value == title, titles))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_content_package_listed(browser, name) -> bool:
|
||||||
|
functional.nav_to_module(browser, 'kiwix')
|
||||||
|
links_found = browser.links.find_by_partial_href(f'/kiwix/viewer#{name}')
|
||||||
|
return len(links_found) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_content_package(browser, zim_id):
|
||||||
|
functional.nav_to_module(browser, 'kiwix')
|
||||||
|
link = browser.links.find_by_href(
|
||||||
|
f'/plinth/apps/kiwix/content/{zim_id}/delete/')
|
||||||
|
if not link:
|
||||||
|
raise ValueError('ZIM file missing!')
|
||||||
|
link.first.click()
|
||||||
|
functional.submit(browser, form_class='form-delete')
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Add test to check that Kiwix can be viewed without logging in.
|
||||||
86
plinth/modules/kiwix/tests/test_privileged.py
Normal file
86
plinth/modules/kiwix/tests/test_privileged.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Test module for Kiwix actions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import pkg_resources
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plinth.modules.kiwix import privileged
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures('mock_privileged')
|
||||||
|
privileged_modules_to_mock = ['plinth.modules.kiwix.privileged']
|
||||||
|
|
||||||
|
EMPTY_LIBRARY_CONTENTS = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<library version="20110515">
|
||||||
|
</library>'''
|
||||||
|
|
||||||
|
ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fixture_kiwix_home(tmpdir):
|
||||||
|
"""Set Kiwix home to a new temporary directory
|
||||||
|
initialized with an empty library file."""
|
||||||
|
privileged.KIWIX_HOME = pathlib.Path(str(tmpdir / 'kiwix'))
|
||||||
|
privileged.KIWIX_HOME.mkdir()
|
||||||
|
privileged.CONTENT_DIR = privileged.KIWIX_HOME / 'content'
|
||||||
|
privileged.CONTENT_DIR.mkdir()
|
||||||
|
privileged.LIBRARY_FILE = privileged.KIWIX_HOME / 'library_zim.xml'
|
||||||
|
with open(privileged.LIBRARY_FILE, 'w', encoding='utf_8') as library_file:
|
||||||
|
library_file.write(EMPTY_LIBRARY_CONTENTS)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fixture_patch():
|
||||||
|
"""Patch some underlying methods."""
|
||||||
|
with patch('subprocess.check_call'), patch('subprocess.run'):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_content(tmpdir):
|
||||||
|
"""Test adding a content package to Kiwix."""
|
||||||
|
some_dir = tmpdir / 'some' / 'dir'
|
||||||
|
pathlib.Path(some_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
zim_file_name = 'wikipedia_en_all_maxi_2022-05.zim'
|
||||||
|
orig_file = some_dir / zim_file_name
|
||||||
|
pathlib.Path(orig_file).touch()
|
||||||
|
|
||||||
|
privileged.add_content(str(orig_file))
|
||||||
|
assert (privileged.KIWIX_HOME / 'content' / zim_file_name).exists()
|
||||||
|
assert not orig_file.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_content_packages():
|
||||||
|
"""Test listing the content packages from a library file."""
|
||||||
|
privileged.LIBRARY_FILE = pkg_resources.resource_filename(
|
||||||
|
'plinth.modules.kiwix.tests', 'data/sample_library_zim.xml')
|
||||||
|
content_packages = privileged.list_content_packages()
|
||||||
|
assert content_packages[ZIM_ID] == {
|
||||||
|
'title': 'FreedomBox',
|
||||||
|
'description': 'A sample content archive',
|
||||||
|
'path': 'freedombox'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_content_package():
|
||||||
|
"""Test deleting one content package."""
|
||||||
|
sample_library_file = pkg_resources.resource_filename(
|
||||||
|
'plinth.modules.kiwix.tests', 'data/sample_library_zim.xml')
|
||||||
|
|
||||||
|
with open(sample_library_file, 'r',
|
||||||
|
encoding='utf_8') as sample_library_file:
|
||||||
|
with open(privileged.LIBRARY_FILE, 'w',
|
||||||
|
encoding='utf_8') as library_file:
|
||||||
|
library_file.write(sample_library_file.read())
|
||||||
|
|
||||||
|
zim_file = privileged.CONTENT_DIR / 'FreedomBox.zim'
|
||||||
|
zim_file.touch()
|
||||||
|
|
||||||
|
privileged.delete_content_package(ZIM_ID)
|
||||||
|
|
||||||
|
assert not zim_file.exists()
|
||||||
|
# Cannot check that the book is removed from library_zim.xml
|
||||||
21
plinth/modules/kiwix/tests/test_validations.py
Normal file
21
plinth/modules/kiwix/tests/test_validations.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Test module for Kiwix validations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from plinth.modules import kiwix
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidations(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_add_file_with_invalid_extension(self):
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
lambda: kiwix.validate_file_name('wikipedia.zip'))
|
||||||
|
|
||||||
|
# We don't support the legacy format of split zim files.
|
||||||
|
self.assertRaises(
|
||||||
|
ValueError, lambda: kiwix.validate_file_name(
|
||||||
|
'wikipedia_en_all_maxi_2022-05.zima'))
|
||||||
|
|
||||||
|
kiwix.validate_file_name('wikipedia_en_all_maxi_2022-05.zim')
|
||||||
147
plinth/modules/kiwix/tests/test_views.py
Normal file
147
plinth/modules/kiwix/tests/test_views.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Test module for Kiwix views.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from plinth import module_loader
|
||||||
|
from django import urls
|
||||||
|
from unittest.mock import call, patch
|
||||||
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||||
|
from django.http.response import Http404
|
||||||
|
from django.test.client import encode_multipart, RequestFactory
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from plinth.modules.kiwix import views
|
||||||
|
|
||||||
|
# For all tests, use plinth.urls instead of urls configured for testing
|
||||||
|
pytestmark = pytest.mark.urls('plinth.urls')
|
||||||
|
|
||||||
|
ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope='module')
|
||||||
|
def fixture_kiwix_urls():
|
||||||
|
"""Make sure kiwix app's URLs are part of plinth.urls."""
|
||||||
|
with patch('plinth.module_loader._modules_to_load', new=[]) as modules, \
|
||||||
|
patch('plinth.urls.urlpatterns', new=[]):
|
||||||
|
modules.append('plinth.modules.kiwix')
|
||||||
|
module_loader.include_urls()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def make_request(request, view, **kwargs):
|
||||||
|
"""Make request with a message storage."""
|
||||||
|
setattr(request, 'session', 'session')
|
||||||
|
messages = FallbackStorage(request)
|
||||||
|
setattr(request, '_messages', messages)
|
||||||
|
response = view(request, **kwargs)
|
||||||
|
|
||||||
|
return response, messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def kiwix_patch():
|
||||||
|
"""Patch kiwix methods."""
|
||||||
|
with patch('plinth.modules.kiwix.privileged.list_content_packages'
|
||||||
|
) as list_libraries:
|
||||||
|
list_libraries.return_value = {
|
||||||
|
ZIM_ID: {
|
||||||
|
'title': 'TestExistingContentPackage',
|
||||||
|
'description': 'A sample content package',
|
||||||
|
'path': 'test_existing_content_package'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def storage_info_patch():
|
||||||
|
"""Patch storage info method."""
|
||||||
|
with patch('plinth.modules.storage.get_mount_info') as get_mount_info:
|
||||||
|
get_mount_info.return_value = {'free_bytes': 1000000000000}
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.modules.kiwix.privileged.add_content')
|
||||||
|
def test_add_content_package(add_content, rf):
|
||||||
|
"""Test that adding content view works."""
|
||||||
|
with open('plinth/modules/kiwix/tests/data/FreedomBox.zim',
|
||||||
|
'rb') as zim_file:
|
||||||
|
post_data = {
|
||||||
|
'kiwix-file': zim_file,
|
||||||
|
}
|
||||||
|
post_data = encode_multipart('BoUnDaRyStRiNg', post_data)
|
||||||
|
request = rf.post(
|
||||||
|
'', data=post_data, content_type='multipart/form-data; '
|
||||||
|
'boundary=BoUnDaRyStRiNg')
|
||||||
|
response, messages = make_request(request,
|
||||||
|
views.AddContentView.as_view())
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == urls.reverse('kiwix:index')
|
||||||
|
assert list(messages)[0].message == 'Content package added.'
|
||||||
|
add_content.assert_has_calls([call('/tmp/FreedomBox.zim')])
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.modules.kiwix.privileged.add_content')
|
||||||
|
def test_add_content_package_failed(add_content, rf):
|
||||||
|
"""Test that adding content package fails in case of an error."""
|
||||||
|
add_content.side_effect = RuntimeError('TestError')
|
||||||
|
with open('plinth/modules/kiwix/tests/data/FreedomBox.zim',
|
||||||
|
'rb') as zim_file:
|
||||||
|
post_data = {
|
||||||
|
'kiwix-file': zim_file,
|
||||||
|
}
|
||||||
|
post_data = encode_multipart('BoUnDaRyStRiNg', post_data)
|
||||||
|
request = rf.post(
|
||||||
|
'', data=post_data, content_type='multipart/form-data; '
|
||||||
|
'boundary=BoUnDaRyStRiNg')
|
||||||
|
response, messages = make_request(request,
|
||||||
|
views.AddContentView.as_view())
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == urls.reverse('kiwix:index')
|
||||||
|
assert list(messages)[0].message == \
|
||||||
|
'Failed to add content package.'
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.app.App.get')
|
||||||
|
def test_delete_package_confirmation_view(_app, rf):
|
||||||
|
"""Test that deleting package confirmation shows correct title."""
|
||||||
|
response, _ = make_request(rf.get(''), views.delete_content, zim_id=ZIM_ID)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.context_data['name'] == 'TestExistingContentPackage'
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.modules.kiwix.privileged.delete_content_package')
|
||||||
|
@patch('plinth.app.App.get')
|
||||||
|
def test_delete_content_package(_app, delete_content_package, rf):
|
||||||
|
"""Test that deleting a content package works."""
|
||||||
|
response, messages = make_request(rf.post(''), views.delete_content,
|
||||||
|
zim_id=ZIM_ID)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == urls.reverse('kiwix:index')
|
||||||
|
assert list(messages)[0].message == 'TestExistingContentPackage deleted.'
|
||||||
|
delete_content_package.assert_has_calls([call(ZIM_ID)])
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.modules.kiwix.privileged.delete_content_package')
|
||||||
|
def test_delete_content_package_error(delete_content_package, rf):
|
||||||
|
"""Test that deleting a content package shows an error when operation fails."""
|
||||||
|
delete_content_package.side_effect = ValueError('TestError')
|
||||||
|
response, messages = make_request(rf.post(''), views.delete_content,
|
||||||
|
zim_id=ZIM_ID)
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == urls.reverse('kiwix:index')
|
||||||
|
assert list(messages)[0].message == \
|
||||||
|
'Could not delete TestExistingContentPackage: TestError'
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_content_package_non_existing(rf):
|
||||||
|
"""Test that deleting a content package shows error when operation fails."""
|
||||||
|
with pytest.raises(Http404):
|
||||||
|
make_request(rf.post(''), views.delete_content,
|
||||||
|
zim_id='NonExistentZimId')
|
||||||
|
|
||||||
|
with pytest.raises(Http404):
|
||||||
|
make_request(rf.get(''), views.delete_content,
|
||||||
|
zim_id='NonExistentZimId')
|
||||||
16
plinth/modules/kiwix/urls.py
Normal file
16
plinth/modules/kiwix/urls.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
URLs for the Kiwix module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.urls import re_path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r'^apps/kiwix/$', views.KiwixAppView.as_view(), name='index'),
|
||||||
|
re_path(r'^apps/kiwix/content/add/$', views.AddContentView.as_view(),
|
||||||
|
name='add-content'),
|
||||||
|
re_path(r'^apps/kiwix/content/(?P<zim_id>[a-zA-Z0-9-]+)/delete/$',
|
||||||
|
views.delete_content, name='delete-content'),
|
||||||
|
]
|
||||||
105
plinth/modules/kiwix/views.py
Normal file
105
plinth/modules/kiwix/views.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""
|
||||||
|
Views for the Kiwix module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
|
from plinth import app as app_module
|
||||||
|
from plinth import views
|
||||||
|
from plinth.errors import PlinthError
|
||||||
|
from plinth.modules import storage
|
||||||
|
from plinth.modules.kiwix import privileged
|
||||||
|
|
||||||
|
from . import forms
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KiwixAppView(views.AppView):
|
||||||
|
"""Serve configuration form."""
|
||||||
|
app_id = 'kiwix'
|
||||||
|
template_name = 'kiwix.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Return additional context for rendering the template."""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['packages'] = privileged.list_content_packages()
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class AddContentView(SuccessMessageMixin, FormView):
|
||||||
|
"""View to add content in the form of ZIM files."""
|
||||||
|
form_class = forms.AddContentForm
|
||||||
|
prefix = 'kiwix'
|
||||||
|
template_name = 'add-content-package.html'
|
||||||
|
success_url = reverse_lazy('kiwix:index')
|
||||||
|
success_message = _('Content package added.')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Return additional context for rendering the template."""
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['title'] = _('Add a new content package')
|
||||||
|
|
||||||
|
# TODO The following is almost duplicated in backups/views.py
|
||||||
|
try:
|
||||||
|
mount_info = storage.get_mount_info('/')
|
||||||
|
except PlinthError as exception:
|
||||||
|
logger.exception(
|
||||||
|
'Error getting information about root partition: %s',
|
||||||
|
exception)
|
||||||
|
else:
|
||||||
|
context['max_filesize'] = storage.format_bytes(
|
||||||
|
mount_info['free_bytes'])
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Store the uploaded file."""
|
||||||
|
multipart_file = self.request.FILES['kiwix-file']
|
||||||
|
zim_file_name = '/tmp/' + multipart_file.name
|
||||||
|
with open(zim_file_name, 'wb+') as zim_file:
|
||||||
|
for chunk in multipart_file.chunks():
|
||||||
|
zim_file.write(chunk)
|
||||||
|
|
||||||
|
try:
|
||||||
|
privileged.add_content(zim_file_name)
|
||||||
|
except Exception:
|
||||||
|
messages.error(self.request, _('Failed to add content package.'))
|
||||||
|
return redirect(reverse_lazy('kiwix:index'))
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_content(request, zim_id):
|
||||||
|
"""View to delete a library."""
|
||||||
|
packages = privileged.list_content_packages()
|
||||||
|
if zim_id not in packages:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
name = packages[zim_id]['title']
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
privileged.delete_content_package(zim_id)
|
||||||
|
messages.success(request, _(f'{name} deleted.'))
|
||||||
|
except Exception as error:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_('Could not delete {name}: {error}').format(
|
||||||
|
name=name, error=error))
|
||||||
|
return redirect(reverse_lazy('kiwix:index'))
|
||||||
|
|
||||||
|
return TemplateResponse(request, 'delete-content-package.html', {
|
||||||
|
'title': app_module.App.get('kiwix').info.name,
|
||||||
|
'name': name
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user