mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +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/*/*.raw.xml
|
||||
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+
|
||||
|
||||
Files: doc/*.xml
|
||||
doc/*.rst
|
||||
doc/*.png
|
||||
doc/*.jpg
|
||||
Copyright: 2011-2019 FreedomBox Authors
|
||||
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