mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-26 12:46:08 +00:00
doc: Add developer documentation using Sphinx
- This is completely reworked but based on /Developer page in the FreedomBox Manual. - This documentation can be made available as static site on https://docs.freedombox.org and the /Developer page in the FreedomBox Manual can be dropped. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
108f7b8d46
commit
80498919fb
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@ doc/manual/*/*.html
|
|||||||
doc/manual/*/*.xml
|
doc/manual/*/*.xml
|
||||||
!doc/manual/*/*.raw.xml
|
!doc/manual/*/*.raw.xml
|
||||||
doc/plinth.1
|
doc/plinth.1
|
||||||
|
doc/dev/_build
|
||||||
\#*
|
\#*
|
||||||
.#*
|
.#*
|
||||||
*~
|
*~
|
||||||
|
|||||||
3
debian/copyright
vendored
3
debian/copyright
vendored
@ -10,6 +10,9 @@ Copyright: 2011-2019 FreedomBox Authors
|
|||||||
License: AGPL-3+
|
License: AGPL-3+
|
||||||
|
|
||||||
Files: doc/*.xml
|
Files: doc/*.xml
|
||||||
|
doc/*.rst
|
||||||
|
doc/*.png
|
||||||
|
doc/*.jpg
|
||||||
Copyright: 2011-2019 FreedomBox Authors
|
Copyright: 2011-2019 FreedomBox Authors
|
||||||
License: CC-BY-SA-4.0
|
License: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
|||||||
41
doc/dev/Makefile
Normal file
41
doc/dev/Makefile
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#
|
||||||
|
# This file is part of FreedomBox.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SPHINXAUTOBUILD = sphinx-autobuild
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
livehtml: Makefile
|
||||||
|
@$(SPHINXAUTOBUILD) -b html -r "\\.#.*" $(SPHINXOPTS) "$(SOURCEDIR)" "$(BUILDDIR)/html"
|
||||||
0
doc/dev/_static/.gitkeep
Normal file
0
doc/dev/_static/.gitkeep
Normal file
15
doc/dev/_templates/layout.html
Normal file
15
doc/dev/_templates/layout.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{%- extends "alabaster/layout.html" %}
|
||||||
|
|
||||||
|
{%- block footer %}
|
||||||
|
<div class="footer">
|
||||||
|
{% if show_copyright %}©{{ copyright }} | {% endif %}
|
||||||
|
Licensed under the <a href="https://creativecommons.org/licenses/by-sa/4.0/">
|
||||||
|
CC BY-SA 4.0</a> license
|
||||||
|
{%- if show_source and has_source and sourcename %}
|
||||||
|
{% if show_copyright or theme_show_powered_by %}|{% endif %}
|
||||||
|
<a href="{{ pathto('_sources/' + sourcename, true)|e }}"
|
||||||
|
rel="nofollow">{{ _('Page source') }}</a>
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
209
doc/dev/conf.py
Normal file
209
doc/dev/conf.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# This file is part of FreedomBox.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Configuration file for the Sphinx documentation builder.
|
||||||
|
|
||||||
|
This file does only contain a selection of the most common options. For a full
|
||||||
|
list see the documentation: http://www.sphinx-doc.org/en/master/config
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('../../'))
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
project = 'FreedomBox'
|
||||||
|
copyright = '2019, FreedomBox Authors'
|
||||||
|
author = 'FreedomBox Authors'
|
||||||
|
|
||||||
|
# The short X.Y version
|
||||||
|
version = ''
|
||||||
|
# The full version, including alpha/beta/rc tags
|
||||||
|
release = ''
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.todo',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = None
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
html_theme_options = {
|
||||||
|
'fixed_sidebar': True,
|
||||||
|
'show_related': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
|
# to template names.
|
||||||
|
#
|
||||||
|
# The default sidebars (for documents that don't match any pattern) are
|
||||||
|
# defined by theme itself. Builtin themes are using these templates by
|
||||||
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||||
|
# 'searchbox.html']``.
|
||||||
|
#
|
||||||
|
# html_sidebars = {}
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ---------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'FreedomBoxdoc'
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'FreedomBox.tex', 'FreedomBox Documentation',
|
||||||
|
'FreedomBox Authors', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# -- Options for manual page output ------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [(master_doc, 'freedombox', 'FreedomBox Documentation', [author],
|
||||||
|
1)]
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ----------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'FreedomBox', 'FreedomBox Documentation', author,
|
||||||
|
'FreedomBox', 'One line description of project.', 'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# -- Options for Epub output -------------------------------------------------
|
||||||
|
|
||||||
|
# Bibliographic Dublin Core info.
|
||||||
|
epub_title = project
|
||||||
|
|
||||||
|
# The unique identifier of the text. This can be a ISBN number
|
||||||
|
# or the project homepage.
|
||||||
|
#
|
||||||
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
# A unique identification for the text.
|
||||||
|
#
|
||||||
|
# epub_uid = ''
|
||||||
|
|
||||||
|
# A list of files that should not be packed into the epub file.
|
||||||
|
epub_exclude_files = ['search.html']
|
||||||
|
|
||||||
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|
||||||
|
# -- Options for intersphinx extension ---------------------------------------
|
||||||
|
|
||||||
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
|
intersphinx_mapping = {
|
||||||
|
'python': ('https://docs.python.org/3', None),
|
||||||
|
'django': ('https://docs.djangoproject.com/en/stable/',
|
||||||
|
'https://docs.djangoproject.com/en/stable/_objects/'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- Options for todo extension ----------------------------------------------
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = True
|
||||||
|
|
||||||
|
# -- Options for autodoc extension -------------------------------------------
|
||||||
|
|
||||||
|
autodoc_default_options = {
|
||||||
|
'special-members': '__init__',
|
||||||
|
}
|
||||||
34
doc/dev/index.rst
Normal file
34
doc/dev/index.rst
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
FreedomBox Developer Documentation
|
||||||
|
==================================
|
||||||
|
|
||||||
|
This manual is meant for developers intending to develop app for FreedomBox. It
|
||||||
|
provides a step by step tutorial and an API reference.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
tutorial/index
|
||||||
|
reference/index
|
||||||
|
|
||||||
|
External References
|
||||||
|
===================
|
||||||
|
|
||||||
|
#. :doc:`Django Documentation - Getting Started <django:intro/index>`
|
||||||
|
|
||||||
|
#. `Debian Packaging Portal <https://wiki.debian.org/Packaging>`_
|
||||||
|
|
||||||
|
#. `systemd System and Service Manager <https://www.freedesktop.org/wiki/Software/systemd/>`_
|
||||||
|
|
||||||
|
#. `Bootstrap - CSS Library <http://getbootstrap.com/css/>`_
|
||||||
|
|
||||||
|
#. `FreedomBox User Manual <https://wiki.debian.org/FreedomBox/Manual>`_
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
10
doc/dev/reference/action_utils.rst
Normal file
10
doc/dev/reference/action_utils.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Action Utils
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Several utilities to help with the implementation of actions and diagnostic
|
||||||
|
tests are implemented in this module.
|
||||||
|
|
||||||
|
.. automodule:: plinth.action_utils
|
||||||
|
:members:
|
||||||
16
doc/dev/reference/actions.rst
Normal file
16
doc/dev/reference/actions.rst
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Actions
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
FreedomBox's web front does not directly change any aspect of the underlying
|
||||||
|
operating system. Instead, it calls upon **actions**, as shell commands. Actions
|
||||||
|
live in ``/usr/share/plinth/actions`` directory. They require no interaction
|
||||||
|
beyond passing command line arguments or taking sensitive arguments via stdin.
|
||||||
|
They change the operation of the services and apps of the FreedomBox and nothing
|
||||||
|
else. These actions are also directly usable by a skilled administrator.
|
||||||
|
|
||||||
|
The following documentation for the ``actions`` module.
|
||||||
|
|
||||||
|
.. automodule:: plinth.actions
|
||||||
|
:members: run, superuser_run, run_as_user, _run
|
||||||
7
doc/dev/reference/app.rst
Normal file
7
doc/dev/reference/app.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
App
|
||||||
|
---
|
||||||
|
|
||||||
|
.. autoclass:: plinth.app.App
|
||||||
|
:members:
|
||||||
37
doc/dev/reference/app_module.rst
Normal file
37
doc/dev/reference/app_module.rst
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
App Module
|
||||||
|
----------
|
||||||
|
|
||||||
|
These methods are optionally provided by the module in which an app is
|
||||||
|
implemented and FreedomBox calls/uses them if they are present.
|
||||||
|
|
||||||
|
<app-module>.init()
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Optional. This method is called by FreedomBox soon after all the applications
|
||||||
|
are loaded. The ``init()`` call order guarantees that other applications that
|
||||||
|
this application depends on will be initialized before this application is
|
||||||
|
initialized.
|
||||||
|
|
||||||
|
<app-module>.diagnose()
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Optional. Called when the user invokes system-wide diagnostics by visiting
|
||||||
|
**System -> Diagnositcs**. This method must return an array of diagnostic
|
||||||
|
results. Each diagnostic result must be a two-tuple with first element as a
|
||||||
|
string that is shown to the user as name of the test and second element is the
|
||||||
|
result of the test. It must be one of ``passed``, ``failed``, ``error``. Example
|
||||||
|
return value:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
[('Check http://localhost/app is reachable', 'passed'),
|
||||||
|
('Check configuration is sane', 'passed')]
|
||||||
|
|
||||||
|
<app-module>.depends
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Optional. This module property must contain a list of all apps that this
|
||||||
|
application depends on. The application is specified as string containing the
|
||||||
|
full module load path. For example, ``names``.
|
||||||
7
doc/dev/reference/components/daemon.rst
Normal file
7
doc/dev/reference/components/daemon.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Daemon
|
||||||
|
^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.daemon.Daemon
|
||||||
|
:members:
|
||||||
10
doc/dev/reference/components/domain.rst
Normal file
10
doc/dev/reference/components/domain.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Domain Name
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.modules.names.components.DomainName
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: plinth.modules.names.components.DomainType
|
||||||
|
:members:
|
||||||
7
doc/dev/reference/components/firewall.rst
Normal file
7
doc/dev/reference/components/firewall.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Firewall
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.modules.firewall.components.Firewall
|
||||||
|
:members:
|
||||||
7
doc/dev/reference/components/frontpage.rst
Normal file
7
doc/dev/reference/components/frontpage.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Frontpage
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.frontpage.Shortcut
|
||||||
|
:members:
|
||||||
27
doc/dev/reference/components/index.rst
Normal file
27
doc/dev/reference/components/index.rst
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Components
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:caption: Available components:
|
||||||
|
|
||||||
|
menu
|
||||||
|
daemon
|
||||||
|
firewall
|
||||||
|
webserver
|
||||||
|
frontpage
|
||||||
|
domain
|
||||||
|
letsencrypt
|
||||||
|
|
||||||
|
Base Classes
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.app.Component
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: plinth.app.LeaderComponent
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: plinth.app.FollowerComponent
|
||||||
|
:members:
|
||||||
7
doc/dev/reference/components/letsencrypt.rst
Normal file
7
doc/dev/reference/components/letsencrypt.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Let's Encrypt
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.modules.letsencrypt.components.LetsEncrypt
|
||||||
|
:members:
|
||||||
7
doc/dev/reference/components/menu.rst
Normal file
7
doc/dev/reference/components/menu.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Menu
|
||||||
|
^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.menu.Menu
|
||||||
|
:members:
|
||||||
10
doc/dev/reference/components/webserver.rst
Normal file
10
doc/dev/reference/components/webserver.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Webserver
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: plinth.modules.apache.components.Webserver
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: plinth.modules.apache.components.Uwsgi
|
||||||
|
:members:
|
||||||
13
doc/dev/reference/forms.rst
Normal file
13
doc/dev/reference/forms.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Forms
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. autoclass:: plinth.forms.AppForm
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: plinth.forms.DomainSelectionForm
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: plinth.forms.CheckboxSelectMultipleWithReadOnly
|
||||||
|
:members:
|
||||||
23
doc/dev/reference/index.rst
Normal file
23
doc/dev/reference/index.rst
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
=========
|
||||||
|
Reference
|
||||||
|
=========
|
||||||
|
|
||||||
|
This section describes the FreedomBox API that is most frequently used by apps.
|
||||||
|
Note that since FreedomBox is under development and has not yet declared a
|
||||||
|
stable API, this API is subject to change. This is not usually a problem because
|
||||||
|
all the FreedomBox apps currently reside in FreedomBox source repository itself
|
||||||
|
and are updated when the API is updated.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
app
|
||||||
|
components/index
|
||||||
|
app_module
|
||||||
|
actions
|
||||||
|
action_utils
|
||||||
|
views
|
||||||
|
forms
|
||||||
|
|
||||||
|
.. automodule:: plinth.modules.ttrss
|
||||||
7
doc/dev/reference/views.rst
Normal file
7
doc/dev/reference/views.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Views
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. autoclass:: plinth.views.AppView
|
||||||
|
:members:
|
||||||
65
doc/dev/tutorial/beginning.rst
Normal file
65
doc/dev/tutorial/beginning.rst
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 1: Beginning
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Before we begin
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
FreedomBox Service (Plinth) is a web interface built using Python3 and Django.
|
||||||
|
FreedomBox apps are simply Django applications within the project. Hence, for
|
||||||
|
the most part, writing a FreedomBox app is all about :doc:`writing a Django
|
||||||
|
application <django:intro/tutorial01>`.
|
||||||
|
|
||||||
|
You should start by reading the :doc:`Django tutorial <django:intro/index>`. All
|
||||||
|
the concepts described there are applicable for how FreedomBox and its apps are
|
||||||
|
be built.
|
||||||
|
|
||||||
|
Picking an app
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
We must first, of course, pick an application to add to FreedomBox. For the
|
||||||
|
purpose of this tutorial, let us pick Transmission. Transmission daemon handles
|
||||||
|
Bitorrent file sharing. BitTorrent is a peer-to-peer file sharing protocol.
|
||||||
|
|
||||||
|
.. important:: Choosing an app
|
||||||
|
|
||||||
|
When choosing an application we must make sure that it respects users'
|
||||||
|
freedom and privacy. By choosing to use FreedomBox, users have explicitly made
|
||||||
|
a choice to keep the data with themselves, to not provide privacy compromising
|
||||||
|
data to centralized entities and to use Free Software that respects their
|
||||||
|
Software Freedom. These are not properties of *some* of the applications in
|
||||||
|
FreedomBox but all applications *must* adhere to these principles. Apps should
|
||||||
|
not even ask the users questions to this effect, because users have already
|
||||||
|
made a choice.
|
||||||
|
|
||||||
|
Packaging the application
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Majority of the effort in creating an application for FreedomBox is to package
|
||||||
|
it for Debian and get it uploaded to Debian repositories. Going through the
|
||||||
|
process of packaging itself is outside the scope of this tutorial. It is,
|
||||||
|
however, well documented elsewhere. You should start at the `Debian Packaging
|
||||||
|
Portal <https://wiki.debian.org/Packaging>`_.
|
||||||
|
|
||||||
|
Debian packaging might seem like an unnecessary process that takes time with its
|
||||||
|
adherence to standards, review process, legal checks, etc. However, upon close
|
||||||
|
examination, one will find that without these steps the goals of the FreedomBox
|
||||||
|
project cannot be met. Some of the advantages of Debian packaging are listed
|
||||||
|
below:
|
||||||
|
|
||||||
|
* Legal check ensures that proprietary licensed code or code with bad licenses
|
||||||
|
does not inadvertently creep in.
|
||||||
|
|
||||||
|
* Libraries have to be packaged separately easing security handling. When a
|
||||||
|
security vulnerability is identified in a library, just the library will have
|
||||||
|
to be updated and not all the applications that depend on it.
|
||||||
|
|
||||||
|
* Upgrades become smoother. The dependency handling of the packaging system,
|
||||||
|
configuration handling tools, tools to deal with various types of well known
|
||||||
|
files help with ensuring a proper upgrade.
|
||||||
|
|
||||||
|
* Collaborative maintenance teams ensure that the package is well cared for even
|
||||||
|
if you get busy with other work and can't spend time on your package.
|
||||||
|
Following standards and using common infrastructure is critical to enable this
|
||||||
|
development methodology.
|
||||||
61
doc/dev/tutorial/code.rst
Normal file
61
doc/dev/tutorial/code.rst
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Full Code
|
||||||
|
---------
|
||||||
|
|
||||||
|
Transmission app is already included in FreedomBox. Here is the full source for
|
||||||
|
the module for reference.
|
||||||
|
|
||||||
|
plinth/modules/transmission/__init__.py
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/__init__.py
|
||||||
|
:language: python3
|
||||||
|
|
||||||
|
plinth/modules/transmission/forms.py
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/forms.py
|
||||||
|
:language: python3
|
||||||
|
|
||||||
|
plinth/modules/transmission/manifest.py
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/manifest.py
|
||||||
|
:language: python3
|
||||||
|
|
||||||
|
plinth/modules/transmission/urls.py
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/urls.py
|
||||||
|
:language: python3
|
||||||
|
|
||||||
|
plinth/modules/transmission/views.py
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/views.py
|
||||||
|
:language: python3
|
||||||
|
|
||||||
|
plinth/modules/transmission/data/etc/plinth/modules-enabled/transmission
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/data/etc/plinth/modules-enabled/transmission
|
||||||
|
:language: text
|
||||||
|
|
||||||
|
plinth/modules/transmission/data/etc/apache2/conf-available/transmission-plinth.conf
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/data/etc/apache2/conf-available/transmission-plinth.conf
|
||||||
|
:language: apache
|
||||||
|
|
||||||
|
plinth/modules/transmission/tests/__init__.py
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../plinth/modules/transmission/tests/__init__.py
|
||||||
|
:language: python3
|
||||||
|
|
||||||
|
actions/transmission
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../actions/transmission
|
||||||
|
:language: python3
|
||||||
181
doc/dev/tutorial/components.rst
Normal file
181
doc/dev/tutorial/components.rst
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 4: Components
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Managing a daemon
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Transmission, like many services in the FreedomBox, requires a daemon to be
|
||||||
|
running in the system to work. When the app is enabled, the daemon should be
|
||||||
|
enabled. When the app is disabled, the daemon should be disabled. We should also
|
||||||
|
show the status of whether the daemon is running in the app's view. All of these
|
||||||
|
concerns are automatically handled by the framework if a
|
||||||
|
:class:`~plinth.daemon.Daemon` component is added to the app. Let us do that in
|
||||||
|
our app's class.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.daemon import Daemon
|
||||||
|
|
||||||
|
managed_services = ['transmission-daemon']
|
||||||
|
|
||||||
|
class TransmissionApp(app_module.App):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
daemon = Daemon('daemon-transmission', managed_services[0])
|
||||||
|
self.add(daemon)
|
||||||
|
|
||||||
|
|
||||||
|
The first argument to instantiate the :class:`~plinth.daemon.Daemon` class is a
|
||||||
|
unique ID. The second is the name of the `systemd
|
||||||
|
<https://www.freedesktop.org/wiki/Software/systemd/>`_ unit file which manages
|
||||||
|
the daemon.
|
||||||
|
|
||||||
|
Managing web server configuration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Transmission provides a web interface to the user. This web interface needs to
|
||||||
|
be proxied through a web server for security and access control. We will need to
|
||||||
|
write a configuration snippet for Apache, the default web server on FreedomBox.
|
||||||
|
This configuration snippet needs to be activated when our app is enabled. The
|
||||||
|
configuration snippet needs to be deactivated when our app is disabled. All of
|
||||||
|
these concerns are automatically handled by the framework if a
|
||||||
|
:class:`~plinth.modules.apache.components.Webserver` component is added to the
|
||||||
|
app. Let us do that in our app's class.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.modules.apache.components import Webserver
|
||||||
|
|
||||||
|
class TransmissionApp(app_module.App):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
webserver = Webserver('webserver-transmission', 'transmission-plinth')
|
||||||
|
self.add(webserver)
|
||||||
|
|
||||||
|
The first argument to instantiate the
|
||||||
|
:class:`~plinth.modules.apache.components.Webserver` class is a unique ID. The
|
||||||
|
second is the name of the Apache2 web server configuration snippet that contains
|
||||||
|
the directives to proxy Transmission web interface via Apache2. We then need to
|
||||||
|
create the configuration file itself in ``tranmission-freedombox.conf``.
|
||||||
|
|
||||||
|
.. code-block:: apache
|
||||||
|
|
||||||
|
## On all sites, provide Transmission on a default path: /transmission
|
||||||
|
<Location /transmission>
|
||||||
|
ProxyPass http://localhost:9091/transmission
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
Managing the firewall
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
FreedomBox has a tight firewall that closes off all TCP/UDP ports by default. If
|
||||||
|
a service needs to available to users on a port, it needs to open the ports in
|
||||||
|
firewalld, the default firewall configuration manager in FreedomBox. When the
|
||||||
|
app is enabled, the ports need to opened and when the app is disabled, the ports
|
||||||
|
needs to be closed. The FreedomBox framework again provides a component for
|
||||||
|
handling these operations. In case of our app, there is no need to open a
|
||||||
|
special port since the web ports are always kept open. However, it is still good
|
||||||
|
to specify that we operate on http/https ports so that users can be provided
|
||||||
|
this information along with additional information on whether the service is
|
||||||
|
available over Internet. Create the
|
||||||
|
:class:`~plinth.modules.firewall.components.Firewall` component during app
|
||||||
|
initialization.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.modules.firewall.components import Firewall
|
||||||
|
|
||||||
|
class TransmissionApp(app_module.App):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
firewall = Firewall('firewall-transmission', name,
|
||||||
|
ports=['http', 'https'], is_external=True)
|
||||||
|
self.add(firewall)
|
||||||
|
|
||||||
|
The first parameter is a unique ID. Second one is the name of the app that as
|
||||||
|
shown to the user in the firewall status page. Third argument is the list of
|
||||||
|
services known to firewalld as listed in ``/usr/lib/firewalld/services/``.
|
||||||
|
Custom services can also be written. The final argument decides whether the
|
||||||
|
service should be made available by FreedomBox from external networks,
|
||||||
|
essentially the Internet.
|
||||||
|
|
||||||
|
User authentication and authorization
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
We wish that only users of FreedomBox should access the web interface of our
|
||||||
|
app. Further, only users belonging to a specially created group are the only
|
||||||
|
ones who should be able access the app. Again, FreedomBox handles all of this
|
||||||
|
and we simply need to declare and use. First we need to register a user group
|
||||||
|
with the FreedomBox framework in ``__init.py__``.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
group = ('bit-torrent', 'Download files using BitTorrent applications')
|
||||||
|
|
||||||
|
def init():
|
||||||
|
...
|
||||||
|
register_group(group)
|
||||||
|
|
||||||
|
Then in the Apache configuration snippet, we can mandate that only users of this
|
||||||
|
group (and, of course, admin users) should be allowed to access our app. In the
|
||||||
|
file ``tranmission-freedombox.conf``, add the following.
|
||||||
|
|
||||||
|
.. code-block:: apache
|
||||||
|
|
||||||
|
<Location /transmission>
|
||||||
|
...
|
||||||
|
Include includes/freedombox-single-sign-on.conf
|
||||||
|
<IfModule mod_auth_pubtkt.c>
|
||||||
|
TKTAuthToken "admin" "bit-torrent"
|
||||||
|
</IfModule>
|
||||||
|
</Location>
|
||||||
|
|
||||||
|
Showing a shortcut in the front page
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The app view we have created is only accessible by administrators of FreedomBox
|
||||||
|
since only they can configure the app. Other users who have access to this app
|
||||||
|
should have a way of discovering the app. This is done by providing a link in
|
||||||
|
the front page of FreedomBox web interface. This is the page that user's see
|
||||||
|
when they visit FreedomBox. To provide this shortcut, a
|
||||||
|
:class:`~plinth.frontpage.Shortcut` component can added to the app.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth import frontpage
|
||||||
|
|
||||||
|
group = ('bit-torrent', 'Download files using BitTorrent applications')
|
||||||
|
|
||||||
|
class TransmissionApp(app_module.App):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
shortcut = frontpage.Shortcut(
|
||||||
|
'shortcut-transmission', name, short_description=short_description,
|
||||||
|
icon='transmission', url='/transmission', clients=clients,
|
||||||
|
login_required=True, allowed_groups=[group[0]])
|
||||||
|
self.add(shortcut)
|
||||||
|
|
||||||
|
The first parameter, as usual, is a unique ID. The next three parameters are
|
||||||
|
basic information about the app similar to the menu item. The URL parameter
|
||||||
|
specifies the URL that the user should be directed to when the shortcut is
|
||||||
|
clicked. This is the web interface provided by our app. The next parameter
|
||||||
|
provides a list of clients. This is useful for the FreedomBox mobile app when
|
||||||
|
the information is used to suggest installing mobile apps. This is described in
|
||||||
|
a later section of this tutorial. The next parameter specifies whether anonymous
|
||||||
|
users who are not logged into FreedomBox should be shown this shortcut. The
|
||||||
|
final parameter further restricts to which group of users this shortcut must be
|
||||||
|
shown.
|
||||||
231
doc/dev/tutorial/customizing.rst
Normal file
231
doc/dev/tutorial/customizing.rst
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 5: Customizing
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Customizing the application page
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The view that we have written above requires a template file. A default template
|
||||||
|
file is provided by the framework. In some cases, we will need to customize this
|
||||||
|
template. Let us create a custom template file in ``transmission.html``.
|
||||||
|
|
||||||
|
.. code-block:: django
|
||||||
|
|
||||||
|
{% extends "app.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block configuration %}
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
<h3>{% trans "Custom Section" %}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Custom paragraph content.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
This template extends an existing template known as ``app.html``. This template
|
||||||
|
is available in FreedomBox core to provide all the basic layout, styling, menus,
|
||||||
|
JavaScript and CSS libraries required for a typical app view. We will override
|
||||||
|
the configuration area after inheriting from the app template and keep the rest
|
||||||
|
as is. ``{{ block.super }}`` adds back the overwritten content in the
|
||||||
|
``configuration`` block.
|
||||||
|
|
||||||
|
Yet again, there is nothing special about the way this template is written. This
|
||||||
|
is a regular Django template. See :doc:`Django Template documentation
|
||||||
|
<django:topics/templates>`.
|
||||||
|
|
||||||
|
For styling and UI components, FreedomBox uses the Twitter Bootstrap project.
|
||||||
|
See `Bootstrap documentation <http://getbootstrap.com/css/>`_ for reference.
|
||||||
|
|
||||||
|
To start using our custom template, we need to pass this to our view. In
|
||||||
|
``views.py``, add the following line:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
class TransmissionAppView(AppView):
|
||||||
|
...
|
||||||
|
template_name = 'transmission.html'
|
||||||
|
|
||||||
|
Writing a configuration form
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Our app needs some configuration. So, we need to write a configuration form to
|
||||||
|
provide options to the user. Add the following to ``forms.py``.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from plinth.forms import AppForm
|
||||||
|
|
||||||
|
|
||||||
|
class TransmissionForm(AppForm): # pylint: disable=W0232
|
||||||
|
"""Transmission configuration form"""
|
||||||
|
download_dir = forms.CharField(
|
||||||
|
label='Download directory',
|
||||||
|
help_text='Directory where downloads are saved. If you change the '
|
||||||
|
'default directory, ensure that the new directory exists '
|
||||||
|
'and is writable by "debian-transmission" user.')
|
||||||
|
|
||||||
|
This creates a Django form that shows a single option to set the download
|
||||||
|
directory for our Transmission app. This is how a regular Django form is built.
|
||||||
|
See :doc:`Django Forms documentation <django:topics/forms/index>` for more
|
||||||
|
information.
|
||||||
|
|
||||||
|
.. tip: Too many options
|
||||||
|
|
||||||
|
Resist the temptation to create a lot of configuration options. Although this
|
||||||
|
will put more control in the hands of the users, it will make FreedomBox less
|
||||||
|
usable. FreedomBox is a consumer product. Our target users are not technically
|
||||||
|
savvy and we have make most of the decisions on behalf of the user to make the
|
||||||
|
interface as simple and easy to use as possible.
|
||||||
|
|
||||||
|
Applying the changes from the form
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The view we have created needs to display the form and process the form after
|
||||||
|
the user submits it. Let us implement that in ``views.py``.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
from plinth import actions, views
|
||||||
|
|
||||||
|
from .forms import TransmissionForm
|
||||||
|
|
||||||
|
class TransmissionAppView(views.AppView):
|
||||||
|
...
|
||||||
|
form_class = TransmissionForm
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
"""Get the current settings from Transmission server."""
|
||||||
|
status = super().get_initial()
|
||||||
|
configuration = actions.superuser_run('transmission',
|
||||||
|
['get-configuration'])
|
||||||
|
configuration = json.loads(configuration)
|
||||||
|
status.update({
|
||||||
|
key.translate(str.maketrans({
|
||||||
|
'-': '_'
|
||||||
|
})): value
|
||||||
|
for key, value in configuration.items()
|
||||||
|
})
|
||||||
|
return status
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Apply the changes submitted in the form."""
|
||||||
|
old_status = form.initial
|
||||||
|
new_status = form.cleaned_data
|
||||||
|
|
||||||
|
if old_status['download_dir'] != new_status['download_dir']:
|
||||||
|
new_configuration = {
|
||||||
|
'download-dir': new_status['download_dir'],
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.superuser_run('transmission', ['merge-configuration'],
|
||||||
|
input=json.dumps(new_configuration).encode())
|
||||||
|
messages.success(self.request, 'Configuration updated')
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
We check to make sure that the configuration value has actually changed after
|
||||||
|
the form is submitted. Although FreedomBox's operations are idempotent, meaning
|
||||||
|
that running them twice will not be problematic, we still wish to avoid
|
||||||
|
unnecessary operations for the sake of speed.
|
||||||
|
|
||||||
|
We are actually performing the operation using *actions*. We will implement this
|
||||||
|
action a bit later.
|
||||||
|
|
||||||
|
After we perform the operation, we will show a message on the response page that
|
||||||
|
the action was successful or that nothing happened. We use the Django messaging
|
||||||
|
framework to accomplish this. See :doc:`Django messaging framework
|
||||||
|
<django:ref/contrib/messages>` for more information.
|
||||||
|
|
||||||
|
Writing actions
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The actual work of performing the configuration change is carried out by an
|
||||||
|
*action*. Actions are independent scripts that run with higher privileges
|
||||||
|
required to perform a task. They are placed in a separate directory and invoked
|
||||||
|
as scripts via sudo. For our application we need to write an action that can
|
||||||
|
enable and disable the web configuration. We will do this by creating a file
|
||||||
|
``actions/transmission``.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from plinth import action_utils
|
||||||
|
|
||||||
|
TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json'
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
"""Return parsed command line arguments as dictionary."""
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||||
|
|
||||||
|
subparsers.add_parser('get-configuration',
|
||||||
|
help='Return the current configuration')
|
||||||
|
subparsers.add_parser(
|
||||||
|
'merge-configuration',
|
||||||
|
help='Merge JSON configuration from stdin with existing')
|
||||||
|
|
||||||
|
subparsers.required = True
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def subcommand_get_configuration(_):
|
||||||
|
"""Return the current configuration in JSON format."""
|
||||||
|
configuration = open(TRANSMISSION_CONFIG, 'r').read()
|
||||||
|
print(configuration)
|
||||||
|
|
||||||
|
|
||||||
|
def subcommand_merge_configuration(arguments):
|
||||||
|
"""Merge given JSON configuration with existing configuration."""
|
||||||
|
configuration = sys.stdin.read()
|
||||||
|
configuration = json.loads(configuration)
|
||||||
|
|
||||||
|
current_configuration = open(TRANSMISSION_CONFIG, 'r').read()
|
||||||
|
current_configuration = json.loads(current_configuration)
|
||||||
|
|
||||||
|
new_configuration = current_configuration
|
||||||
|
new_configuration.update(configuration)
|
||||||
|
new_configuration = json.dumps(new_configuration, indent=4, sort_keys=True)
|
||||||
|
|
||||||
|
open(TRANSMISSION_CONFIG, 'w').write(new_configuration)
|
||||||
|
action_utils.service_reload('transmission-daemon')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Parse arguments and perform all duties."""
|
||||||
|
arguments = parse_arguments()
|
||||||
|
|
||||||
|
subcommand = arguments.subcommand.replace('-', '_')
|
||||||
|
subcommand_method = globals()['subcommand_' + subcommand]
|
||||||
|
subcommand_method(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
This is a simple Python3 program that parses command line arguments. While
|
||||||
|
Python3 is preferred, it can be written in other languages also. It may use
|
||||||
|
various helper utilities provided by the FreedomBox framework in
|
||||||
|
:obj:`plinth.action_utils` to easily perform it's duties.
|
||||||
|
|
||||||
|
This script is automatically installed to ``/usr/share/plinth/actions`` by
|
||||||
|
FreedomBox's installation script ``setup.py``. Only from here will there is a
|
||||||
|
possibility of running the script under ``sudo``. If you are writing an
|
||||||
|
application that resides indenpendently of FreedomBox's source code, your app's
|
||||||
|
``setup.py`` script will need to take care of copying the file to this target
|
||||||
|
location.
|
||||||
95
doc/dev/tutorial/finishing.rst
Normal file
95
doc/dev/tutorial/finishing.rst
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 8: Finishing
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Adding a License
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
FreedomBox is licensed under the GNU Affero General Public License Version 3 or
|
||||||
|
later. FreedomBox apps, which run as modules under FreedomBox Service (Plinth),
|
||||||
|
also need to be under the same license or under a compatible license. The
|
||||||
|
license of our app needs to clear for our app to be accepted by users and other
|
||||||
|
developers. Let us add license headers to our application.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
#
|
||||||
|
# This file is part of FreedomBox.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
The above header needs to be present in every file of the application. It is
|
||||||
|
suitable for Python files. However, in template files, we need to modify it
|
||||||
|
slightly.
|
||||||
|
|
||||||
|
.. code-block:: django
|
||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
#
|
||||||
|
# This file is part of FreedomBox.
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
Coding standards
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
For readability and easy collaboration it is important to follow common coding
|
||||||
|
standards. FreedomBox uses the Python coding standards and uses the ``pylint``
|
||||||
|
and ``flake8`` tools to check if the there are any violations. Run these tools
|
||||||
|
on our application and fix any errors and warnings. Better yet, integrate these
|
||||||
|
tools into your favorite IDE for on-the-fly checking.
|
||||||
|
|
||||||
|
For the most part, the code we have written so far, is already compliant with
|
||||||
|
the coding standards. This includes variable/method naming, indentation,
|
||||||
|
document strings, comments, etc. One thing we have to add are the module
|
||||||
|
documentation strings. Let us add those. In ``__init__.py`` add the top:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
FreedomBox app to configure Transmission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Contributing code to FreedomBox
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``HACKING.md`` and ``CONTRIBUTING.md`` files in the FredomBox source code
|
||||||
|
have tips on how to contribute code to the project. Be sure to read them if you
|
||||||
|
are submitting your app for including on the project.
|
||||||
|
|
||||||
|
Here is ``HACKING.md``:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../HACKING.md
|
||||||
|
:language: md
|
||||||
|
|
||||||
|
Here is ``CONTRIBUTING.md``:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../../CONTRIBUTING.md
|
||||||
|
:language: md
|
||||||
48
doc/dev/tutorial/index.rst
Normal file
48
doc/dev/tutorial/index.rst
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
=====================================
|
||||||
|
Tutorial: Writing Apps for FreedomBox
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
This tutorial covers writing an app for FreedomBox. FreedomBox is a pure blend
|
||||||
|
of Debian with a web interface, that configures its apps. We shall discuss
|
||||||
|
various aspects of building an app for FreedomBox, by creating an example app.
|
||||||
|
The app that is discussed in the tutorial already available in FreedomBox so you
|
||||||
|
can also study it's full source code.
|
||||||
|
|
||||||
|
There are two parts to writing a FreedomBox app. First is to make sure that the
|
||||||
|
app is available as a Debian package uploaded to its repositories. This is the
|
||||||
|
majority of the work involved. However, if an app is already available in Debian
|
||||||
|
repositories, the whole task is simplified.. The second part of writing an app
|
||||||
|
for FreedomBox is to provide a thin web interface layer for configuring and
|
||||||
|
managing the app. This is done by extending FreedomBox's user interface to
|
||||||
|
provide visibility to the app and to let the user control its operations in a
|
||||||
|
highly simplified way. This layer is what we typically refer to as a 'FreedomBox
|
||||||
|
app'.
|
||||||
|
|
||||||
|
FreedomBox apps can either be distributed to the end user as part of FreedomBox
|
||||||
|
Service (Plinth) source code by submitting the apps to the project or they can
|
||||||
|
distributed independently. This tutorial covers writing an app that is meant to
|
||||||
|
be distributed as part of FreedomBox Service (Plinth). However, writing
|
||||||
|
independent FreedomBox apps is also very similar and most of this tutorial is
|
||||||
|
applicable.
|
||||||
|
|
||||||
|
.. note:: The term *App*
|
||||||
|
|
||||||
|
The term app, in this tutorial, is used to mean multiple concepts. A service
|
||||||
|
or an application available to end users in FreedomBox is a combination of
|
||||||
|
Debian package and a web interface layer. The web interface layer is also
|
||||||
|
called a FreedomBox app which is very similar to and built upon a Django
|
||||||
|
application.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
|
||||||
|
beginning
|
||||||
|
skeleton
|
||||||
|
view
|
||||||
|
components
|
||||||
|
customizing
|
||||||
|
setup
|
||||||
|
other
|
||||||
|
finishing
|
||||||
|
code
|
||||||
222
doc/dev/tutorial/other.rst
Normal file
222
doc/dev/tutorial/other.rst
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 7: Other Changes
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Showing information about app clients
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
It would be helpful to our users if we can show how they can use our app. If
|
||||||
|
there are desktop and mobile clients that can used to access our service, we
|
||||||
|
need to list them and present them. Let's add this information to
|
||||||
|
``manifest.py``.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.clients import validate
|
||||||
|
|
||||||
|
clients = validate([{
|
||||||
|
'name': _('Transmission'),
|
||||||
|
'platforms': [{
|
||||||
|
'type': 'web',
|
||||||
|
'url': '/transmission'
|
||||||
|
}]
|
||||||
|
}])
|
||||||
|
|
||||||
|
Since our app is a simple web application with no clients needed, we just list
|
||||||
|
that. We need to include this into the main app view. In ``__init__.py``, add:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from .manifest import clients
|
||||||
|
|
||||||
|
clients = clients
|
||||||
|
|
||||||
|
In ``views.py``, add:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.modules import transmission
|
||||||
|
|
||||||
|
class TransmissionAppView(views.AppView):
|
||||||
|
...
|
||||||
|
clients = transmission.clients
|
||||||
|
|
||||||
|
Writing a manual page
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The description of app should provide basic information on what the app is about
|
||||||
|
and how to use it. It is impractical, however, to explain everything about the
|
||||||
|
app in a few short paragraphs. So, we need to write a page about the app in the
|
||||||
|
FreedomBox manual. This page will be available to the users from within the
|
||||||
|
FreedomBox web interface. To make this happen, let us write a `manual page entry
|
||||||
|
<https://wiki.debian.org/FreedomBox/Manual/Transmission>`_ for our app in the
|
||||||
|
`FreedomBox Wiki <https://wiki.debian.org/FreedomBox/Manual>`_ and then provide
|
||||||
|
a link to it from app page. In ``__init__.py``, add:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
manual_page = 'Transmission'
|
||||||
|
|
||||||
|
Then, in ``views.py``, add:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.modules import transmission
|
||||||
|
|
||||||
|
class TransmissionAppView(views.AppView):
|
||||||
|
...
|
||||||
|
manual_page = transmission.manual_page
|
||||||
|
|
||||||
|
Adding backup/restore functionality
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Each app in FreedomBox needs to provide the ability to backup its configuration
|
||||||
|
and data. Apart from providing durability to users' data, this allows the user
|
||||||
|
to migrate from one machine to another. FreedomBox framework provides a simple
|
||||||
|
declarative mechanism to allow the app to be backed up and restored. In
|
||||||
|
``manifest.py``, add:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.modules.backups.api import validate as validate_backup
|
||||||
|
|
||||||
|
backup = validate_backup({
|
||||||
|
'data': {
|
||||||
|
'directories': ['/var/lib/transmission-daemon/.config']
|
||||||
|
},
|
||||||
|
'secrets': {
|
||||||
|
'files': ['/etc/transmission-daemon/settings.json']
|
||||||
|
},
|
||||||
|
'services': ['transmission-daemon']
|
||||||
|
})
|
||||||
|
|
||||||
|
The data and secrets information specifies which list of files and directories
|
||||||
|
FreedomBox framework needs to backup. The list of services specifies which
|
||||||
|
daemons should be stopped during the backup process. In ``__init__.py``, add:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from .manifest import backup
|
||||||
|
|
||||||
|
Creating diagnostics
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
When the app does not work as expected, the user should known what is happening
|
||||||
|
with the app. The FreedomBox framework provides an API for running and showing
|
||||||
|
diagnostics results. The app has to implement a method for actually running the
|
||||||
|
diagnostics and return the results as a list. FreedomBox then takes care of
|
||||||
|
calling the diagnostics method and displaying the list in a formatted manner.
|
||||||
|
|
||||||
|
To implement the diagnostics, a method called ``diagnose()`` has to be available
|
||||||
|
as ``<app-module>.diagnose()``. It must return a list in which each item is the
|
||||||
|
result of a test performed. The item itself is a two-tuple containing the
|
||||||
|
display name of the test followed by the result as ``passed``, ``failed`` or
|
||||||
|
``error``.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
def diagnose():
|
||||||
|
"""Run diagnostics and return the results."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
results.extend(action_utils.diagnose_url_on_all(
|
||||||
|
'https://{host}/transmission', extra_options=['--no-check-certificate']))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
Now that we have implemented diagnostics, we also need to show a diagnostics
|
||||||
|
button in the App's page. Adding an attribute to the
|
||||||
|
:class:`~plinth.views.AppView` will take care of this.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
class TransmissionView(views.AppView):
|
||||||
|
...
|
||||||
|
diagnostics_module_name = 'transmission'
|
||||||
|
|
||||||
|
There are several helpers available to implement some of the common diagnostic
|
||||||
|
tests. For our application we wish to implement a test to check whether the
|
||||||
|
``/transmission`` URL is accessible. Since this is a commonly performed test,
|
||||||
|
there is a helper method available and we have used it in the above code. The
|
||||||
|
``{host}`` tag replaced with various IP addresses, hostnames and domain names by
|
||||||
|
the helper to produce different kinds of URLs and they are all tested. Results
|
||||||
|
for all tests are returned which we then pass on to the framework.
|
||||||
|
|
||||||
|
The user can trigger the diagnostics test by going to **System -> Diagnostics**
|
||||||
|
page. This runs diagnostics for all the applications. Users can also run
|
||||||
|
diagnostics specifically for this app from the app's page. A diagnostics button
|
||||||
|
is shown by the `app.html` template automatically when
|
||||||
|
``diagnostics_module_name`` attribute is set in the app's ``AppView`` derived
|
||||||
|
from :obj:`plinth.views.AppView`.
|
||||||
|
|
||||||
|
.. code-block:: django
|
||||||
|
|
||||||
|
{% include "diagnostics_button.html" with module="ttrss" enabled=True %}
|
||||||
|
|
||||||
|
Logging
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
Sometimes we may feel the need to write some debug messages to the console and
|
||||||
|
system logs. Doing this in FreedomBox is just like doing this any other Python
|
||||||
|
application.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def example_method():
|
||||||
|
logger.debug('A debug level message')
|
||||||
|
|
||||||
|
logger.info('Showing application page - %s', request.method)
|
||||||
|
|
||||||
|
try:
|
||||||
|
something()
|
||||||
|
except Exception as exception:
|
||||||
|
# Print stack trace
|
||||||
|
logger.exception('Encountered an exception - %s', exception)
|
||||||
|
|
||||||
|
For more information see Python :doc:`logging framework <howto/logging>`
|
||||||
|
documentation.
|
||||||
|
|
||||||
|
Internationalization
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Every string message that is visible to the user must be localized to user's
|
||||||
|
native language. For this to happen, our app needs to be internationalized. This
|
||||||
|
requires marking the user visible messages for translation. FreedomBox apps use
|
||||||
|
the Django's localization methods to make that happen.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
name = _('Transmission')
|
||||||
|
|
||||||
|
short_description = _('BitTorrent Web Client')
|
||||||
|
|
||||||
|
description = [
|
||||||
|
_('BitTorrent is a peer-to-peer file sharing protocol. '
|
||||||
|
'Transmission daemon handles Bitorrent file sharing. Note that '
|
||||||
|
'BitTorrent is not anonymous.'),
|
||||||
|
_('Access the web interface at <a href="/transmission">/transmission</a>.')
|
||||||
|
]
|
||||||
|
|
||||||
|
Notice that the app's name, description, etc. are wrapped in the ``_()`` method
|
||||||
|
call. This needs to be done for the rest of our app. We use the
|
||||||
|
:obj:`~django.utils.translation.ugettext_lazy` in some cases and we use the
|
||||||
|
regular :obj:`~django.utils.translation.ugettext` in other cases. This is
|
||||||
|
because in the second case the :obj:`~django.utils.translation.gettext` lookup
|
||||||
|
is made once and reused for every user looking at the interface. These users may
|
||||||
|
each have a different language set for their interface. Lookup made for one
|
||||||
|
language for a user should not be used for other users. The ``_lazy`` methods
|
||||||
|
provided by Django makes sure that the return value is an object that will
|
||||||
|
actually be converted to string at the final moment when the string is being
|
||||||
|
displayed. In the first case, the lookup is made and string is returned
|
||||||
|
immediately.
|
||||||
|
|
||||||
|
All of this is the usual way internationalization is done in Django. See
|
||||||
|
:doc:`Internationalization and localization <django:topics/i18n/index>`
|
||||||
|
documentation for more information.
|
||||||
40
doc/dev/tutorial/setup.rst
Normal file
40
doc/dev/tutorial/setup.rst
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 6: Setup
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Installing packages required for the app
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
So far, we haven't dealt with installing the packages needed for Transmission to
|
||||||
|
work. Nor did we take care of performing the initial configuration for
|
||||||
|
Transmission. FreedomBox takes care of installing all the Debian packages
|
||||||
|
required for our app to work. All we need to do is specify the list of the
|
||||||
|
Debian packages required in the ``setup()`` method that is called during
|
||||||
|
installation:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
managed_packages = ['transmission-daemon']
|
||||||
|
|
||||||
|
def setup(helper, old_version=None):
|
||||||
|
"""Install and configure the module."""
|
||||||
|
helper.install(managed_packages)
|
||||||
|
|
||||||
|
new_configuration = {
|
||||||
|
'rpc-whitelist-enabled': False,
|
||||||
|
'rpc-authentication-required': False
|
||||||
|
}
|
||||||
|
helper.call('post', actions.superuser_run, 'transmission',
|
||||||
|
['merge-configuration'],
|
||||||
|
input=json.dumps(new_configuration).encode())
|
||||||
|
|
||||||
|
helper.call('post', app.enable)
|
||||||
|
|
||||||
|
The first time this app's view is accessed, FreedomBox shows an app installation
|
||||||
|
page and allows the user to install the app. After the app installation is
|
||||||
|
completed, the user is shown the app's configuration page.
|
||||||
|
|
||||||
|
In case of our app Transmission, first we are installing the Debian packages,
|
||||||
|
then performing the first time configuration on the app using the action script
|
||||||
|
and finally enabling the app.
|
||||||
102
doc/dev/tutorial/skeleton.rst
Normal file
102
doc/dev/tutorial/skeleton.rst
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 2: Skeleton
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Let us get started with creating our FreedomBox app.
|
||||||
|
|
||||||
|
Creating the project structure
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Create a directory structure as follows with empty files. We will fill them up
|
||||||
|
in a step-by-step manner::
|
||||||
|
|
||||||
|
─┬ <plinth_root>/
|
||||||
|
├─┬ plinth/
|
||||||
|
│ └─┬ modules/
|
||||||
|
│ └─┬ transmission/
|
||||||
|
│ ├─ __init__.py
|
||||||
|
│ ├─ forms.py
|
||||||
|
│ ├─ manifest.py
|
||||||
|
│ ├─ urls.py
|
||||||
|
│ ├─ views.py
|
||||||
|
│ ├─┬ data/
|
||||||
|
│ │ └─┬ etc/
|
||||||
|
│ │ ├─┬ plinth/
|
||||||
|
│ │ │ └─┬ modules-enabled/
|
||||||
|
│ │ │ └─ transmission
|
||||||
|
│ │ └─┬ apache2/
|
||||||
|
│ │ └─┬ conf-available/
|
||||||
|
│ │ └─ transmission-freedombox.conf
|
||||||
|
│ └─┬ tests
|
||||||
|
│ └─ __init__.py
|
||||||
|
└─┬ actions/
|
||||||
|
└─ transmission
|
||||||
|
|
||||||
|
The file ``__init__.py`` indicates that the directory in which it is present is
|
||||||
|
a Python module. For now, it is an empty file.
|
||||||
|
|
||||||
|
FreedomBox's setup script ``setup.py`` will automatically install the
|
||||||
|
``plinth/modules/transmission`` directory (along with other files described
|
||||||
|
later) to an appropriate location. If you are creating an app that stays
|
||||||
|
independent and outside of FreedomBox source tree, then ``setup.py`` script in
|
||||||
|
your source tree will need to install it to a proper location on the system. The
|
||||||
|
``plinth/modules/`` directory is a Python3 `namespace package
|
||||||
|
<https://www.python.org/dev/peps/pep-0420/>`_. So, you can install it with the
|
||||||
|
``plinth/modules/`` directory structure into any Python path and still be
|
||||||
|
discovered as ``plinth.modules.*``.
|
||||||
|
|
||||||
|
Tell FreedomBox that our app exists
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The first thing to do is tell FreedomBox that our app exists. This is done by
|
||||||
|
writing a small file with the Python import path to our app and placing it in
|
||||||
|
``plinth/modules/transmission/data/etc/plinth/modules-enabled/``. Let us create
|
||||||
|
this file ``transmission``::
|
||||||
|
|
||||||
|
plinth.modules.transmission
|
||||||
|
|
||||||
|
This file is automatically installed to ``/etc/plinth/modules-enabled/`` by
|
||||||
|
FreedomBox's installation script ``setup.py``. If we are writing a module that
|
||||||
|
resides independently outside the FreedomBox's source code, the setup script
|
||||||
|
will need to copy it to the target location. Further, it is not necessary for
|
||||||
|
the app to be part of the ``plinth.modules`` namespace. It can, for example, be
|
||||||
|
``freedombox_transmission``.
|
||||||
|
|
||||||
|
Creating the App class
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
In the FreedomBox framework, each app must be a class derived from the
|
||||||
|
:class:`plinth.app.App`. Let us to that in ``__init__.py``. We will fill up the
|
||||||
|
class later.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth import app as app_module
|
||||||
|
|
||||||
|
class TransmissionApp(app_module.App):
|
||||||
|
"""FreedomBox app for Transmission."""
|
||||||
|
|
||||||
|
app_id = 'transmission'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Create components for the app."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
As soon as FreedomBox Service (Plinth) starts, it will load all the enabled
|
||||||
|
modules. After this, it gives a chance to each of the modules to initialize
|
||||||
|
itself by calling the ``init()`` method if there is such a method available as
|
||||||
|
``<module>.init()``. The app class must be instantiated here.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
app = None
|
||||||
|
|
||||||
|
def init():
|
||||||
|
"""Initialize the Transmission module."""
|
||||||
|
global app
|
||||||
|
app = TransmissionApp()
|
||||||
|
|
||||||
|
setup_helper = globals()['setup_helper']
|
||||||
|
if setup_helper.get_state() != 'needs-setup' and app.is_enabled():
|
||||||
|
app.set_enabled(True)
|
||||||
112
doc/dev/tutorial/view.rst
Normal file
112
doc/dev/tutorial/view.rst
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
.. SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
|
|
||||||
|
Part 3: View
|
||||||
|
------------
|
||||||
|
|
||||||
|
Writing the URLs
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
For a user to visit our app in FreedomBox, we need to provide a URL. When the
|
||||||
|
user visits this URL, a view is executed and a page is displayed. In ``urls.py``
|
||||||
|
write the following:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .views import TransmissionAppView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^apps/transmission/$', TransmissionAppView.as_view(), name='index'),
|
||||||
|
]
|
||||||
|
|
||||||
|
This routes the ``/apps/transmission/`` URL to a view called
|
||||||
|
``TransmissionAppView`` defined in ``plinth/modules/transmission/views.py``.
|
||||||
|
This is no different than how routing URLs is done in Django. See :doc:`Django
|
||||||
|
URL dispatcher <django:topics/http/urls>` for more information.
|
||||||
|
|
||||||
|
Adding a menu item
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
We have added a URL to be handled by our app but this does not yet show up to be
|
||||||
|
a link in FreedomBox web interface. Let us add a link in the apps list. In
|
||||||
|
``__init__.py`` add the following:
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth.menu import main_menu
|
||||||
|
|
||||||
|
name = 'Transmission'
|
||||||
|
|
||||||
|
short_description = 'BitTorrent Web Client'
|
||||||
|
|
||||||
|
description = [
|
||||||
|
'BitTorrent is a peer-to-peer file sharing protocol. '
|
||||||
|
'Transmission daemon handles Bitorrent file sharing. Note that '
|
||||||
|
'BitTorrent is not anonymous.',
|
||||||
|
'Access the web interface at <a href="/transmission">/transmission</a>.'
|
||||||
|
]
|
||||||
|
|
||||||
|
class TransmissionApp(app_module.App):
|
||||||
|
...
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
menu_item = menu.Menu('menu-transmission', name, short_description,
|
||||||
|
'transmission', 'transmission:index',
|
||||||
|
parent_url_name='apps')
|
||||||
|
self.add(menu_item)
|
||||||
|
|
||||||
|
What this does is add a menu item component into the our app. In FreedomBox
|
||||||
|
framework, an app is made up of many simple components. When operations such as
|
||||||
|
enable/disable are performed on the app, they will be applied on all the
|
||||||
|
components. In case of menu components, FreedomBox framework takes care of
|
||||||
|
presenting them appropriately. The component captures various details about the
|
||||||
|
menu item we want to present.
|
||||||
|
|
||||||
|
* The first parameter is simply a unique ID for the component.
|
||||||
|
|
||||||
|
* The second parameter is the display name to use for our menu item which
|
||||||
|
happens to be the name of the app as well.
|
||||||
|
|
||||||
|
* The third parameter is a short description for the menu item.
|
||||||
|
|
||||||
|
* The fourth parameter is the name of the icon to use when showing the menu
|
||||||
|
item. An SVG file and a PNG should be created in the ``static/theme/icons/``
|
||||||
|
directory.
|
||||||
|
|
||||||
|
* The fifth parameter is the URL that the user should be directed to when the
|
||||||
|
menu item is clicked. This is a Django URL name and we have already created a
|
||||||
|
URL with this name. Note that when including our app's URLs, FreedomBox will
|
||||||
|
automatically set the name of the module as the Django URL namespace. Hence it
|
||||||
|
is ``transmission:index`` and not just ``index``.
|
||||||
|
|
||||||
|
* We wish to add our menu item to the list of apps in the apps page which is why
|
||||||
|
we have specified ``apps`` as the parent URL for the this app in the final
|
||||||
|
parameter.
|
||||||
|
|
||||||
|
Writing a view
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
We have a URL pointing to our view. We have also added a menu item in the apps
|
||||||
|
section of the web interface that points to our view. We now need to create a
|
||||||
|
view to show the app page for our app. In ``views.py``, let us add a view.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
from plinth import views
|
||||||
|
from plinth.modules import transmission
|
||||||
|
|
||||||
|
class TransmissionAppView(views.AppView):
|
||||||
|
"""Serve configuration page."""
|
||||||
|
name = transmission.name
|
||||||
|
description = transmission.description
|
||||||
|
app_id = 'transmission'
|
||||||
|
|
||||||
|
The base view :class:`~plinth.views.AppView` takes care of a lot of details for
|
||||||
|
us. First, it shows basic information about the app like name, description,
|
||||||
|
desktop/mobiles clients for the service (described later), link to the manual
|
||||||
|
page (described later), link to diagnostics button, etc. Then it shows the
|
||||||
|
status of the app whether it is running and can also present a form for
|
||||||
|
configuration. It also presents a way to enable/disable the app.
|
||||||
Loading…
x
Reference in New Issue
Block a user