kiwix: Fix various issues after review

- Fix icon paths in copyright file.

- Minor refactoring.

- Add Kiwix library link to app page as well as users may want to see the
  content available before installing the app.

- Consolidate terminology to 'content package' for UI and just 'package'
internally.

- Drop unused SYSTEM_USER constant.

- Simplify the ExecStart= in systemd service file.

- Fix incorrect i18n caused by non-lazy formatting of strings.

- Confirm that xml parsing is not vulnerable as expat library of required
version is used in Debian bookworm.

- Don't start the kiwix daemon when managing library if app is disabled.

- Ignore errors when removing files during uninstallation.

- Handle failures more gracefully when library XML file does not have required
attributes.

- Update SVG/PNG icons to adhere to FreedomBox guidelines.

- Trim block translations in templates.

- Drop comments/deadcode inside translation strings.

- Drop a comment inside add content page that only makes sense with multiple
methods for adding content.

- tests: Don't use pkg_resources library as it is deprecated. We can use
importlib.resources library in future if we run tests on zip installations.

- Fix potential security issues while writing file to tmp directory.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Sunil Mohan Adapa 2023-10-17 13:21:04 -07:00
parent 34976ac4b0
commit cfdf92cf0d
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
19 changed files with 301 additions and 289 deletions

4
debian/copyright vendored
View File

@ -132,8 +132,8 @@ 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 Files: plinth/modules/kiwix/static/icons/kiwix.png
static/themes/default/icons/kiwix.svg plinth/modules/kiwix/static/icons/kiwix.svg
Copyright: 2020 The other Kiwix guy Copyright: 2020 The other Kiwix guy
Comment: https://commons.wikimedia.org/wiki/File:Kiwix_logo_v3.svg Comment: https://commons.wikimedia.org/wiki/File:Kiwix_logo_v3.svg
License: CC-BY-SA-4.0 License: CC-BY-SA-4.0

View File

@ -5,13 +5,14 @@ FreedomBox app for Kiwix content server.
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from plinth import app as app_module, frontpage, menu, package from plinth import app as app_module
from plinth import frontpage, menu, package
from plinth.config import DropinConfigs from plinth.config import DropinConfigs
from plinth.daemon import Daemon from plinth.daemon import Daemon
from plinth.modules.apache.components import Webserver from plinth.modules.apache.components import Webserver
from plinth.modules.backups.components import BackupRestore from plinth.modules.backups.components import BackupRestore
from plinth.modules.kiwix import manifest from plinth.modules.firewall.components import (Firewall,
from plinth.modules.firewall.components import Firewall, FirewallLocalProtection FirewallLocalProtection)
from plinth.modules.users.components import UsersAndGroups from plinth.modules.users.components import UsersAndGroups
from . import manifest, privileged from . import manifest, privileged
@ -28,11 +29,13 @@ _description = [
<li>Educational materials: PHET, TED Ed, Vikidia</li> <li>Educational materials: PHET, TED Ed, Vikidia</li>
<li>eBooks: Project Gutenberg</li> <li>eBooks: Project Gutenberg</li>
<li>Magazines: Low-tech Magazine</li> <li>Magazines: Low-tech Magazine</li>
</ul>''') </ul>'''),
_('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.'),
] ]
SYSTEM_USER = 'kiwix'
class KiwixApp(app_module.App): class KiwixApp(app_module.App):
"""FreedomBox app for Kiwix.""" """FreedomBox app for Kiwix."""
@ -116,6 +119,6 @@ class KiwixApp(app_module.App):
def validate_file_name(file_name: str): def validate_file_name(file_name: str):
"""Check if the content archive file has a valid extension.""" """Check if the content package file has a valid extension."""
if not file_name.endswith(".zim"): if not file_name.endswith('.zim'):
raise ValueError(f"Expected a ZIM file. Found {file_name}") raise ValueError(f'Expected a ZIM file. Found {file_name}')

View File

@ -13,7 +13,7 @@ Environment=HOME="/var/lib/kiwix-server-freedombox"
Environment=LIBRARY_PATH="/var/lib/kiwix-server-freedombox/library_zim.xml" Environment=LIBRARY_PATH="/var/lib/kiwix-server-freedombox/library_zim.xml"
Environment=ARGS="--library --port=4201 --urlRootLocation=kiwix" 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" 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" ExecStart=/usr/bin/kiwix-serve $ARGS $LIBRARY_PATH
Restart=on-failure Restart=on-failure
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID
DynamicUser=yes DynamicUser=yes

View File

@ -8,18 +8,21 @@ from django.core import validators
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from plinth import cfg from plinth import cfg
from plinth.utils import format_lazy
from .privileged import KIWIX_HOME from .privileged import KIWIX_HOME
class AddContentForm(forms.Form): class AddPackageForm(forms.Form):
"""Form to create an empty library.""" """Form to upload a content package to a library."""
# Would be nice to have a progress bar when uploading large files. # Would be nice to have a progress bar when uploading large files.
file = forms.FileField( file = forms.FileField(
label=_('Upload File'), required=True, validators=[ label=_('Upload File'), required=True, validators=[
validators.FileExtensionValidator( validators.FileExtensionValidator(
['zim'], _('Content packages have to be in .zim format')) ['zim'], _('Content packages have to be in .zim format'))
], help_text=_(f'''Uploaded ZIM files will be stored under ], help_text=format_lazy(
{KIWIX_HOME}/content on your {cfg.box_name}. If Kiwix fails to add the file, _('Uploaded ZIM files will be stored under {kiwix_home}/content '
it will be deleted immediately to save disk space.''')) 'on your {box_name}. If Kiwix fails to add the file, it will be '
'deleted immediately to save disk space.'),
box_name=_(cfg.box_name), kiwix_home=KIWIX_HOME))

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from plinth.clients import validate from plinth.clients import validate
clients = validate([{ clients = validate([{
'name': _('kiwix'), 'name': _('Kiwix'),
'platforms': [{ 'platforms': [{
'type': 'web', 'type': 'web',
'url': '/kiwix' 'url': '/kiwix'

View File

@ -3,10 +3,11 @@
Privileged actions for Kiwix content server. Privileged actions for Kiwix content server.
""" """
import subprocess import os
import pathlib import pathlib
import shutil import shutil
import xml.etree.ElementTree as ET import subprocess
from xml.etree import ElementTree
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -19,19 +20,19 @@ CONTENT_DIR = KIWIX_HOME / 'content'
@privileged @privileged
def add_content(file_name: str): def add_package(file_name: str):
"""Adds a content package to Kiwix. """Adds a content package to Kiwix.
Adding packages is idempotent. Adding packages is idempotent.
Users can add content to Kiwix in multiple ways: Users can add content to Kiwix in multiple ways:
- Upload a ZIM file - Upload a ZIM file
- Provide a link to the ZIM file - Provide a link to the ZIM file
- Provide a magnet link to the ZIM file - Provide a magnet link to the ZIM file
The commandline download manager aria2c is a dependency of kiwix-tools. The commandline download manager aria2c is a dependency of kiwix-tools.
aria2c is used for both HTTP and Magnet downloads. aria2c is used for both HTTP and Magnet downloads.
""" """
kiwix.validate_file_name(file_name) kiwix.validate_file_name(file_name)
# Moving files to the Kiwix library path ensures that # Moving files to the Kiwix library path ensures that
@ -39,6 +40,8 @@ def add_content(file_name: str):
zim_file_name = pathlib.Path(file_name).name zim_file_name = pathlib.Path(file_name).name
CONTENT_DIR.mkdir(exist_ok=True) CONTENT_DIR.mkdir(exist_ok=True)
zim_file_dest = str(CONTENT_DIR / zim_file_name) zim_file_dest = str(CONTENT_DIR / zim_file_name)
shutil.chown(file_name, 'root', 'root')
os.chmod(file_name, 0o644)
shutil.move(file_name, zim_file_dest) shutil.move(file_name, zim_file_dest)
_kiwix_manage_add(zim_file_dest) _kiwix_manage_add(zim_file_dest)
@ -48,40 +51,51 @@ def _kiwix_manage_add(zim_file: str):
subprocess.check_call(['kiwix-manage', LIBRARY_FILE, 'add', zim_file]) subprocess.check_call(['kiwix-manage', LIBRARY_FILE, 'add', zim_file])
# kiwix-serve doesn't read the library file unless it is restarted. # kiwix-serve doesn't read the library file unless it is restarted.
action_utils.service_restart('kiwix-server-freedombox') action_utils.service_try_restart('kiwix-server-freedombox')
@privileged @privileged
def uninstall(): def uninstall() -> None:
"""Remove all content during uninstall.""" """Remove all content during uninstall."""
shutil.rmtree(str(CONTENT_DIR)) shutil.rmtree(str(CONTENT_DIR), ignore_errors=True)
LIBRARY_FILE.unlink() LIBRARY_FILE.unlink(missing_ok=True)
@privileged @privileged
def list_content_packages() -> dict[str, dict]: def list_packages() -> dict[str, dict[str, str]]:
library = ET.parse(LIBRARY_FILE).getroot() """Return the list of content packages configured in library file."""
library = ElementTree.parse(LIBRARY_FILE).getroot()
# Relying on the fact that Python dictionaries maintain order of insertion. books = {}
return { for book in library:
book.attrib['id']: { path = book.attrib['path'].split('/')[-1]
'title': book.attrib['title'], path = path.removesuffix('.zim').lower() # Strip '.zim' from the path
'description': book.attrib['description'], try:
# strip '.zim' from the path books[book.attrib['id']] = {
'path': book.attrib['path'].split('/')[-1][:-4].lower() 'title': book.attrib['title'],
} 'description': book.attrib['description'],
for book in library 'path': path
} }
except KeyError:
pass # Ignore entries that don't have expected properties
return books
@privileged @privileged
def delete_content_package(zim_id: str): def delete_package(zim_id: str):
library = ET.parse(LIBRARY_FILE).getroot() """Remove a content package from the library file."""
library = ElementTree.parse(LIBRARY_FILE).getroot()
for book in library: for book in library:
if book.attrib['id'] == zim_id: try:
if book.attrib['id'] != zim_id:
continue
subprocess.check_call( subprocess.check_call(
['kiwix-manage', LIBRARY_FILE, 'remove', zim_id]) ['kiwix-manage', LIBRARY_FILE, 'remove', zim_id])
(KIWIX_HOME / book.attrib['path']).unlink() (KIWIX_HOME / book.attrib['path']).unlink()
action_utils.service_restart('kiwix-server-freedombox') action_utils.service_try_restart('kiwix-server-freedombox')
return return
except KeyError: # Expected properties not found on elements
pass

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,20 +1,48 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- 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" <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" version="1.1"
style="enable-background:new 0 0 1256 1256;" xml:space="preserve"> id="Layer_1"
<style type="text/css"> x="0px"
y="0px"
viewBox="0 0 512 512"
xml:space="preserve"
sodipodi:docname="kiwix.svg"
width="512"
height="512"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs9" /><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.59282121"
inkscape:cx="448.70189"
inkscape:cy="609.79599"
inkscape:window-width="1504"
inkscape:window-height="1282"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style2">
.st0{fill:#010101;} .st0{fill:#010101;}
</style> </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 <path
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 class="st0"
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 d="m 511.31939,320.69463 c -3.94623,-17.30636 -32.56828,-67.181 -91.09611,-111.44534 -10.69761,-8.13018 -20.34923,-14.88156 -28.38433,-20.25414 11.69606,-50.06482 -49.11392,-110.446904 -108.45,-82.01502 C 240.78868,66.899736 193.29129,50.544276 149.59749,51.209906 33.920647,52.826436 -55.321204,173.35286 40.672028,288.36407 c 0.04754,0.0476 0.09509,0.0951 0.19018,0.19018 1.473893,1.75916 2.995331,3.51833 4.516769,5.27749 6.228389,7.46456 10.364799,14.0733 13.883124,25.72181 l 28.622054,93.80617 H 77.757083 c -9.318809,0 -16.925999,7.55964 -16.925999,16.926 h 38.416311 l 64.566025,30.52385 c 3.99378,-8.463 0.38036,-18.54253 -8.03509,-22.48876 l -16.926,-7.98755 h 48.68602 c 0,-9.31881 -7.55965,-16.926 -16.926,-16.926 h -44.1217 c -7.70228,0 -14.54876,-5.03976 -16.78337,-12.40923 L 87.028346,326.49511 c -5.657848,-19.58851 7.321921,-32.3781 19.540974,-33.89954 11.12551,-1.3788 16.73581,5.80048 21.96576,23.20193 l 20.15905,66.08747 h -10.12707 c -9.31881,0 -16.926,7.55964 -16.926,16.926 h 38.41631 l 64.56603,30.52385 c 3.99378,-8.463 0.38036,-18.54253 -8.03509,-22.48876 l -16.926,-7.98755 h 35.70625 c 3.61341,6.1333 8.03509,11.93378 13.31258,17.16373 33.28146,33.28145 87.34006,33.28145 120.62151,0 33.28146,-33.28146 33.28146,-87.34006 0,-120.62152 -33.28145,-33.28145 -87.34005,-33.28145 -120.62151,0 -23.39211,23.39211 -30.38122,57.05393 -20.87223,86.53179 h -40.4132 c -7.70228,0 -14.54875,-5.03976 -16.78336,-12.40923 l -33.47164,-110.01899 5.7054,-3.04287 c 79.44759,-33.329 164.4104,26.19726 223.5563,-30.9993 0.14264,-0.14264 0.28527,-0.28527 0.38036,-0.38036 7.32192,-6.65629 14.64384,-13.45522 36.27679,0.0951 23.29702,14.59629 74.69311,52.67979 97.99013,117.81636 0.0475,0 14.16839,-5.03977 10.2697,-22.29858 z M 347.85989,176.39574 c -0.19018,-8.8909 -7.41702,-16.02265 -16.40301,-16.02265 -9.03354,0 -16.403,7.32193 -16.403,16.40301 0,4.94467 2.18706,9.31881 5.6103,12.31414 -11.88623,-2.28216 -20.82468,-12.64696 -20.82468,-25.15127 0,-14.1684 11.45833,-25.62673 25.62672,-25.62673 14.16839,0 25.62672,11.45833 25.62672,25.62673 0,4.51676 -1.18862,8.74826 -3.23305,12.45677 z"
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 id="path4"
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 style="stroke-width:0.475449" />
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> </svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,58 +0,0 @@
{% 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 %}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h3>{{ title }}</h3>
<p>
{% blocktrans trimmed %}
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>
{% if max_filesize %}
<div class="alert alert-warning" role="alert">
{% 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 ZIM file" %}"/>
</form>
{% endblock %}

View File

@ -15,7 +15,8 @@
<p> <p>
{% blocktrans trimmed %} {% blocktrans trimmed %}
Delete this package permanently? You may add it back later if you have a copy of the ZIM file. Delete this package permanently? You may add it back later if you have a
copy of the ZIM file.
{% endblocktrans %} {% endblocktrans %}
</p> </p>

View File

@ -8,32 +8,31 @@
{% block configuration %} {% block configuration %}
{{ block.super }} {{ block.super }}
<h3>{% trans "Manage Content" %}</h3> <h3>{% trans "Manage Content Packages" %}</h3>
<div class="btn-toolbar"> <div class="btn-toolbar">
<a href="{% url 'kiwix:add-content' %}" class="btn btn-default" <a href="{% url 'kiwix:add-package' %}" class="btn btn-default"
role="button" title="{% trans 'Add a content package' %}"> role="button" title="{% trans 'Add a content package' %}">
<span class="fa fa-plus" aria-hidden="true"></span> <span class="fa fa-plus" aria-hidden="true"></span>
{% trans 'Add' %} {% trans 'Add Package' %}
</a> </a>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
{% if not packages %} {% if not packages %}
<p>{% trans 'No content available.' %}</p> <p>{% trans 'No content packages available.' %}</p>
{% else %} {% else %}
<div id="kiwix-packages" class="list-group list-group-two-column"> <div id="kiwix-packages" class="list-group list-group-two-column">
{% for id, package in packages.items %} {% for id, package in packages.items %}
<div class="list-group-item"> <div class="list-group-item">
<a id="{{ id }}" <a id="{{ id }}" class="primary"
class="primary"
href="/kiwix/viewer#{{ package.path }}" href="/kiwix/viewer#{{ package.path }}"
title="{{ package.description }}"> title="{{ package.description }}">
{{ package.title }} {{ package.title }}
</a> </a>
<a href="{% url 'kiwix:delete-content' id %}" <a href="{% url 'kiwix:delete-package' id %}"
class="btn btn-default btn-sm secondary" role="button" class="btn btn-default btn-sm secondary" role="button"
title="{% blocktrans with title=package.title %}Delete package {{ title }}{% endblocktrans %}"> title="{% blocktrans with title=package.title %}Delete package {{ title }}{% endblocktrans %}">
<span class="fa fa-trash-o" aria-hidden="true"></span> <span class="fa fa-trash-o" aria-hidden="true"></span>

View File

@ -1 +1 @@
Nothing to see here. Nothing to see here.

View File

@ -3,94 +3,97 @@
Functional, browser based tests for Kiwix app. Functional, browser based tests for Kiwix app.
""" """
import pkg_resources import pathlib
from time import sleep
import pytest import pytest
from time import sleep
from plinth.modules.kiwix.tests.test_privileged import ZIM_ID
from plinth.tests import functional from plinth.tests import functional
from .test_privileged import ZIM_ID
pytestmark = [pytest.mark.apps, pytest.mark.sso, pytest.mark.kiwix] pytestmark = [pytest.mark.apps, pytest.mark.sso, pytest.mark.kiwix]
_default_url = functional.config['DEFAULT']['url'] _default_url = functional.config['DEFAULT']['url']
ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea' _data_dir = pathlib.Path(__file__).parent / 'data'
class TestKiwixApp(functional.BaseAppTests): class TestKiwixApp(functional.BaseAppTests):
"""Basic functional tests for Kiwix app."""
app_name = 'kiwix' app_name = 'kiwix'
has_service = True has_service = True
has_web = True has_web = True
def test_add_delete_content_package(self, session_browser): def test_add_delete_package(self, session_browser):
"""Test adding/deleting content package to the library.""" """Test adding/deleting content package to the library."""
functional.app_enable(session_browser, 'kiwix') functional.app_enable(session_browser, 'kiwix')
zim_file = pkg_resources.resource_filename( zim_file = _data_dir / 'FreedomBox.zim'
'plinth.modules.kiwix.tests', 'data/FreedomBox.zim') _add_package(session_browser, str(zim_file))
_add_content_package(session_browser, zim_file) assert _is_package_listed(session_browser, 'freedombox')
assert _is_content_package_listed(session_browser, 'freedombox') assert _is_package_available(session_browser, 'FreedomBox')
assert _is_content_package_available(session_browser, 'FreedomBox')
_delete_content_package(session_browser, ZIM_ID) _delete_package(session_browser, ZIM_ID)
assert not _is_content_package_listed(session_browser, 'freedombox') assert not _is_package_listed(session_browser, 'freedombox')
assert not _is_content_package_available(session_browser, 'FreedomBox') assert not _is_package_available(session_browser, 'FreedomBox')
@pytest.mark.backups @pytest.mark.backups
def test_backup_restore(self, session_browser): def test_backup_restore(self, session_browser):
"""Test backing up and restoring.""" """Test backing up and restoring."""
functional.app_enable(session_browser, 'kiwix') functional.app_enable(session_browser, 'kiwix')
zim_file = pkg_resources.resource_filename( zim_file = _data_dir / 'FreedomBox.zim'
'plinth.modules.kiwix.tests', 'data/FreedomBox.zim') _add_package(session_browser, str(zim_file))
_add_content_package(session_browser, zim_file)
functional.backup_create(session_browser, 'kiwix', 'test_kiwix') functional.backup_create(session_browser, 'kiwix', 'test_kiwix')
_delete_content_package(session_browser, ZIM_ID) _delete_package(session_browser, ZIM_ID)
functional.backup_restore(session_browser, 'kiwix', 'test_kiwix') functional.backup_restore(session_browser, 'kiwix', 'test_kiwix')
assert _is_content_package_listed(session_browser, 'freedombox') assert _is_package_listed(session_browser, 'freedombox')
assert _is_content_package_available(session_browser, 'FreedomBox') assert _is_package_available(session_browser, 'FreedomBox')
def test_add_invalid_zim_file(self, session_browser): def test_add_invalid_zim_file(self, session_browser):
"""Test handling of invalid zim files.""" """Test handling of invalid zim files."""
functional.app_enable(session_browser, 'kiwix') functional.app_enable(session_browser, 'kiwix')
zim_file = pkg_resources.resource_filename( zim_file = _data_dir / 'invalid.zim'
'plinth.modules.kiwix.tests', 'data/invalid.zim') _add_package(session_browser, str(zim_file))
_add_content_package(session_browser, zim_file)
assert not _is_content_package_listed(session_browser, 'invalid') assert not _is_package_listed(session_browser, 'invalid')
def _add_content_package(browser, file_name): def _add_package(browser, file_name):
browser.links.find_by_href('/plinth/apps/kiwix/content/add/').first.click() """Add a package by uploading the ZIM file in kiwix app page."""
browser.links.find_by_href('/plinth/apps/kiwix/package/add/').first.click()
browser.attach_file('kiwix-file', file_name) browser.attach_file('kiwix-file', file_name)
functional.submit(browser, form_class='form-kiwix') functional.submit(browser, form_class='form-kiwix')
def _is_content_package_available(browser, title) -> bool: def _is_package_available(browser, title) -> bool:
"""Check whether a ZIM file is available in Kiwix web interface."""
browser.visit(f'{_default_url}/kiwix') browser.visit(f'{_default_url}/kiwix')
sleep(1) # Allow time for the books to appear sleep(1) # Allow time for the books to appear
titles = browser.find_by_id('book__title') titles = browser.find_by_id('book__title')
print(len(titles)) return any(element.value == title for element in 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: def _is_package_listed(browser, name) -> bool:
"""Return whether a content package is list in kiwix app page."""
functional.nav_to_module(browser, 'kiwix') functional.nav_to_module(browser, 'kiwix')
links_found = browser.links.find_by_partial_href(f'/kiwix/viewer#{name}') links_found = browser.links.find_by_partial_href(f'/kiwix/viewer#{name}')
return len(links_found) == 1 return len(links_found) == 1
def _delete_content_package(browser, zim_id): def _delete_package(browser, zim_id):
"""Delete a content package from the kiwix app page."""
functional.nav_to_module(browser, 'kiwix') functional.nav_to_module(browser, 'kiwix')
link = browser.links.find_by_href( link = browser.links.find_by_href(
f'/plinth/apps/kiwix/content/{zim_id}/delete/') f'/plinth/apps/kiwix/package/{zim_id}/delete/')
if not link: if not link:
raise ValueError('ZIM file missing!') raise ValueError('ZIM file missing!')
link.first.click() link.first.click()
functional.submit(browser, form_class='form-delete') functional.submit(browser, form_class='form-delete')

View File

@ -4,7 +4,7 @@ Test module for Kiwix actions.
""" """
import pathlib import pathlib
import pkg_resources import shutil
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -22,65 +22,57 @@ ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea'
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def fixture_kiwix_home(tmpdir): def fixture_kiwix_home(tmp_path):
"""Set Kiwix home to a new temporary directory """Create a new Kiwix home in a new temporary directory.
initialized with an empty library file."""
privileged.KIWIX_HOME = pathlib.Path(str(tmpdir / 'kiwix')) Initialize with a sample, valid library file.
"""
privileged.KIWIX_HOME = tmp_path / 'kiwix'
privileged.KIWIX_HOME.mkdir() privileged.KIWIX_HOME.mkdir()
privileged.CONTENT_DIR = privileged.KIWIX_HOME / 'content' privileged.CONTENT_DIR = privileged.KIWIX_HOME / 'content'
privileged.CONTENT_DIR.mkdir() privileged.CONTENT_DIR.mkdir()
privileged.LIBRARY_FILE = privileged.KIWIX_HOME / 'library_zim.xml' privileged.LIBRARY_FILE = privileged.KIWIX_HOME / 'library_zim.xml'
with open(privileged.LIBRARY_FILE, 'w', encoding='utf_8') as library_file: source_file = pathlib.Path(__file__).parent / 'data/sample_library_zim.xml'
library_file.write(EMPTY_LIBRARY_CONTENTS) shutil.copy(source_file, privileged.LIBRARY_FILE)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def fixture_patch(): def fixture_patch():
"""Patch some underlying methods.""" """Patch some underlying methods."""
with patch('subprocess.check_call'), patch('subprocess.run'): with patch('subprocess.check_call'), patch('subprocess.run'), patch(
'os.chown'):
yield yield
def test_add_content(tmpdir): def test_add_package(tmp_path):
"""Test adding a content package to Kiwix.""" """Test adding a content package to Kiwix."""
some_dir = tmpdir / 'some' / 'dir' some_dir = tmp_path / 'some' / 'dir'
pathlib.Path(some_dir).mkdir(parents=True, exist_ok=True) some_dir.mkdir(parents=True, exist_ok=True)
zim_file_name = 'wikipedia_en_all_maxi_2022-05.zim' zim_file_name = 'wikipedia_en_all_maxi_2022-05.zim'
orig_file = some_dir / zim_file_name orig_file = some_dir / zim_file_name
pathlib.Path(orig_file).touch() orig_file.touch()
privileged.add_content(str(orig_file)) privileged.add_package(str(orig_file))
assert (privileged.KIWIX_HOME / 'content' / zim_file_name).exists() assert (privileged.KIWIX_HOME / 'content' / zim_file_name).exists()
assert not orig_file.exists() assert not orig_file.exists()
def test_list_content_packages(): def test_list_packages():
"""Test listing the content packages from a library file.""" """Test listing the content packages from a library file."""
privileged.LIBRARY_FILE = pkg_resources.resource_filename( content = privileged.list_packages()
'plinth.modules.kiwix.tests', 'data/sample_library_zim.xml') assert content[ZIM_ID] == {
content_packages = privileged.list_content_packages()
assert content_packages[ZIM_ID] == {
'title': 'FreedomBox', 'title': 'FreedomBox',
'description': 'A sample content archive', 'description': 'A sample content archive',
'path': 'freedombox' 'path': 'freedombox'
} }
def test_delete_content_package(): def test_delete_package():
"""Test deleting one 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 = privileged.CONTENT_DIR / 'FreedomBox.zim'
zim_file.touch() zim_file.touch()
privileged.delete_content_package(ZIM_ID) privileged.delete_package(ZIM_ID)
assert not zim_file.exists() assert not zim_file.exists()
# Cannot check that the book is removed from library_zim.xml # Cannot check that the book is removed from library_zim.xml

View File

@ -3,19 +3,18 @@
Test module for Kiwix validations. Test module for Kiwix validations.
""" """
import unittest import pytest
from plinth.modules import kiwix from plinth.modules import kiwix
class TestValidations(unittest.TestCase): def test_add_file_with_invalid_extension():
"""Test that adding a file with invalid fails as expected."""
with pytest.raises(ValueError):
kiwix.validate_file_name('wikipedia.zip')
def test_add_file_with_invalid_extension(self): # We don't support the legacy format of split zim files.
self.assertRaises(ValueError, with pytest.raises(ValueError):
lambda: kiwix.validate_file_name('wikipedia.zip')) kiwix.validate_file_name('wikipedia_en_all_maxi_2022-05.zima')
# We don't support the legacy format of split zim files. kiwix.validate_file_name('wikipedia_en_all_maxi_2022-05.zim')
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')

View File

@ -3,15 +3,15 @@
Test module for Kiwix views. Test module for Kiwix views.
""" """
from plinth import module_loader import pathlib
from django import urls
from unittest.mock import call, patch 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 import pytest
from django import urls
from django.contrib.messages.storage.fallback import FallbackStorage
from django.http.response import Http404
from plinth import module_loader
from plinth.modules.kiwix import views from plinth.modules.kiwix import views
# For all tests, use plinth.urls instead of urls configured for testing # For all tests, use plinth.urls instead of urls configured for testing
@ -19,6 +19,8 @@ pytestmark = pytest.mark.urls('plinth.urls')
ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea' ZIM_ID = 'bc4f8cdf-5626-2b13-3860-0033deddfbea'
_data_dir = pathlib.Path(__file__).parent / 'data'
@pytest.fixture(autouse=True, scope='module') @pytest.fixture(autouse=True, scope='module')
def fixture_kiwix_urls(): def fixture_kiwix_urls():
@ -41,107 +43,88 @@ def make_request(request, view, **kwargs):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def kiwix_patch(): def fiture_kiwix_patch():
"""Patch kiwix methods.""" """Patch kiwix methods."""
with patch('plinth.modules.kiwix.privileged.list_content_packages' with patch(
) as list_libraries: 'plinth.modules.kiwix.privileged.list_packages') as list_libraries:
list_libraries.return_value = { list_libraries.return_value = {
ZIM_ID: { ZIM_ID: {
'title': 'TestExistingContentPackage', 'title': 'TestExistingPackage',
'description': 'A sample content package', 'description': 'A sample content package',
'path': 'test_existing_content_package' 'path': 'test_existing_package'
} }
} }
yield yield
@pytest.fixture() @patch('tempfile.TemporaryDirectory')
def storage_info_patch(): @patch('plinth.modules.kiwix.privileged.add_package')
"""Patch storage info method.""" def test_add_package(add_package, temp_dir_class, rf, tmp_path):
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.""" """Test that adding content view works."""
with open('plinth/modules/kiwix/tests/data/FreedomBox.zim', temp_dir_class.return_value.__enter__.return_value = str(tmp_path)
'rb') as zim_file: with open(_data_dir / 'FreedomBox.zim', 'rb') as zim_file:
post_data = { post_data = {'kiwix-file': zim_file}
'kiwix-file': zim_file, request = rf.post('', data=post_data)
}
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, response, messages = make_request(request,
views.AddContentView.as_view()) views.AddPackageView.as_view())
assert response.status_code == 302 assert response.status_code == 302
assert response.url == urls.reverse('kiwix:index') assert response.url == urls.reverse('kiwix:index')
assert list(messages)[0].message == 'Content package added.' assert list(messages)[0].message == 'Content package added.'
add_content.assert_has_calls([call('/tmp/FreedomBox.zim')]) add_package.assert_has_calls([call(f'{tmp_path}/FreedomBox.zim')])
@patch('plinth.modules.kiwix.privileged.add_content') @patch('plinth.modules.kiwix.privileged.add_package')
def test_add_content_package_failed(add_content, rf): def test_add_package_failed(add_package, rf):
"""Test that adding content package fails in case of an error.""" """Test that adding content package fails in case of an error."""
add_content.side_effect = RuntimeError('TestError') add_package.side_effect = RuntimeError('TestError')
with open('plinth/modules/kiwix/tests/data/FreedomBox.zim', with open(_data_dir / 'FreedomBox.zim', 'rb') as zim_file:
'rb') as zim_file: post_data = {'kiwix-file': zim_file}
post_data = { request = rf.post('', data=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, response, messages = make_request(request,
views.AddContentView.as_view()) views.AddPackageView.as_view())
assert response.status_code == 302 assert response.status_code == 302
assert response.url == urls.reverse('kiwix:index') assert response.url == urls.reverse('kiwix:index')
assert list(messages)[0].message == \ assert list(messages)[0].message == 'Failed to add content package.'
'Failed to add content package.'
@patch('plinth.app.App.get') @patch('plinth.app.App.get')
def test_delete_package_confirmation_view(_app, rf): def test_delete_package_confirmation_view(_app, rf):
"""Test that deleting package confirmation shows correct title.""" """Test that deleting content confirmation shows correct title."""
response, _ = make_request(rf.get(''), views.delete_content, zim_id=ZIM_ID) response, _ = make_request(rf.get(''), views.delete_package, zim_id=ZIM_ID)
assert response.status_code == 200 assert response.status_code == 200
assert response.context_data['name'] == 'TestExistingContentPackage' assert response.context_data['name'] == 'TestExistingPackage'
@patch('plinth.modules.kiwix.privileged.delete_content_package') @patch('plinth.modules.kiwix.privileged.delete_package')
@patch('plinth.app.App.get') @patch('plinth.app.App.get')
def test_delete_content_package(_app, delete_content_package, rf): def test_delete_package(_app, delete_package, rf):
"""Test that deleting a content package works.""" """Test that deleting a content package works."""
response, messages = make_request(rf.post(''), views.delete_content, response, messages = make_request(rf.post(''), views.delete_package,
zim_id=ZIM_ID) zim_id=ZIM_ID)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == urls.reverse('kiwix:index') assert response.url == urls.reverse('kiwix:index')
assert list(messages)[0].message == 'TestExistingContentPackage deleted.' assert list(messages)[0].message == 'TestExistingPackage deleted.'
delete_content_package.assert_has_calls([call(ZIM_ID)]) delete_package.assert_has_calls([call(ZIM_ID)])
@patch('plinth.modules.kiwix.privileged.delete_content_package') @patch('plinth.modules.kiwix.privileged.delete_package')
def test_delete_content_package_error(delete_content_package, rf): def test_delete_package_error(delete_package, rf):
"""Test that deleting a content package shows an error when operation fails.""" """Test that deleting content shows an error when operation fails."""
delete_content_package.side_effect = ValueError('TestError') delete_package.side_effect = ValueError('TestError')
response, messages = make_request(rf.post(''), views.delete_content, response, messages = make_request(rf.post(''), views.delete_package,
zim_id=ZIM_ID) zim_id=ZIM_ID)
assert response.status_code == 302 assert response.status_code == 302
assert response.url == urls.reverse('kiwix:index') assert response.url == urls.reverse('kiwix:index')
assert list(messages)[0].message == \ assert list(messages)[0].message == \
'Could not delete TestExistingContentPackage: TestError' 'Could not delete TestExistingPackage: TestError'
def test_delete_content_package_non_existing(rf): def test_delete_package_non_existing(rf):
"""Test that deleting a content package shows error when operation fails.""" """Test that deleting content shows error when operation fails."""
with pytest.raises(Http404): with pytest.raises(Http404):
make_request(rf.post(''), views.delete_content, make_request(rf.post(''), views.delete_package,
zim_id='NonExistentZimId') zim_id='NonExistentZimId')
with pytest.raises(Http404): with pytest.raises(Http404):
make_request(rf.get(''), views.delete_content, make_request(rf.get(''), views.delete_package,
zim_id='NonExistentZimId') zim_id='NonExistentZimId')

View File

@ -9,8 +9,8 @@ from . import views
urlpatterns = [ urlpatterns = [
re_path(r'^apps/kiwix/$', views.KiwixAppView.as_view(), name='index'), re_path(r'^apps/kiwix/$', views.KiwixAppView.as_view(), name='index'),
re_path(r'^apps/kiwix/content/add/$', views.AddContentView.as_view(), re_path(r'^apps/kiwix/package/add/$', views.AddPackageView.as_view(),
name='add-content'), name='add-package'),
re_path(r'^apps/kiwix/content/(?P<zim_id>[a-zA-Z0-9-]+)/delete/$', re_path(r'^apps/kiwix/package/(?P<zim_id>[a-zA-Z0-9-]+)/delete/$',
views.delete_content, name='delete-content'), views.delete_package, name='delete-package'),
] ]

View File

@ -4,6 +4,7 @@ Views for the Kiwix module.
""" """
import logging import logging
import tempfile
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
@ -27,21 +28,23 @@ logger = logging.getLogger(__name__)
class KiwixAppView(views.AppView): class KiwixAppView(views.AppView):
"""Serve configuration form.""" """Serve configuration form."""
app_id = 'kiwix' app_id = 'kiwix'
template_name = 'kiwix.html' template_name = 'kiwix.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Return additional context for rendering the template.""" """Return additional context for rendering the template."""
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['packages'] = privileged.list_content_packages() context['packages'] = privileged.list_packages()
return context return context
class AddContentView(SuccessMessageMixin, FormView): class AddPackageView(SuccessMessageMixin, FormView):
"""View to add content in the form of ZIM files.""" """View to add content package in the form of ZIM files."""
form_class = forms.AddContentForm
form_class = forms.AddPackageForm
prefix = 'kiwix' prefix = 'kiwix'
template_name = 'add-content-package.html' template_name = 'kiwix-add-package.html'
success_url = reverse_lazy('kiwix:index') success_url = reverse_lazy('kiwix:index')
success_message = _('Content package added.') success_message = _('Content package added.')
@ -66,23 +69,26 @@ class AddContentView(SuccessMessageMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
"""Store the uploaded file.""" """Store the uploaded file."""
multipart_file = self.request.FILES['kiwix-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: with tempfile.TemporaryDirectory() as temp_dir:
privileged.add_content(zim_file_name) zim_file_name = temp_dir + '/' + multipart_file.name
except Exception: with open(zim_file_name, 'wb+') as zim_file:
messages.error(self.request, _('Failed to add content package.')) for chunk in multipart_file.chunks():
return redirect(reverse_lazy('kiwix:index')) zim_file.write(chunk)
try:
privileged.add_package(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) return super().form_valid(form)
def delete_content(request, zim_id): def delete_package(request, zim_id):
"""View to delete a library.""" """View to delete a library."""
packages = privileged.list_content_packages() packages = privileged.list_packages()
if zim_id not in packages: if zim_id not in packages:
raise Http404 raise Http404
@ -90,8 +96,8 @@ def delete_content(request, zim_id):
if request.method == 'POST': if request.method == 'POST':
try: try:
privileged.delete_content_package(zim_id) privileged.delete_package(zim_id)
messages.success(request, _(f'{name} deleted.')) messages.success(request, _('{name} deleted.').format(name=name))
except Exception as error: except Exception as error:
messages.error( messages.error(
request, request,
@ -99,7 +105,7 @@ def delete_content(request, zim_id):
name=name, error=error)) name=name, error=error))
return redirect(reverse_lazy('kiwix:index')) return redirect(reverse_lazy('kiwix:index'))
return TemplateResponse(request, 'delete-content-package.html', { return TemplateResponse(request, 'kiwix-delete-package.html', {
'title': app_module.App.get('kiwix').info.name, 'title': app_module.App.get('kiwix').info.name,
'name': name 'name': name
}) })