mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
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:
parent
559a4c30e8
commit
96bd9c8bd4
7
debian/copyright
vendored
7
debian/copyright
vendored
@ -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.
|
||||
|
||||
126
plinth/modules/tiddlywiki/__init__.py
Normal file
126
plinth/modules/tiddlywiki/__init__.py
Normal 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'
|
||||
])
|
||||
@ -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>
|
||||
@ -0,0 +1 @@
|
||||
plinth.modules.tiddlywiki
|
||||
37
plinth/modules/tiddlywiki/forms.py
Normal file
37
plinth/modules/tiddlywiki/forms.py
Normal 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.'))
|
||||
16
plinth/modules/tiddlywiki/manifest.py
Normal file
16
plinth/modules/tiddlywiki/manifest.py
Normal 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)]}}
|
||||
92
plinth/modules/tiddlywiki/privileged.py
Normal file
92
plinth/modules/tiddlywiki/privileged.py
Normal 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)
|
||||
BIN
plinth/modules/tiddlywiki/static/icons/tiddlywiki.png
Normal file
BIN
plinth/modules/tiddlywiki/static/icons/tiddlywiki.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
65
plinth/modules/tiddlywiki/static/icons/tiddlywiki.svg
Normal file
65
plinth/modules/tiddlywiki/static/icons/tiddlywiki.svg
Normal 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 |
@ -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 %}
|
||||
37
plinth/modules/tiddlywiki/templates/tiddlywiki_delete.html
Normal file
37
plinth/modules/tiddlywiki/templates/tiddlywiki_delete.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
0
plinth/modules/tiddlywiki/tests/__init__.py
Normal file
0
plinth/modules/tiddlywiki/tests/__init__.py
Normal file
9
plinth/modules/tiddlywiki/tests/data/dummy_wiki.html
Normal file
9
plinth/modules/tiddlywiki/tests/data/dummy_wiki.html
Normal 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>
|
||||
107
plinth/modules/tiddlywiki/tests/test_functional.py
Normal file
107
plinth/modules/tiddlywiki/tests/test_functional.py
Normal 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)
|
||||
20
plinth/modules/tiddlywiki/urls.py
Normal file
20
plinth/modules/tiddlywiki/urls.py
Normal 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'),
|
||||
]
|
||||
157
plinth/modules/tiddlywiki/views.py
Normal file
157
plinth/modules/tiddlywiki/views.py
Normal 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
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user