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. [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
|
||||
|
||||
@ -20,8 +20,6 @@ Newcomers will find easy, self-contained tasks tagged as "beginner".
|
||||
Source code for FreedomBox Service is available from
|
||||
[salsa.debian.org](https://salsa.debian.org/freedombox-team/freedombox).
|
||||
|
||||
|
||||
|
||||
## Development environments: setting up and their usage
|
||||
|
||||
### 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
|
||||
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.
|
||||
* 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.
|
||||
|
||||
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
|
||||
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
|
||||
static/themes/default/icons/macos.svg
|
||||
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