diff --git a/debian/copyright b/debian/copyright
index bb756b6ff..64b25d866 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -101,6 +101,12 @@ Copyright: 2012 William Theaker
Comment: https://gitlab.com/fdroid/artwork/blob/master/fdroid-logo-2015/fdroid-logo.svg
License: CC-BY-SA-3.0 or GPL-3+
+Files: plinth/modules/featherwiki/static/icons/featherwiki.png
+ plinth/modules/featherwiki/static/icons/featherwiki.svg
+Copyright: 2022 Robbie Antenesse
+Comment: https://codeberg.org/Alamantus/FeatherWiki/src/branch/main/logo.svg
+License: AGPL-3+
+
Files: plinth/modules/gitweb/static/icons/gitweb.png
plinth/modules/gitweb/static/icons/gitweb.svg
Copyright: 2010 Git Authors
diff --git a/plinth/modules/featherwiki/__init__.py b/plinth/modules/featherwiki/__init__.py
new file mode 100644
index 000000000..0a7120438
--- /dev/null
+++ b/plinth/modules/featherwiki/__init__.py
@@ -0,0 +1,120 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+FreedomBox app for Feather Wiki.
+
+This is a FreedomBox-native implementation of a Feather Wiki Nest.
+This app doesn't install any Debian packages.
+"""
+
+from django.urls import reverse_lazy
+from django.utils.translation import gettext_lazy as _
+
+from plinth import app as app_module
+from plinth import cfg, frontpage, menu
+from plinth.config import DropinConfigs
+from plinth.modules.apache.components import Webserver
+from plinth.modules.backups.components import BackupRestore
+from plinth.modules.firewall.components import Firewall
+from plinth.modules.users.components import UsersAndGroups
+from plinth.utils import format_lazy
+
+from . import manifest, privileged
+
+_description = [
+ format_lazy(
+ _('Feather Wiki is a tool to create simple self-contained wikis, each '
+ 'stored in a single HTML file on your {box_name}. You can use it as '
+ 'a personal wiki, as a web notebook, or for project documentation.'),
+ box_name=_(cfg.box_name)),
+ _('Each wiki is a small file. Create as many wikis as you like, such as '
+ 'one wiki per topic. Customize each wiki to your liking with extensions '
+ 'and other customization options.'),
+ _('Feather Wiki is downloaded from upstream website and not from Debian. '
+ 'Wikis need to be upgraded to newer version manually.'),
+ format_lazy(
+ _('Wikis are not public by default, but they can be downloaded for '
+ 'sharing or publishing. They can be edited by '
+ 'any user on {box_name} belonging to the wiki group. '
+ 'Simultaneous editing is not supported.'), box_name=_(cfg.box_name),
+ users_url=reverse_lazy('users:index'))
+]
+
+
+class FeatherWikiApp(app_module.App):
+ """FreedomBox app for Feather Wiki."""
+
+ app_id = 'featherwiki'
+
+ _version = 1
+
+ def __init__(self):
+ """Create components for the app."""
+ super().__init__()
+
+ groups = {'wiki': _('View and edit wiki applications')}
+
+ info = app_module.Info(self.app_id, self._version,
+ name=_('Feather Wiki'),
+ icon_filename='featherwiki',
+ short_description=_('Personal Notebooks'),
+ description=_description,
+ manual_page='FeatherWiki',
+ clients=manifest.clients)
+ self.add(info)
+
+ menu_item = menu.Menu('menu-featherwiki', info.name,
+ info.short_description, info.icon_filename,
+ 'featherwiki:index', parent_url_name='apps')
+ self.add(menu_item)
+
+ # The shortcut is a simple directory listing provided by Apache server.
+ # Expecting a large number of wiki files, so creating a shortcut for
+ # each file (like in ikiwiki's case) will crowd the front page.
+ shortcut = frontpage.Shortcut(
+ 'shortcut-featherwiki', info.name,
+ short_description=info.short_description, icon=info.icon_filename,
+ description=info.description, manual_page=info.manual_page,
+ url='/featherwiki/', clients=info.clients, login_required=True,
+ allowed_groups=list(groups))
+ self.add(shortcut)
+
+ dropin_configs = DropinConfigs('dropin-configs-featherwiki', [
+ '/etc/apache2/conf-available/featherwiki-freedombox.conf',
+ ])
+ self.add(dropin_configs)
+
+ firewall = Firewall('firewall-featherwiki', info.name,
+ ports=['http', 'https'], is_external=True)
+ self.add(firewall)
+
+ webserver = Webserver('webserver-featherwiki',
+ 'featherwiki-freedombox')
+ self.add(webserver)
+
+ users_and_groups = UsersAndGroups('users-and-groups-featherwiki',
+ groups=groups)
+ self.add(users_and_groups)
+
+ backup_restore = BackupRestore('backup-restore-featherwiki',
+ **manifest.backup)
+ self.add(backup_restore)
+
+ def setup(self, old_version=None):
+ """Install and configure the app."""
+ super().setup(old_version)
+ privileged.setup()
+ if not old_version:
+ self.enable()
+
+ def uninstall(self):
+ """Purge directory with all the wikis."""
+ super().uninstall()
+ privileged.uninstall()
+
+
+def get_wiki_list():
+ """List all the Feather Wiki files."""
+ return sorted([
+ path.name for path in privileged.wiki_dir.iterdir()
+ if path.suffix == '.html'
+ ])
diff --git a/plinth/modules/featherwiki/data/usr/share/freedombox/etc/apache2/conf-available/featherwiki-freedombox.conf b/plinth/modules/featherwiki/data/usr/share/freedombox/etc/apache2/conf-available/featherwiki-freedombox.conf
new file mode 100644
index 000000000..3781dedb8
--- /dev/null
+++ b/plinth/modules/featherwiki/data/usr/share/freedombox/etc/apache2/conf-available/featherwiki-freedombox.conf
@@ -0,0 +1,25 @@
+##
+## On all sites, provide Feather Wiki files on a path: /featherwiki
+##
+
+Alias /featherwiki /var/lib/featherwiki
+
+
+ Include includes/freedombox-single-sign-on.conf
+
+ TKTAuthToken "admin" "wiki"
+
+
+
+
+ Dav On
+
+ # Don't accept overrides in .htaccess
+ AllowOverride None
+
+ # Disable following symlinks, show an index page
+ Options Indexes
+
+ # Accept and serve only HTML files
+ ForceType text/html
+
diff --git a/plinth/modules/featherwiki/data/usr/share/freedombox/modules-enabled/featherwiki b/plinth/modules/featherwiki/data/usr/share/freedombox/modules-enabled/featherwiki
new file mode 100644
index 000000000..9e2d74f56
--- /dev/null
+++ b/plinth/modules/featherwiki/data/usr/share/freedombox/modules-enabled/featherwiki
@@ -0,0 +1 @@
+plinth.modules.featherwiki
diff --git a/plinth/modules/featherwiki/forms.py b/plinth/modules/featherwiki/forms.py
new file mode 100644
index 000000000..7f2687264
--- /dev/null
+++ b/plinth/modules/featherwiki/forms.py
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Django forms for configuring Feather Wiki."""
+
+from django import forms
+from django.core import validators
+from django.utils.translation import gettext_lazy as _
+
+
+class CreateWikiForm(forms.Form):
+ """Form to create a new wiki file."""
+
+ name = forms.CharField(
+ label=_('Name of the wiki file, with file extension ".html"'),
+ strip=True, help_text=_(
+ 'Wiki title and description can be set from within the wiki. '
+ 'This file name is independent of the wiki title.'))
+
+
+class RenameWikiForm(forms.Form):
+ """Form to rename a wiki file."""
+
+ new_name = forms.CharField(
+ label=_('New name for the wiki file, with file extension ".html"'),
+ strip=True, help_text=_(
+ 'Renaming the file has no effect on the title of the wiki.'))
+
+
+class UploadWikiForm(forms.Form):
+ """Form to upload a wiki file."""
+
+ file = forms.FileField(
+ label=_('A Feather Wiki file with .html file extension'),
+ required=True, validators=[
+ validators.FileExtensionValidator(
+ ['html'], _('Feather Wiki files must be in HTML format'))
+ ], help_text=_(
+ 'Upload an existing Feather Wiki file from this computer.'))
diff --git a/plinth/modules/featherwiki/manifest.py b/plinth/modules/featherwiki/manifest.py
new file mode 100644
index 000000000..0469011c0
--- /dev/null
+++ b/plinth/modules/featherwiki/manifest.py
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Application manifest for Feather Wiki."""
+
+from django.utils.translation import gettext_lazy as _
+
+from .privileged import wiki_dir
+
+clients = [{
+ 'name': _('Feather Wiki'),
+ 'platforms': [{
+ 'type': 'web',
+ 'url': '/featherwiki/'
+ }]
+}]
+
+backup = {'data': {'directories': [str(wiki_dir)]}}
diff --git a/plinth/modules/featherwiki/privileged.py b/plinth/modules/featherwiki/privileged.py
new file mode 100644
index 000000000..d5066d17c
--- /dev/null
+++ b/plinth/modules/featherwiki/privileged.py
@@ -0,0 +1,93 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Configure Feather Wiki."""
+
+import pathlib
+import re
+import shutil
+import tempfile
+import urllib.request
+
+from plinth.actions import privileged
+
+# Needs to be changed on a new release
+EMPTY_WIKI_FILE = 'https://feather.wiki/builds/v1.8.x/FeatherWiki_Skylark.html'
+
+wiki_dir = pathlib.Path('/var/lib/featherwiki')
+
+
+def _set_ownership(path: pathlib.Path):
+ """Makes www-data:www-data the owner of the give path."""
+ shutil.chown(path, user='www-data', group='www-data')
+
+
+@privileged
+def setup():
+ """Setup wiki dir and CGI script."""
+ wiki_dir.mkdir(parents=True, exist_ok=True)
+ _set_ownership(wiki_dir)
+
+
+def _normalize_wiki_file_name(name):
+ """Return a normalized file name from a wiki name."""
+ file_name = name.replace(' ', '_')
+ invalid_characters = r'[\/\\\:\*\?\"\'\<\>\|]'
+ file_name = re.sub(invalid_characters, '', file_name)
+ if not file_name.endswith('.html'):
+ return file_name + '.html'
+
+ return file_name
+
+
+@privileged
+def create_wiki(file_name: str):
+ """Initialize wiki with the latest version of Feather Wiki."""
+ file_name = _normalize_wiki_file_name(file_name)
+ response = urllib.request.urlopen(EMPTY_WIKI_FILE)
+ file_path = wiki_dir / file_name
+ if file_path.exists():
+ raise ValueError('Wiki exists')
+
+ file_path.write_bytes(response.read())
+ _set_ownership(file_path)
+
+
+@privileged
+def add_wiki_file(upload_file_path: str):
+ """Add an uploaded wiki file."""
+ upload_file_path = pathlib.Path(upload_file_path)
+ temp_dir = tempfile.gettempdir()
+ if not upload_file_path.is_relative_to(temp_dir):
+ raise Exception('Uploaded file is not in expected temp directory.')
+
+ file_name = _normalize_wiki_file_name(upload_file_path.name)
+ file_path = wiki_dir / file_name
+ if file_path.exists():
+ raise ValueError('Wiki exists')
+
+ shutil.move(upload_file_path, file_path)
+ _set_ownership(file_path)
+
+
+@privileged
+def rename_wiki(old_name: str, new_name: str):
+ """Rename wiki file."""
+ old_name = _normalize_wiki_file_name(old_name)
+ new_name = _normalize_wiki_file_name(new_name)
+ file_path = wiki_dir / new_name
+ if file_path.exists():
+ raise ValueError('Wiki exists')
+
+ (wiki_dir / old_name).rename(file_path)
+
+
+@privileged
+def delete_wiki(file_name: str):
+ """Delete one wiki file by name."""
+ file_name = _normalize_wiki_file_name(file_name)
+ (wiki_dir / file_name).unlink(missing_ok=True)
+
+
+@privileged
+def uninstall():
+ """Delete all the wiki content."""
+ shutil.rmtree(wiki_dir)
diff --git a/plinth/modules/featherwiki/static/icons/featherwiki.png b/plinth/modules/featherwiki/static/icons/featherwiki.png
new file mode 100644
index 000000000..35660077e
Binary files /dev/null and b/plinth/modules/featherwiki/static/icons/featherwiki.png differ
diff --git a/plinth/modules/featherwiki/static/icons/featherwiki.svg b/plinth/modules/featherwiki/static/icons/featherwiki.svg
new file mode 100644
index 000000000..fa4f2d7e2
--- /dev/null
+++ b/plinth/modules/featherwiki/static/icons/featherwiki.svg
@@ -0,0 +1,159 @@
+
+
diff --git a/plinth/modules/featherwiki/templates/featherwiki_configure.html b/plinth/modules/featherwiki/templates/featherwiki_configure.html
new file mode 100644
index 000000000..416d42f8f
--- /dev/null
+++ b/plinth/modules/featherwiki/templates/featherwiki_configure.html
@@ -0,0 +1,60 @@
+{% extends "app.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+
+{% block configuration %}
+ {{ block.super }}
+
+ {% trans "Manage Wikis" %}
+
+
+
+
+
+ {% if not wikis %}
+
{% trans 'No wikis available.' %}
+ {% else %}
+
+ {% for wiki in wikis %}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/plinth/modules/featherwiki/templates/featherwiki_delete.html b/plinth/modules/featherwiki/templates/featherwiki_delete.html
new file mode 100644
index 000000000..714bff5bc
--- /dev/null
+++ b/plinth/modules/featherwiki/templates/featherwiki_delete.html
@@ -0,0 +1,37 @@
+{% extends "base.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+
+{% block content %}
+
+
+ {% blocktrans trimmed %}
+ Delete wiki {{ name }}
+ {% endblocktrans %}
+
+
+
+ {% blocktrans trimmed %}
+ Hint: You can download a copy of this wiki from within
+ Feather Wiki before deleting it.
+ {% endblocktrans %}
+
+
+
+ {% blocktrans trimmed %}
+ Delete this wiki file permanently?
+ {% endblocktrans %}
+
+
+
+
+{% endblock %}
diff --git a/plinth/modules/featherwiki/templates/featherwiki_upload_file.html b/plinth/modules/featherwiki/templates/featherwiki_upload_file.html
new file mode 100644
index 000000000..99b274c4b
--- /dev/null
+++ b/plinth/modules/featherwiki/templates/featherwiki_upload_file.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{% comment %}
+# SPDX-License-Identifier: AGPL-3.0-or-later
+{% endcomment %}
+
+{% load bootstrap %}
+{% load i18n %}
+
+{% block content %}
+
+ {{ title }}
+
+
+
+{% endblock %}
diff --git a/plinth/modules/featherwiki/tests/__init__.py b/plinth/modules/featherwiki/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/modules/featherwiki/tests/data/dummy_wiki.html b/plinth/modules/featherwiki/tests/data/dummy_wiki.html
new file mode 100644
index 000000000..fd092d709
--- /dev/null
+++ b/plinth/modules/featherwiki/tests/data/dummy_wiki.html
@@ -0,0 +1,9 @@
+
+
+
+ Dummy Feather Wiki File
+
+
+ This is a not a real Feather Wiki file.
+
+
diff --git a/plinth/modules/featherwiki/tests/test_functional.py b/plinth/modules/featherwiki/tests/test_functional.py
new file mode 100644
index 000000000..812c54e97
--- /dev/null
+++ b/plinth/modules/featherwiki/tests/test_functional.py
@@ -0,0 +1,105 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Functional, browser based tests for Feather Wiki app."""
+
+import pathlib
+
+import pytest
+
+from plinth.tests import functional
+
+pytestmark = [pytest.mark.apps, pytest.mark.featherwiki]
+
+course_1 = 'Computer Organization and Architecture'
+file_name_1 = 'Computer_Organization_and_Architecture.html'
+
+
+class TestFeatherWikiApp(functional.BaseAppTests):
+ app_name = 'featherwiki'
+ has_service = False
+ has_web = True
+
+ def _create_wiki_file(self, session_browser):
+ """Add a wiki to using the 'Create' functionality."""
+ functional.nav_to_module(session_browser, 'featherwiki')
+
+ wiki_link_1 = f'/featherwiki/{file_name_1}'
+ if self._get_links_in_app_page(session_browser, wiki_link_1):
+ return
+
+ session_browser.links.find_by_href(
+ '/plinth/apps/featherwiki/create/').first.click()
+ session_browser.find_by_id('id_featherwiki-name').fill(course_1)
+ functional.submit(session_browser, form_class='form-featherwiki')
+
+ def _get_links_in_app_page(self, session_browser, link):
+ """Return the links matching a href in the app page."""
+ functional.nav_to_module(session_browser, 'featherwiki')
+ return session_browser.links.find_by_href(link)
+
+ def _get_links_in_apache_listing(self, session_browser, link):
+ """Return the links matching a href in the index page."""
+ default_url = functional.config['DEFAULT']['url']
+ session_browser.visit(f'{default_url}/featherwiki')
+ return session_browser.links.find_by_href(link)
+
+ def _assert_wiki_present(self, session_browser, file_name, present=True):
+ """Assert that a wiki is present."""
+ wiki_link = f'/featherwiki/{file_name}'
+ assert bool(self._get_links_in_app_page(session_browser,
+ wiki_link)) == present
+ assert bool(
+ self._get_links_in_apache_listing(session_browser,
+ file_name)) == present
+
+ def _assert_wiki_works(self, session_browser, file_name):
+ """Assert that wiki loads and run as expected."""
+ wiki_link = f'/featherwiki/{file_name}'
+ default_url = functional.config['DEFAULT']['url']
+ session_browser.visit(f'{default_url}{wiki_link}')
+ links = session_browser.links.find_by_href('https://feather.wiki')
+ assert len(links) == 1
+
+ def test_wiki_file_access(self, session_browser):
+ """Test creating a new wiki file."""
+ self._create_wiki_file(session_browser)
+
+ self._assert_wiki_present(session_browser, file_name_1)
+ self._assert_wiki_works(session_browser, file_name_1)
+
+ def test_rename_wiki_file(self, session_browser):
+ """Test changing the name of a wiki file."""
+ self._create_wiki_file(session_browser)
+
+ new_course = 'A Midsummer Night\'s Dream'
+ new_file_name = 'A_Midsummer_Nights_Dream.html'
+ self._get_links_in_app_page(
+ session_browser, '/plinth/apps/featherwiki/' + file_name_1 +
+ '/rename/').first.click()
+ session_browser.find_by_id('id_featherwiki-new_name').fill(new_course)
+ functional.submit(session_browser, form_class='form-featherwiki')
+
+ self._assert_wiki_present(session_browser, new_file_name)
+ self._assert_wiki_works(session_browser, new_file_name)
+
+ def test_upload_wiki_file(self, session_browser):
+ """Test uploading an existing wiki file."""
+ _test_data_dir = pathlib.Path(__file__).parent / 'data'
+ test_wiki_file = str(_test_data_dir / 'dummy_wiki.html')
+
+ session_browser.links.find_by_href(
+ '/plinth/apps/featherwiki/upload/').first.click()
+ session_browser.attach_file('featherwiki-file', test_wiki_file)
+ functional.submit(session_browser, form_class='form-featherwiki')
+
+ self._assert_wiki_present(session_browser, 'dummy_wiki.html')
+
+ def test_delete_wiki_file(self, session_browser):
+ """Test deleting an existing wiki file"""
+ self._create_wiki_file(session_browser)
+
+ self._get_links_in_app_page(
+ session_browser, '/plinth/apps/featherwiki/' + file_name_1 +
+ '/delete/').first.click()
+ functional.submit(session_browser, form_class='form-delete')
+
+ self._assert_wiki_present(session_browser, file_name_1, present=False)
diff --git a/plinth/modules/featherwiki/urls.py b/plinth/modules/featherwiki/urls.py
new file mode 100644
index 000000000..87efd38a6
--- /dev/null
+++ b/plinth/modules/featherwiki/urls.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""URLs for the Feather Wiki app."""
+
+from django.urls import re_path
+
+from .views import (CreateWikiView, FeatherWikiAppView, RenameWikiView,
+ UploadWikiView, delete)
+
+urlpatterns = [
+ re_path(r'^apps/featherwiki/$', FeatherWikiAppView.as_view(),
+ name='index'),
+ re_path(r'^apps/featherwiki/create/$', CreateWikiView.as_view(),
+ name='create'),
+ re_path(r'^apps/featherwiki/upload/$', UploadWikiView.as_view(),
+ name='upload'),
+ re_path(r'^apps/featherwiki/(?P.+\.html)/rename/$',
+ RenameWikiView.as_view(), name='rename'),
+ re_path(r'^apps/featherwiki/(?P.+\.html)/delete/$', delete,
+ name='delete'),
+]
diff --git a/plinth/modules/featherwiki/views.py b/plinth/modules/featherwiki/views.py
new file mode 100644
index 000000000..7692028c0
--- /dev/null
+++ b/plinth/modules/featherwiki/views.py
@@ -0,0 +1,157 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Django views for Feather Wiki."""
+
+import tempfile
+
+from django.contrib import messages
+from django.contrib.messages.views import SuccessMessageMixin
+from django.shortcuts import redirect
+from django.template.response import TemplateResponse
+from django.urls import reverse_lazy
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import FormView
+
+from plinth import app as app_module
+from plinth import views
+from plinth.modules import featherwiki
+
+from . import privileged
+from .forms import CreateWikiForm, RenameWikiForm, UploadWikiForm
+
+DUPLICATE_FILE_ERROR = _('A wiki file with the given name already exists.')
+
+
+class FeatherWikiAppView(views.AppView):
+ """Serve configuration page."""
+
+ app_id = 'featherwiki'
+ template_name = 'featherwiki_configure.html'
+
+ def get_context_data(self, *args, **kwargs):
+ """Add wikis to the context data."""
+ context = super().get_context_data(*args, **kwargs)
+ context['wikis'] = featherwiki.get_wiki_list()
+ return context
+
+
+class CreateWikiView(SuccessMessageMixin, FormView):
+ """View to create a new repository."""
+
+ form_class = CreateWikiForm
+ prefix = 'featherwiki'
+ template_name = 'form.html'
+ success_url = reverse_lazy('featherwiki:index')
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['title'] = _('Create Wiki')
+ return context
+
+ def form_valid(self, form):
+ """Create the repository on valid form submission."""
+ try:
+ privileged.create_wiki(form.cleaned_data['name'])
+ self.success_message = _('Wiki created.')
+ except ValueError:
+ messages.error(self.request, DUPLICATE_FILE_ERROR)
+ except Exception as error:
+ messages.error(
+ self.request, "{0} {1}".format(
+ _('An error occurred while creating the wiki.'), error))
+
+ return super().form_valid(form)
+
+
+class RenameWikiView(SuccessMessageMixin, FormView):
+ """View to edit an existing repository."""
+
+ form_class = RenameWikiForm
+ prefix = 'featherwiki'
+ template_name = 'form.html'
+ success_url = reverse_lazy('featherwiki:index')
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['title'] = _('Rename Wiki')
+ return context
+
+ def form_valid(self, form):
+ """Rename the wiki on valid form submission."""
+ try:
+ privileged.rename_wiki(self.kwargs['old_name'],
+ form.cleaned_data['new_name'])
+ self.success_message = _('Wiki renamed.')
+ except ValueError:
+ messages.error(self.request, DUPLICATE_FILE_ERROR)
+ except Exception as error:
+ messages.error(
+ self.request, "{0} {1}".format(
+ _('An error occurred while renaming the wiki.'), error))
+
+ return super().form_valid(form)
+
+
+class UploadWikiView(SuccessMessageMixin, FormView):
+ """View to upload an existing wiki file."""
+
+ form_class = UploadWikiForm
+ prefix = 'featherwiki'
+ template_name = 'featherwiki_upload_file.html'
+ success_url = reverse_lazy('featherwiki:index')
+
+ def get_context_data(self, **kwargs):
+ """Return additional context for rendering the template."""
+ context = super().get_context_data(**kwargs)
+ context['title'] = _('Upload Wiki File')
+ return context
+
+ def form_valid(self, form):
+ """Add the wiki file on valid form submission."""
+ multipart_file = self.request.FILES['featherwiki-file']
+
+ try:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ wiki_file_name = temp_dir + '/' + multipart_file.name
+ with open(wiki_file_name, 'wb+') as wiki_file:
+ for chunk in multipart_file.chunks():
+ wiki_file.write(chunk)
+
+ privileged.add_wiki_file(wiki_file_name)
+
+ self.success_message = _('Wiki file added.')
+ except ValueError:
+ messages.error(self.request, DUPLICATE_FILE_ERROR)
+ except Exception as error:
+ messages.error(
+ self.request, "{0} {1}".format(_('Failed to add wiki file.'),
+ error))
+ return redirect(reverse_lazy('featherwiki:index'))
+
+ return super().form_valid(form)
+
+
+def delete(request, name):
+ """Handle deleting wikis, showing a confirmation dialog first.
+
+ On GET, display a confirmation page.
+ On POST, delete the wiki.
+ """
+ app = app_module.App.get('featherwiki')
+ if request.method == 'POST':
+ try:
+ privileged.delete_wiki(name)
+ messages.success(request, _('{name} deleted.').format(name=name))
+ except Exception as error:
+ messages.error(
+ request,
+ _('Could not delete {name}: {error}').format(
+ name=name, error=error))
+
+ return redirect(reverse_lazy('featherwiki:index'))
+
+ return TemplateResponse(request, 'featherwiki_delete.html', {
+ 'title': app.info.name,
+ 'name': name
+ })