diff --git a/debian/copyright b/debian/copyright
index 64b25d866..577de3278 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -284,6 +284,13 @@ Copyright: Jakob Borg and the Syncthing project
Comment: https://commons.wikimedia.org/wiki/File:SyncthingLogoHorizontal.svg
License: MPL-2.0
+Files: plinth/modules/tiddlywiki/static/icons/tiddlywiki.svg
+ plinth/modules/tiddlywiki/static/icons/tiddlywiki.png
+Copyright: 2004-2007 Jeremy Ruston
+ 2007-2016 UnaMesa Association
+Comment: https://github.com/Jermolene/TiddlyWiki5/blob/086506012d98e9db34c7d96dc27aea249a9bdbc8/editions/introduction/tiddlers/images/Motovun%20Jack.svg
+License: BSD-3-clause
+
Files: plinth/modules/tor/static/icons/tor.png
plinth/modules/tor/static/icons/tor.svg
Copyright: The Tor Project, Inc.
diff --git a/plinth/modules/tiddlywiki/__init__.py b/plinth/modules/tiddlywiki/__init__.py
new file mode 100644
index 000000000..dd7d673ed
--- /dev/null
+++ b/plinth/modules/tiddlywiki/__init__.py
@@ -0,0 +1,126 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+FreedomBox app for TiddlyWiki.
+
+This is a FreedomBox-native implementation of a TiddlyWiki 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(
+ _('TiddlyWiki is an interactive application that runs entirely in the '
+ 'web browser. Each wiki is a self-contained HTML file stored on your'
+ ' {box_name}. Instead of writing long wiki pages, TiddlyWiki '
+ 'encourages you to write several short notes called Tiddlers and '
+ 'link them together into a dense graph.'), box_name=cfg.box_name),
+ _('It is a versatile application with a wide variety of use cases - '
+ 'non-linear notebook, website, personal knowledge base, task and project'
+ ' management system, personal diary etc. Plugins can extend the '
+ 'functionality of TiddlyWiki. Encrypting individual tiddlers or '
+ 'password-protecting a wiki file is possible from within the '
+ 'application.'),
+ format_lazy(
+ _('TiddlyWiki is downloaded from {box_name} website and not from '
+ 'Debian. Wikis need to be upgraded to newer version manually.'),
+ box_name=_(cfg.box_name)),
+ 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')),
+ _('Create a new wiki or upload your existing wiki file to get started.')
+]
+
+
+class TiddlyWikiApp(app_module.App):
+ """FreedomBox app for TiddlyWiki."""
+
+ app_id = 'tiddlywiki'
+
+ _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=_('TiddlyWiki'),
+ icon_filename='tiddlywiki',
+ short_description=_('Non-linear Notebooks'),
+ description=_description,
+ manual_page='TiddlyWiki',
+ clients=manifest.clients)
+ self.add(info)
+
+ menu_item = menu.Menu('menu-tiddlywiki', info.name,
+ info.short_description, info.icon_filename,
+ 'tiddlywiki: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-tiddlywiki', info.name,
+ short_description=info.short_description, icon=info.icon_filename,
+ description=info.description, manual_page=info.manual_page,
+ url='/tiddlywiki/', clients=info.clients, login_required=True,
+ allowed_groups=list(groups))
+ self.add(shortcut)
+
+ dropin_configs = DropinConfigs('dropin-configs-tiddlywiki', [
+ '/etc/apache2/conf-available/tiddlywiki-freedombox.conf',
+ ])
+ self.add(dropin_configs)
+
+ firewall = Firewall('firewall-tiddlywiki', info.name,
+ ports=['http', 'https'], is_external=True)
+ self.add(firewall)
+
+ webserver = Webserver('webserver-tiddlywiki', 'tiddlywiki-freedombox')
+ self.add(webserver)
+
+ users_and_groups = UsersAndGroups('users-and-groups-tiddlywiki',
+ groups=groups)
+ self.add(users_and_groups)
+
+ backup_restore = BackupRestore('backup-restore-tiddlywiki',
+ **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 TiddlyWiki files."""
+ return sorted([
+ path.name for path in privileged.wiki_dir.iterdir()
+ if path.suffix == '.html'
+ ])
diff --git a/plinth/modules/tiddlywiki/data/usr/share/freedombox/etc/apache2/conf-available/tiddlywiki-freedombox.conf b/plinth/modules/tiddlywiki/data/usr/share/freedombox/etc/apache2/conf-available/tiddlywiki-freedombox.conf
new file mode 100644
index 000000000..f968db2c1
--- /dev/null
+++ b/plinth/modules/tiddlywiki/data/usr/share/freedombox/etc/apache2/conf-available/tiddlywiki-freedombox.conf
@@ -0,0 +1,26 @@
+##
+## On all sites, provide TiddlyWiki files on a path: /tiddlywiki
+##
+
+Alias /tiddlywiki /var/lib/tiddlywiki
+
+
+ SetEnvIf Request_Method HEAD no-gzip
+ 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/tiddlywiki/data/usr/share/freedombox/modules-enabled/tiddlywiki b/plinth/modules/tiddlywiki/data/usr/share/freedombox/modules-enabled/tiddlywiki
new file mode 100644
index 000000000..be7632a2b
--- /dev/null
+++ b/plinth/modules/tiddlywiki/data/usr/share/freedombox/modules-enabled/tiddlywiki
@@ -0,0 +1 @@
+plinth.modules.tiddlywiki
diff --git a/plinth/modules/tiddlywiki/forms.py b/plinth/modules/tiddlywiki/forms.py
new file mode 100644
index 000000000..227aa1601
--- /dev/null
+++ b/plinth/modules/tiddlywiki/forms.py
@@ -0,0 +1,37 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Django forms for configuring TiddlyWiki."""
+
+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 TiddlyWiki file with .html file extension'),
+ required=True, validators=[
+ validators.FileExtensionValidator(
+ ['html'], _('TiddlyWiki files must be in HTML format'))
+ ], help_text=_(
+ 'Upload an existing TiddlyWiki file from this computer.'))
diff --git a/plinth/modules/tiddlywiki/manifest.py b/plinth/modules/tiddlywiki/manifest.py
new file mode 100644
index 000000000..9b9f4d29f
--- /dev/null
+++ b/plinth/modules/tiddlywiki/manifest.py
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Application manifest for TiddlyWiki."""
+
+from django.utils.translation import gettext_lazy as _
+
+from .privileged import wiki_dir
+
+clients = [{
+ 'name': _('TiddlyWiki'),
+ 'platforms': [{
+ 'type': 'web',
+ 'url': '/tiddlywiki/'
+ }]
+}]
+
+backup = {'data': {'directories': [str(wiki_dir)]}}
diff --git a/plinth/modules/tiddlywiki/privileged.py b/plinth/modules/tiddlywiki/privileged.py
new file mode 100644
index 000000000..3f982794d
--- /dev/null
+++ b/plinth/modules/tiddlywiki/privileged.py
@@ -0,0 +1,92 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Configure TiddlyWiki."""
+
+import pathlib
+import re
+import shutil
+import tempfile
+import urllib.request
+
+from plinth.actions import privileged
+
+EMPTY_WIKI_FILE = 'https://ftp.freedombox.org/pub/tiddlywiki/empty.html'
+
+wiki_dir = pathlib.Path('/var/lib/tiddlywiki')
+
+
+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 TiddlyWiki."""
+ 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: str):
+ """Add an uploaded wiki file."""
+ upload_file_path = pathlib.Path(upload_file)
+ 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/tiddlywiki/static/icons/tiddlywiki.png b/plinth/modules/tiddlywiki/static/icons/tiddlywiki.png
new file mode 100644
index 000000000..f85ccb6cf
Binary files /dev/null and b/plinth/modules/tiddlywiki/static/icons/tiddlywiki.png differ
diff --git a/plinth/modules/tiddlywiki/static/icons/tiddlywiki.svg b/plinth/modules/tiddlywiki/static/icons/tiddlywiki.svg
new file mode 100644
index 000000000..1b4f1cd36
--- /dev/null
+++ b/plinth/modules/tiddlywiki/static/icons/tiddlywiki.svg
@@ -0,0 +1,65 @@
+
+
diff --git a/plinth/modules/tiddlywiki/templates/tiddlywiki_configure.html b/plinth/modules/tiddlywiki/templates/tiddlywiki_configure.html
new file mode 100644
index 000000000..75cb40b9d
--- /dev/null
+++ b/plinth/modules/tiddlywiki/templates/tiddlywiki_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/tiddlywiki/templates/tiddlywiki_delete.html b/plinth/modules/tiddlywiki/templates/tiddlywiki_delete.html
new file mode 100644
index 000000000..ec2bd20bb
--- /dev/null
+++ b/plinth/modules/tiddlywiki/templates/tiddlywiki_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
+ TiddlyWiki before deleting it.
+ {% endblocktrans %}
+
+
+
+ {% blocktrans trimmed %}
+ Delete this wiki file permanently?
+ {% endblocktrans %}
+
+
+
+
+{% endblock %}
diff --git a/plinth/modules/tiddlywiki/templates/tiddlywiki_upload_file.html b/plinth/modules/tiddlywiki/templates/tiddlywiki_upload_file.html
new file mode 100644
index 000000000..f26c32703
--- /dev/null
+++ b/plinth/modules/tiddlywiki/templates/tiddlywiki_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/tiddlywiki/tests/__init__.py b/plinth/modules/tiddlywiki/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/plinth/modules/tiddlywiki/tests/data/dummy_wiki.html b/plinth/modules/tiddlywiki/tests/data/dummy_wiki.html
new file mode 100644
index 000000000..99ed9e761
--- /dev/null
+++ b/plinth/modules/tiddlywiki/tests/data/dummy_wiki.html
@@ -0,0 +1,9 @@
+
+
+
+ Dummy TiddlyWiki File
+
+
+ This is a not a real TiddlyWiki file.
+
+
diff --git a/plinth/modules/tiddlywiki/tests/test_functional.py b/plinth/modules/tiddlywiki/tests/test_functional.py
new file mode 100644
index 000000000..aa603a6a1
--- /dev/null
+++ b/plinth/modules/tiddlywiki/tests/test_functional.py
@@ -0,0 +1,107 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Functional, browser based tests for TiddlyWiki app."""
+
+import pathlib
+
+import pytest
+
+from plinth.tests import functional
+
+pytestmark = [pytest.mark.apps, pytest.mark.tiddlywiki]
+
+wiki_name = 'Engineering Daybook'
+file_name = 'Engineering_Daybook.html'
+
+
+class TestTiddlyWikiApp(functional.BaseAppTests):
+ app_name = 'tiddlywiki'
+ 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, 'tiddlywiki')
+
+ wiki_link = f'/tiddlywiki/{file_name}'
+ if self._get_links_in_app_page(session_browser, wiki_link):
+ return
+
+ session_browser.links.find_by_href(
+ '/plinth/apps/tiddlywiki/create/').first.click()
+ session_browser.find_by_id('id_tiddlywiki-name').fill(wiki_name)
+ functional.submit(session_browser, form_class='form-tiddlywiki')
+
+ 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, 'tiddlywiki')
+ 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}/tiddlywiki')
+ 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'/tiddlywiki/{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'/tiddlywiki/{file_name}'
+ default_url = functional.config['DEFAULT']['url']
+ session_browser.visit(f'{default_url}{wiki_link}')
+ links = session_browser.links.find_by_href(
+ 'https://tiddlywiki.com/#GettingStarted')
+ 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)
+ self._assert_wiki_works(session_browser, file_name)
+
+ def test_rename_wiki_file(self, session_browser):
+ """Test changing the name of a wiki file."""
+ self._create_wiki_file(session_browser)
+
+ new_wiki_name = 'A Midsummer Night\'s Dream'
+ new_file_name = 'A_Midsummer_Nights_Dream.html'
+ self._get_links_in_app_page(
+ session_browser,
+ '/plinth/apps/tiddlywiki/' + file_name + '/rename/').first.click()
+ session_browser.find_by_id('id_tiddlywiki-new_name').fill(
+ new_wiki_name)
+ functional.submit(session_browser, form_class='form-tiddlywiki')
+
+ 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/tiddlywiki/upload/').first.click()
+ session_browser.attach_file('tiddlywiki-file', test_wiki_file)
+ functional.submit(session_browser, form_class='form-tiddlywiki')
+
+ 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/tiddlywiki/' + file_name + '/delete/').first.click()
+ functional.submit(session_browser, form_class='form-delete')
+
+ self._assert_wiki_present(session_browser, file_name, present=False)
diff --git a/plinth/modules/tiddlywiki/urls.py b/plinth/modules/tiddlywiki/urls.py
new file mode 100644
index 000000000..2a395f2b8
--- /dev/null
+++ b/plinth/modules/tiddlywiki/urls.py
@@ -0,0 +1,20 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""URLs for the TiddlyWiki app."""
+
+from django.urls import re_path
+
+from .views import (CreateWikiView, TiddlyWikiAppView, RenameWikiView,
+ UploadWikiView, delete)
+
+urlpatterns = [
+ re_path(r'^apps/tiddlywiki/$', TiddlyWikiAppView.as_view(),
+ name='index'),
+ re_path(r'^apps/tiddlywiki/create/$', CreateWikiView.as_view(),
+ name='create'),
+ re_path(r'^apps/tiddlywiki/upload/$', UploadWikiView.as_view(),
+ name='upload'),
+ re_path(r'^apps/tiddlywiki/(?P.+\.html)/rename/$',
+ RenameWikiView.as_view(), name='rename'),
+ re_path(r'^apps/tiddlywiki/(?P.+\.html)/delete/$', delete,
+ name='delete'),
+]
diff --git a/plinth/modules/tiddlywiki/views.py b/plinth/modules/tiddlywiki/views.py
new file mode 100644
index 000000000..324a827c5
--- /dev/null
+++ b/plinth/modules/tiddlywiki/views.py
@@ -0,0 +1,157 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Django views for TiddlyWiki."""
+
+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 tiddlywiki
+
+from . import privileged
+from .forms import CreateWikiForm, RenameWikiForm, UploadWikiForm
+
+DUPLICATE_FILE_ERROR = _('A wiki file with the given name already exists.')
+
+
+class TiddlyWikiAppView(views.AppView):
+ """Serve configuration page."""
+
+ app_id = 'tiddlywiki'
+ template_name = 'tiddlywiki_configure.html'
+
+ def get_context_data(self, *args, **kwargs):
+ """Add wikis to the context data."""
+ context = super().get_context_data(*args, **kwargs)
+ context['wikis'] = tiddlywiki.get_wiki_list()
+ return context
+
+
+class CreateWikiView(SuccessMessageMixin, FormView):
+ """View to create a new repository."""
+
+ form_class = CreateWikiForm
+ prefix = 'tiddlywiki'
+ template_name = 'form.html'
+ success_url = reverse_lazy('tiddlywiki: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 = 'tiddlywiki'
+ template_name = 'form.html'
+ success_url = reverse_lazy('tiddlywiki: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 = 'tiddlywiki'
+ template_name = 'tiddlywiki_upload_file.html'
+ success_url = reverse_lazy('tiddlywiki: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['tiddlywiki-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('tiddlywiki: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('tiddlywiki')
+ 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('tiddlywiki:index'))
+
+ return TemplateResponse(request, 'tiddlywiki_delete.html', {
+ 'title': app.info.name,
+ 'name': name
+ })