tiddlywiki: Add new app

TiddlyWiki uses almost the same Apache configuration as Feather Wiki,
with one difference - disabling gzip for the `HEAD` request.

The FreedomBox app for TiddlyWiki is identical to Feather Wiki in
every other aspect.

- Proxy download through freedombox.org. This serves two purposes:

1. Upstream's website cannot track the IP addresses of FreedomBox users.
2. We can update the versions of the empty quine files without making
   code changes in FreedomBox.

[sunil]

- Update description to correct the list of users who can access the app.

- Update logo to adhere to the logo guidelines.

- Minor styling fix.

- Update the copyright on the logo based on information from upstream git
repository.

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Joseph Nuthalapati 2024-05-05 16:18:12 +05:30 committed by Sunil Mohan Adapa
parent 559a4c30e8
commit 96bd9c8bd4
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
17 changed files with 783 additions and 0 deletions

7
debian/copyright vendored
View File

@ -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 <jeremy@jermolene.com>
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.

View File

@ -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 <a href="{users_url}">'
'any user</a> 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'
])

View File

@ -0,0 +1,26 @@
##
## On all sites, provide TiddlyWiki files on a path: /tiddlywiki
##
Alias /tiddlywiki /var/lib/tiddlywiki
<Location /tiddlywiki>
SetEnvIf Request_Method HEAD no-gzip
Include includes/freedombox-single-sign-on.conf
<IfModule mod_auth_pubtkt.c>
TKTAuthToken "admin" "wiki"
</IfModule>
</Location>
<Directory /var/lib/tiddlywiki>
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
</Directory>

View File

@ -0,0 +1 @@
plinth.modules.tiddlywiki

View File

@ -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.'))

View File

@ -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)]}}

View File

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
viewBox="34 107 384 384"
width="512"
height="512"
id="svg16"
sodipodi:docname="tiddlywiki.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
inkscape:export-filename="tiddlywiki.png"
inkscape:export-xdpi="48"
inkscape:export-ydpi="48"
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"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview18"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false"
inkscape:zoom="0.69046898"
inkscape:cx="-256.34751"
inkscape:cy="155.69128"
inkscape:window-width="1918"
inkscape:window-height="1282"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="svg16" />
<metadata
id="metadata2">
<dc:date>2012-05-10 07:32Z</dc:date>
<!-- Produced by OmniGraffle Professional 5.3.6 -->
</metadata>
<defs
id="defs4" />
<g
stroke="none"
stroke-opacity="1"
stroke-dasharray="none"
fill="none"
fill-opacity="1"
id="g14"
transform="matrix(0.7819711,0,0,0.7819711,0.20115816,91.403102)">
<title
id="title6">Canvas 1</title>
<g
id="g12">
<title
id="title8">Layer 1</title>
<path
d="m 204.10294,372.67294 2.81039,0.8291 c 3.53151,-1.58007 10.63031,0.86197 14.3959,2.05591 -6.934,-7.68695 -17.38058,-18.97509 -24.90698,-26.09145 -2.4704,-8.61546 -1.41632,-17.2848 -0.88481,-26.0799 l 0.10661,-0.7276 c -2.96672,7.0407 -6.73159,13.8847 -8.75512,21.29577 -2.36798,9.99817 10.5243,20.78568 15.5234,26.96817 z m 214.89999,42.28504 c -19.34998,-0.54698 -27.86099,-0.49994 -37.71558,-16.70502 l -7.68051,0.22004 c -8.93988,-0.397 -5.2142,-0.21705 -11.1784,-0.51399 -9.9719,-0.38803 -8.37448,-9.86297 -10.12879,-14.86898 -2.8063,-16.99305 3.71359,-34.07392 3.50791,-51.07032 -0.0728,-6.03332 -8.61032,-27.38909 -11.6604,-35.02423 -9.56162,1.80024 -19.17511,2.14347 -28.8754,2.62683 -22.35922,-0.0548 -44.5668,-2.79281 -66.61382,-6.26983 -4.29641,17.74804 -17.06701,42.58935 -6.5111,60.62682 12.81291,18.65766 21.80439,23.82667 35.7414,24.95164 13.93686,1.12406 17.0839,16.85904 13.71207,22.47903 -2.98447,3.88403 -8.22986,4.58905 -12.68646,5.53003 l -8.9144,0.41898 c -7.01489,-0.23599 -13.28491,-2.12998 -19.53552,-5.051 -10.43848,-5.82696 -21.2195,-17.94095 -29.22959,-26.63797 1.86481,3.47299 2.97712,10.25293 1.28571,13.40802 -4.7359,6.70896 -25.21872,6.66797 -34.59912,2.49897 -10.65598,-4.73502 -36.40497,-37.98197 -40.386,-62.88245 10.591,-20.02872 26.02,-37.47495 33.826,-59.28323 -17.015,-10.85694 -26.128,-28.53113 -24.94499,-48.55152 l 0.427,-2.3175 c -16.74199,3.13418 -8.05998,1.96809 -26.06998,3.33049 -57.356,-0.17549 -107.796001,-39.06484 -79.393993,-99.50579 1.846985,-3.57904 3.603989,-6.833 6.735001,-5.27899 2.512985,1.24695 2.152008,6.24898 0.887985,11.79598 -16.234985,72.21878 63.111997,72.77153 111.887997,59.40782 4.84098,-1.3266 14.46898,-10.2612 21.13848,-13.22311 10.9019,-4.84113 22.7348,-6.8053 34.47801,-8.22059 29.20767,-3.32814 64.31171,12.05838 82.14798,12.56079 17.83648,0.50239 43.20953,-4.27082 58.785,-3.26582 11.30133,0.51708 22.39853,2.55699 33.30252,5.46282 7.05802,-34.3909 7.55701,-59.7379 24.289,-65.6059 9.82001,1.551 17.38696,14.93298 22.98801,22.08301 l 0.023,-0.004 c 11.40697,-0.45001 22.26203,2.44403 33.05499,5.65599 19.54004,-2.77296 35.93702,-13.74597 53.193,-22.28198 -0.054,0.269 -0.33594,0.35998 -0.50397,0.54098 -16.98199,13.73401 -19.35405,36.95803 -17.35602,58.43425 0.74304,11.14415 -2.406,23.24344 -6.29895,34.65357 -7.28503,18.5899 -21.35406,38.18498 -37.68304,37.17997 -6.17298,-0.19526 -9.75901,-3.69059 -14.34699,-7.4223 -0.89001,7.55863 -4.388,14.30321 -7.76001,20.98812 -7.78698,14.82183 -28.13598,21.35339 -46.97802,37.18005 -18.84076,15.8269 6.02902,72.35141 12.05902,82.65039 6.02902,10.29996 22.85998,14.06796 16.32901,23.36392 -1.99799,3.07004 -5.05301,4.16806 -8.31803,5.35904 z"
fill="#000000"
id="path10" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -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 }}
<h3>{% trans "Manage Wikis" %}</h3>
<div class="btn-toolbar">
<a href="{% url 'tiddlywiki:create' %}" class="btn btn-default"
role="button" title="{% trans 'Create Wiki' %}">
<span class="fa fa-plus" aria-hidden="true"></span>
{% trans 'Create Wiki' %}
</a>
<a href="{% url 'tiddlywiki:upload' %}" class="btn btn-default"
role="button" title="{% trans 'Upload Wiki' %}">
<span class="fa fa-upload" aria-hidden="true"></span>
{% trans 'Upload Wiki' %}
</a>
</div>
<div class="row">
<div class="col-md-6">
{% if not wikis %}
<p>{% trans 'No wikis available.' %}</p>
{% else %}
<div id="tiddlywiki-wiki-list" class="list-group list-group-two-column">
{% for wiki in wikis %}
<div class="list-group-item">
<a class="wiki-label" href="/tiddlywiki/{{ wiki }}" target="_blank"
title="{% blocktrans %}Go to wiki {{ wiki }}{% endblocktrans %}">
{{ wiki }}
</a>
<a href="{% url 'tiddlywiki:rename' wiki %}"
class="wiki-edit btn btn-default btn-sm secondary"
role="button"
title="{% blocktrans %}Rename wiki {{ wiki }}{% endblocktrans %}">
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
</a>
<a href="{% url 'tiddlywiki:delete' wiki %}"
class="wiki-delete btn btn-default btn-sm secondary"
role="button"
title="{% blocktrans %}Delete wiki {{ wiki }}{% endblocktrans %}">
<span class="fa fa-trash-o" aria-hidden="true"></span>
</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h3>
{% blocktrans trimmed %}
Delete wiki <em>{{ name }}</em>
{% endblocktrans %}
</h3>
<p>
{% blocktrans trimmed %}
<strong>Hint</strong>: You can download a copy of this wiki from within
TiddlyWiki before deleting it.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Delete this wiki file permanently?</p>
{% endblocktrans %}
</p>
<form class="form form-delete" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-md btn-danger"
value="{% blocktrans %}Delete{% endblocktrans %}"/>
</form>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% comment %}
# SPDX-License-Identifier: AGPL-3.0-or-later
{% endcomment %}
{% load bootstrap %}
{% load i18n %}
{% block content %}
<h3>{{ title }}</h3>
<form class="form form-tiddlywiki" method="post"
enctype="multipart/form-data">
{% csrf_token %}
{{ form|bootstrap }}
<input type="submit" class="btn btn-primary"
value="{% trans "Upload" %}"/>
</form>
{% endblock %}

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<title>Dummy TiddlyWiki File</title>
</head>
<body>
<p> This is a not a real TiddlyWiki file. </p>
</body>
</html>

View File

@ -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)

View File

@ -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<old_name>.+\.html)/rename/$',
RenameWikiView.as_view(), name='rename'),
re_path(r'^apps/tiddlywiki/(?P<name>.+\.html)/delete/$', delete,
name='delete'),
]

View File

@ -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
})