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 @@ + + + + + 2012-05-10 07:32Z + + + + + Canvas 1 + + Layer 1 + + + + 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" %}

+ +
+ + + {% trans 'Create Wiki' %} + + + + {% trans 'Upload Wiki' %} + +
+ +
+
+ {% 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 %} +

+ +
+ {% csrf_token %} + + +
+ +{% 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 }}

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ +{% 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 + })