diff --git a/.gitignore b/.gitignore index 8eac94986..7f1f79b20 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ doc/manual/*/*.html doc/manual/*/*.xml !doc/manual/*/*.raw.xml doc/plinth.1 +doc/dev/_build \#* .#* *~ diff --git a/debian/copyright b/debian/copyright index 00b05b8d2..a2bca8c0b 100644 --- a/debian/copyright +++ b/debian/copyright @@ -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 diff --git a/doc/dev/Makefile b/doc/dev/Makefile new file mode 100644 index 000000000..bfc0d34a4 --- /dev/null +++ b/doc/dev/Makefile @@ -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 . +# + +# +# 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" diff --git a/doc/dev/_static/.gitkeep b/doc/dev/_static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/doc/dev/_templates/layout.html b/doc/dev/_templates/layout.html new file mode 100644 index 000000000..aa02582ec --- /dev/null +++ b/doc/dev/_templates/layout.html @@ -0,0 +1,15 @@ +{%- extends "alabaster/layout.html" %} + +{%- block footer %} + + +{% endblock %} diff --git a/doc/dev/conf.py b/doc/dev/conf.py new file mode 100644 index 000000000..78ef99a9e --- /dev/null +++ b/doc/dev/conf.py @@ -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 . +# +""" +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__', +} diff --git a/doc/dev/index.rst b/doc/dev/index.rst new file mode 100644 index 000000000..595e618a2 --- /dev/null +++ b/doc/dev/index.rst @@ -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 ` + +#. `Debian Packaging Portal `_ + +#. `systemd System and Service Manager `_ + +#. `Bootstrap - CSS Library `_ + +#. `FreedomBox User Manual `_ + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/dev/reference/action_utils.rst b/doc/dev/reference/action_utils.rst new file mode 100644 index 000000000..92c9c565d --- /dev/null +++ b/doc/dev/reference/action_utils.rst @@ -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: diff --git a/doc/dev/reference/actions.rst b/doc/dev/reference/actions.rst new file mode 100644 index 000000000..91d7cf59b --- /dev/null +++ b/doc/dev/reference/actions.rst @@ -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 diff --git a/doc/dev/reference/app.rst b/doc/dev/reference/app.rst new file mode 100644 index 000000000..45f5c1c35 --- /dev/null +++ b/doc/dev/reference/app.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +App +--- + +.. autoclass:: plinth.app.App + :members: diff --git a/doc/dev/reference/app_module.rst b/doc/dev/reference/app_module.rst new file mode 100644 index 000000000..589b64481 --- /dev/null +++ b/doc/dev/reference/app_module.rst @@ -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. + +.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. + +.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')] + +.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``. diff --git a/doc/dev/reference/components/daemon.rst b/doc/dev/reference/components/daemon.rst new file mode 100644 index 000000000..293d715a5 --- /dev/null +++ b/doc/dev/reference/components/daemon.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Daemon +^^^^^^ + +.. autoclass:: plinth.daemon.Daemon + :members: diff --git a/doc/dev/reference/components/domain.rst b/doc/dev/reference/components/domain.rst new file mode 100644 index 000000000..884f918a8 --- /dev/null +++ b/doc/dev/reference/components/domain.rst @@ -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: diff --git a/doc/dev/reference/components/firewall.rst b/doc/dev/reference/components/firewall.rst new file mode 100644 index 000000000..e15eafd02 --- /dev/null +++ b/doc/dev/reference/components/firewall.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Firewall +^^^^^^^^ + +.. autoclass:: plinth.modules.firewall.components.Firewall + :members: diff --git a/doc/dev/reference/components/frontpage.rst b/doc/dev/reference/components/frontpage.rst new file mode 100644 index 000000000..5e185177f --- /dev/null +++ b/doc/dev/reference/components/frontpage.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Frontpage +^^^^^^^^^ + +.. autoclass:: plinth.frontpage.Shortcut + :members: diff --git a/doc/dev/reference/components/index.rst b/doc/dev/reference/components/index.rst new file mode 100644 index 000000000..16c03c09f --- /dev/null +++ b/doc/dev/reference/components/index.rst @@ -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: diff --git a/doc/dev/reference/components/letsencrypt.rst b/doc/dev/reference/components/letsencrypt.rst new file mode 100644 index 000000000..84c423806 --- /dev/null +++ b/doc/dev/reference/components/letsencrypt.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Let's Encrypt +^^^^^^^^^^^^^ + +.. autoclass:: plinth.modules.letsencrypt.components.LetsEncrypt + :members: diff --git a/doc/dev/reference/components/menu.rst b/doc/dev/reference/components/menu.rst new file mode 100644 index 000000000..9bb682c5a --- /dev/null +++ b/doc/dev/reference/components/menu.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Menu +^^^^ + +.. autoclass:: plinth.menu.Menu + :members: diff --git a/doc/dev/reference/components/webserver.rst b/doc/dev/reference/components/webserver.rst new file mode 100644 index 000000000..896d66cca --- /dev/null +++ b/doc/dev/reference/components/webserver.rst @@ -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: diff --git a/doc/dev/reference/forms.rst b/doc/dev/reference/forms.rst new file mode 100644 index 000000000..914438e59 --- /dev/null +++ b/doc/dev/reference/forms.rst @@ -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: diff --git a/doc/dev/reference/index.rst b/doc/dev/reference/index.rst new file mode 100644 index 000000000..e8915e2a6 --- /dev/null +++ b/doc/dev/reference/index.rst @@ -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 diff --git a/doc/dev/reference/views.rst b/doc/dev/reference/views.rst new file mode 100644 index 000000000..d7c99aaf2 --- /dev/null +++ b/doc/dev/reference/views.rst @@ -0,0 +1,7 @@ +.. SPDX-License-Identifier: CC-BY-SA-4.0 + +Views +----- + +.. autoclass:: plinth.views.AppView + :members: diff --git a/doc/dev/tutorial/beginning.rst b/doc/dev/tutorial/beginning.rst new file mode 100644 index 000000000..34f7818d9 --- /dev/null +++ b/doc/dev/tutorial/beginning.rst @@ -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 `. + +You should start by reading the :doc:`Django tutorial `. 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 `_. + +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. diff --git a/doc/dev/tutorial/code.rst b/doc/dev/tutorial/code.rst new file mode 100644 index 000000000..4cde693a7 --- /dev/null +++ b/doc/dev/tutorial/code.rst @@ -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 diff --git a/doc/dev/tutorial/components.rst b/doc/dev/tutorial/components.rst new file mode 100644 index 000000000..f3e9a8d59 --- /dev/null +++ b/doc/dev/tutorial/components.rst @@ -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 +`_ 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 + + ProxyPass http://localhost:9091/transmission + + +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 + + + ... + Include includes/freedombox-single-sign-on.conf + + TKTAuthToken "admin" "bit-torrent" + + + +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. diff --git a/doc/dev/tutorial/customizing.rst b/doc/dev/tutorial/customizing.rst new file mode 100644 index 000000000..a7738f72c --- /dev/null +++ b/doc/dev/tutorial/customizing.rst @@ -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 }} + +

{% trans "Custom Section" %}

+ +

+ {% blocktrans trimmed %} + Custom paragraph content. + {% endblocktrans %} +

+ + {% 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 +`. + +For styling and UI components, FreedomBox uses the Twitter Bootstrap project. +See `Bootstrap documentation `_ 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 ` 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 +` 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. diff --git a/doc/dev/tutorial/finishing.rst b/doc/dev/tutorial/finishing.rst new file mode 100644 index 000000000..8f054ceb3 --- /dev/null +++ b/doc/dev/tutorial/finishing.rst @@ -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 . + # + +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 . + # + {% 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 diff --git a/doc/dev/tutorial/index.rst b/doc/dev/tutorial/index.rst new file mode 100644 index 000000000..d34507ea4 --- /dev/null +++ b/doc/dev/tutorial/index.rst @@ -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 diff --git a/doc/dev/tutorial/other.rst b/doc/dev/tutorial/other.rst new file mode 100644 index 000000000..0d66bbb4b --- /dev/null +++ b/doc/dev/tutorial/other.rst @@ -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 +`_ for our app in the +`FreedomBox Wiki `_ 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 ``.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 ` +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 /transmission.') + ] + +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 ` +documentation for more information. diff --git a/doc/dev/tutorial/setup.rst b/doc/dev/tutorial/setup.rst new file mode 100644 index 000000000..5a3f111b7 --- /dev/null +++ b/doc/dev/tutorial/setup.rst @@ -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. diff --git a/doc/dev/tutorial/skeleton.rst b/doc/dev/tutorial/skeleton.rst new file mode 100644 index 000000000..771507d0a --- /dev/null +++ b/doc/dev/tutorial/skeleton.rst @@ -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/ + │ └─┬ 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 +`_. 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 +``.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) diff --git a/doc/dev/tutorial/view.rst b/doc/dev/tutorial/view.rst new file mode 100644 index 000000000..4e04cfb7e --- /dev/null +++ b/doc/dev/tutorial/view.rst @@ -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 ` 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 /transmission.' + ] + + 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.