This manual is meant for developers intending to develop applications for FreedomBox. It provides a step by step tutorial and an API reference.

Writing Applications - Tutorial

This tutorial covers writing an application for FreedomBox. FreedomBox is a pure blend of Debian with a web interface, known as Plinth, that configures its applications. We shall discuss various aspects of building an application for FreedomBox, by creating an example application.

There are two parts to writing a FreedomBox application. First is to make sure that the application is available as a Debian package uploaded to the repositories. This is the majority of the work involved. However, if an application is already available in Debian repositories, it is trivial to build a FreedomBox UI for it. The second part of writing an application for FreedomBox is to provide a thin web interface layer for configuring the application. This is done by extending Plinth's user interface to provide visibility to the application and to let the user control its operations in a highly simplified way. This layer is referred to as 'Plinth application'.

Plinth applications can either be distributed as part of Plinth source code by submitting the applications to the Plinth project or they can distributed independently. This tutorial covers writing an application that is meant to be distributed as part of Plinth. However, writing independent Plinth applications is also very similar and most of this tutorial is applicable.

Note

The term application, in this tutorial, is used to mean multiple concepts. FreedomBox application is a combination of Debian package and a web interface layer. The web interface layer is also called a Plinth application which is very similar to and built upon a Django application.

1. Before we begin

Plinth is a web interface built using Python3 and Django. FreedomBox applications are simply Django applications within the Plinth project. Hence, for the most part, writing a FreedomBox application is all about writing a Django application.

You should start by reading the Django tutorial. All the concepts described there are applicable for how plinth and its applications are be built.

2. Picking an application

We must first, of course, pick an application to add to FreedomBox. For the purpose of this tutorial, let us pick Tiny Tiny RSS. The project description reads as, Tiny Tiny RSS is an open source web-based news feed (RSS/Atom) reader and aggregator, designed to allow you to read news from any location, while feeling as close to a real desktop application as possible.

Choosing an application

When choosing an application we must make sure that the application 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. Applications should not even ask the users questions to this effect, because users have already made a choice.

3. 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 here.

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:

4. 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/
  |   |
  |   +- ttrss/
  |     |
  |     +- __init__.py
  |     |
  |     +- forms.py
  |     |
  |     +- urls.py
  |     |
  |     +- views.py
  |     |
  |     +- templates/
  |     | |
  |     | +- ttrss.html
  |     |
  |     +- tests
  |       |
  |       +- __init__.py
  |
  +- actions/
  | |
  | +- ttrss
  |
  +- data/
    |
    +- etc/
      |
      +- plinth/
        |
        +- modules-enabled/
          |
          +- ttrss

The __init__.py indicates that the directory in which it is present is a Python module. For now, it is an empty file.

Plinth's setup script setup.py will automatically install the plinth/modules/ttrss directory (along with other files described later) to an appropriate location. If you are creating an application that stays independent and outside of Plinth source tree, then your setup.py script will need to install it 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.*.

5. Tell Plinth that we exist

The first thing to do is tell Plinth that our application exists. This is done by writing a small file with the Python import path to our application and placing it in data/etc/plinth/modules-enabled/. Let us create this file ttrss:

plinth.modules.ttrss

This file is automatically installed to /etc/plinth/modules-enabled/ by Plinth's installation script setup.py. If we are writing a module that resides independently outside the Plinth's source code, the setup script will need to copy it to the target location. Further, it is not necessary for the application to be part of the plinth.modules namespace. It can, for example, be plinth_ttrss.

6. Writing the URLs

For a user to visit our application in Plinth, 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:

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^apps/ttrss/$', views.index, name='index'),
]

This routes the /apps/ttrss/ URL to a view called index defined in plinth/modules/ttrss/views.py. This is no different than how routing URLs are written in Django. See Django URL dispatcher for more information.

7. Adding a menu item

We have added a URL to be handled by our application but this does not yet show up to be a link in Plinth web interface. Let us add a link in the applications list. In __init__.py add the following:

from django.utils.translation import ugettext_lazy as _
from plinth.menu import main_menu

def init():
    """Initialize the module."""
    menu = main_menu.get('apps')
    menu.add_urlname(_('Tiny Tiny RSS'), 'glyphicon-bullhorn',
                     'ttrss:index', _('News Feed Reader'))

As soon as Plinth starts, it will load all the enabled modules into memory. 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 <app>.init(). Here we have implemented this method and added our menu item to the applications menu as part of the initialization process.

We wish to add our menu item to the list of applications which is why we have retrieved the applications menu which is available under the main menu. After this we add our own menu item to this menu. There are several parameters during this process that are important:

We have used the application menu item to insert our own menu item as a child. To be able to use the application menu item, we need to make sure that the module providing the application menu is loaded before our application is loaded. We will do that in the next step.

8. Specifying module dependencies

Specifying a simple list of applications to be loaded before our application provided to Plinth is sufficient. Add this in __init__.py.

depends = ['plinth.modules.apps']

Plinth will now make sure that the apps module is loaded before our module is loaded. Application initialization is also ensured to happen in this order. We can safely use any features of this module knowing that they have been initialized.

Circular dependencies

Circular dependencies are not possible among Plinth applications. Attempting to add them will result in error during startup.

9. Writing the enable/disable form

We wish to provide a user interface to the user to enable and disable the application. Complex modules may require more options but this is sufficient for our application. Add the following forms.py.

from django import forms

class TtrssForm(forms.Form):
    """Tiny Tiny RSS configuration form."""
    enabled = forms.BooleanField(
        label='Enable Tiny Tiny RSS',
        required=False)

This creates a Django form that shows a single option to enable/disable the application. It also shows its current state. This is how a regular Django form is built. See Django Forms documentation for more information.

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.

10. Writing a view

In views.py, let us add a view that can handle the URL we have provided above.

from .forms import TtrssForm

def index(request):
    """Serve configuration page."""
    status = get_status()

    form = None

    if request.method == 'POST':
        form = TtrssForm(request.POST, prefix='ttrss')
        if form.is_valid():
            _apply_changes(request, status, form.cleaned_data)
            status = get_status()
            form = TtrssForm(initial=status, prefix='ttrss')
    else:
        form = TtrssForm(initial=status, prefix='ttrss')

    return TemplateResponse(request, 'ttrss.html',
                            {'title': 'News Feed Reader (Tiny Tiny RSS)',
                             'status': status,
                             'form': form})

This view works with the form we created in the previous step. It shows the current status of the service in form. This status is retrieved with the help of get_status() helper method. When the form is posted, again this view is called and it verifies whether the form's input values are correct. If so, it will apply the actions necessary for changed form values using the _apply_changes() method.

11. Getting the current status of the application

The view in the previous setup requires the status of the application to be retrieved using the get_status() method. Let us implement that method in views.py.

from plinth.modules import ttrss

def get_status():
    """Get the current status."""
    return {'enabled': ttrss.is_enabled()}

This method retrieves the various statuses of the application for display in the view. Currently, we only need to show whether the application is enabled or disabled. So, we retrieve that using a helper method defined in __init__.py.

from plinth import action_utils

def is_enabled():
    """Return whether the module is enabled."""
    return action_utils.webserver_is_enabled('50-tt-rss')

This method uses one of the several action utilities provided by Plinth. This method checks whether a webserver configuration named 50-tt-rss is enabled.

12. Displaying the application page

The view that we have written above requires a template file known as ttrss.html to work. This template file controls how the web page for our application is displayed. Let us create this template file in templates/ttrss.html.

{% extends "base.html" %}

{% load bootstrap %}

{% block content %}

<h2>News Feed Reader (Tiny Tiny RSS)</h2>

<p>Tiny Tiny RSS is a news feed (RSS/Atom) reader and aggregator,
  designed to allow you to read news from any location, while feeling
  as close to a real desktop application as possible.</p>

<h3>Configuration</h3>

<form class="form" method="post">
  {% csrf_token %}

  {{ form|bootstrap }}

  <input type="submit" class="btn btn-primary" value="Update setup"/>
</form>

{% endblock %}

This template extends an existing template known as base.html. This template is available in Plinth core to provide all the basic layout, styling, menus, JavaScript and CSS libraries. We will override the content area of the base template and keep the rest.

Yet again, there is nothing special about the way this template is written. This is a regular Django template. See Django Template documentation.

For styling and UI components, Plinth uses the Twitter Bootstrap project. See Bootstrap documentation for reference.

13. Applying the changes from the form

The view we have created displays the form and processes the form after the user submits it. It used a helper method called _apply_changes() to actually get the work done. Let us implement that method in views.py.

from django.contrib import messages

from plinth import actions

def _apply_changes(request, old_status, new_status):
    """Apply the changes."""
    modified = False

    if old_status['enabled'] != new_status['enabled']:
        sub_command = 'enable' if new_status['enabled'] else 'disable'
        actions.superuser_run('ttrss', [sub_command])
        modified = True

    if modified:
        messages.success(request, 'Configuration updated')
    else:
        messages.info(request, 'Setting unchanged')

We check to make sure that we don't try to disable the application when it is already disabled or try to enable the application when it is already enabled. Although Plinth's operations are idempotent, meaning that running them twice will not be problematic, we still wish avoid unnecessary operations for the sake of speed.

We are actually perform the operation using Plinth actions. We will implement the action to be performed a bit later.

After we perform the operation, we will show a message on the response page showing that the action was successful or that nothing happened. We use the Django messaging framework to accomplish this. See Django messaging framework for more information.

14. Installing packages required for the application

Plinth takes care of installing all the Debian packages required for our application to work. All we need to do is specify the list of the Debian packages required using a decorator on our view as follows:

from plinth import package

@package.required(['tt-rss'])
def index(request):
    """Serve configuration page."""
    ...

The first time this application's view is accessed, Plinth shows a package installation page and allows the user to install the required packages. After the package installation is completed, the user is shown the application's configuration page.

If there are configuration tasks to be performed immediately before or after the package installation, Plinth provides hooks for it. The before_install= and on_install= parameters to the @package.required decorator take a callback methods that are called before installation of packages and after installation of packages respectively. See the reference section of this manual or the plinth.package module for details. Other modules in Plinth that use this feature provided example usage.

15. Writing actions

The actual work of performing the configuration change is carried out by a Plinth 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/ttrss.

import argparse

from plinth import action_utils


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('enable', help='Enable Tiny Tiny RSS')
    subparsers.add_parser('disable', help='Disable Tiny Tiny RSS')

    return parser.parse_args()


def subcommand_enable(_):
    """Enable web configuration and reload."""
    action_utils.webserver_enable('50-tt-rss')


def subcommand_disable(_):
    """Disable web configuration and reload."""
    action_utils.webserver_disable('50-tt-rss')


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 uses a helper utility provided by Plinth to actually enable and disable Apache2 web server configuration.

This script is automatically installed to /usr/share/plinth/actions by Plinth'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 Plinth's source code, your setup.py script will need to take care of copying the file to the target location.

16. Creating diagnostics

Plinth provides a simple API for showing diagnostics results. The application has to implement a method for actually running the diagnostics and return the results as a list. Plinth then takes care of calling the diagnostics method and displaying the list in a formatted manner.

To implement the diagnostics method, method called diagnose() has to be available as <app>.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.

def diagnose():
    """Run diagnostics and return the results."""
    results = []

    results.extend(action_utils.diagnose_url_on_all(
        'https://{host}/ttrss', extra_options=['--no-check-certificate']))

    return results

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 /ttrss 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 Plinth.

The user can trigger the diagnostics test by going to System -> Diagnostics page. This runs diagnostics for all the applications. If we want users to be able to run diagnostics specifically for this application, we can include a button for it in our template immediately after the application description.

{% include "diagnostics_button.html" with module="ttrss" enabled=True %}

17. Logging

Sometimes we may feel the need to write some debug messages to the console and Plinth log file. Doing this in Plinth is just like doing this any other Python application.

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 logging framework documentation.

18. Adding a License

Plinth is licensed under the GNU Affero General Public License Version 3 or later. FreedomBox UI applications, which run as modules under Plinth, also need to be under the same license or under a compatible license. The license of our application needs to clear for our application to be accepted by users and other developers. Let us add license headers to our application.

#
# 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.

{% 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 %}

...

19. Internationalization

Every string message that is visible to the user must be localized to user's native language. For this to happen, our application needs to be internationalized. This requires marking the user visible messages for translation. Plinth applications use the Django's localization methods to make that happen.

from django.utils.translation import ugettext as _

def index(request):
    ...
    return TemplateResponse(request, 'ttrss.html',
                            {'title': _('News Feed Reader (Tiny Tiny RSS)'),
                             'status': status,
                             'form': form})

Notice that the page's title is wrapped in the _() method call. Let us do that for the menu item of the application too.

from django.utils.translation import ugettext_lazy as _

def init():
    """Initialize the module."""
    menu = cfg.main_menu.get('apps:index')
    menu.add_urlname(_('News Feed Reader (Tiny Tiny RSS)'), 'glyphicon-envelope',
                     'ttrss:index', 600)

Notice that in this case, we have used the ugettext_lazy and in the first case we have used the regular ugettext. This is because in the second case the 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 should not be used for other users. The _lazy method 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 looked is made and string is returned immediately.

All of this is the usual way internationalization is done in Django. See Django internationalization and localization documentation for more information.

20. Coding standards

For readability and easy collaboration it is important to follow common coding standards. Plinth 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:

"""
FreedomBox app to configure Tiny Tiny RSS.
"""

Reference Guide

This section describes Plinth API that is most frequently used by application. Note that since Plinth 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 Plinth applications currently reside in Plinth source repository itself and are updated when the API is updated.

1. Applications

These methods are optionally provided by the application and Plinth calls/uses them if they are present.

1.1. <application>.init()

Optional. This method is called by Plinth 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.

1.2. <application>.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:

[('Check http://localhost/app is reachable', 'passed'),
 ('Check configuration is sane', 'passed')]

1.3. <appliation>.depends

Optional. This module property must contain a list of all applications that this application depends on. The application is specified as string containing the full module load path. For example, plinth.modules.apps.

1.4. plinth.package.required(package_list, before_install=None, on_install=on_install)

Make sure that a set of Debian packages are installed before a view can be accessed. If the packages are not currently installed on the system, a special installation view is displayed showing the list of packages to be installed. If the user chooses to proceed, package installation will start and an installation progress screen will be shown. After completion of the installation process, the original view is shown.

The package_list must be an iterable containing the Debian package names as strings. If provided, the before_install callable is called just before the installation process starts. Similarly, on_install callable is called just after the package installation completes.

2. Actions

Plinth'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 applications of the FreedomBox and nothing else. These actions are also directly usable by a skilled administrator.

The following methods are provided by Plinth to run actions and to implement them easily by reusing code for common tasks.

2.1. plinth.actions.run(action, options=None, input=None, async=False)

Run an action command present under the actions/ directory. This runs subprocess.Popen() after some checks. The action must be present in the actions/ directory.

options are a list of additional arguments to pass to the command. If input is given it must be bytearray containing the input to pass on to the executed action. If async is set to True, the method will return without waiting for the command to finish.

2.2. plinth.actions.superuser_run(action, options=None, input=None, async=False)

This is same as plinth.actions.run() except the command is run with superuser privelages.

2.3. plinth.action_utils

Several utilities to help with the implementation of actions and diagnostic tests are implemented in this module. Refer to the module source code for a list of these methods and their documentation.

3.1. plinth.cfg.main_menu

This is a reference to the global main menu. All menu entries in Plinth are descendents of this menu item. See Menu.add_item() and Menu.add_urlname() for adding items to this menu or its children.

3.2. plinth.menu.Menu.get(self, urlname, url_args=None, url_kwargs=None)

Return a child of this menu item. urlname must be the name of a URL as configured in Django. django.core.urlresolvers.reverse() is called before the lookup for child menu item is performed. url_args and url_kwargs are passed on to reverse().

3.3. plinth.menu.Menu.add_item(self, label, icon, url, order=50)

Add a menu item as a child to the current menu item. label is the user visible string shown for the menu item. icon must be a glyphicon class from the Twitter Bootstrap library. url is the relative URL to which this menu item will take the user to.

3.4. plinth.menu.Menu.add_urlname(self, label, icon, urlname, order=50, url_args=None, url_kwargs=None)

Same as plinth.menu.Menu.add_item() but instead of URL as input it is the name of a URL as configured in Django. django.core.urlresolvers.reverse() is called before it is added to the parent menu item. url_args and url_kwargs are passed on to reverse().

4. Services

4.1. plinth.service.Service.__init__(self, service_id, name, ports=None, is_external=False, enabled=True)

Create a new Service object to notify all applications about the existence and status of a given application. service_id is a unique identifier for this application. name is a display name of this application that is shown by other applications such as on the firewall status page. ports is a list of names recognized by firewalld when enabling or disabling firewalld services. If is_external is true, the ports for this service are accessible from external interfaces, that is, from the Internet. Otherwise, the service is only available for client connected via LAN. enabled is the current state of the application.

4.2. plinth.service.Service.is_enabled(self)

Return whether the service is currently enabled.

4.3. plinth.service.Service.notify_enabled(self, sender, enabled)

Notify other applications about the change of status of this application. sender object should identify which application made the change. enabled is a boolean that signifies whether the application is enabled (= True) or disabled (= False).

This is typically caught by the firewall application to enable or disable the ports corresponding to an application.


Information

Support

Contribute

Reports

Promote

Overview

Hardware

Live Help

Where To Start

Translate

Calls

Talks

Features

Vision

Q&A

Design

To Do

Releases

Press

Download

Manual

Code

Contributors

Blog

HELP & DISCUSSIONS: Discussion Forum - Mailing List - #freedombox irc.debian.org | CONTACT Foundation | JOIN Project

Next call: Sunday, September 22nd at 17:00 UTC

Latest news: Announcing Pioneer FreedomBox Kits - 2019-03-26

This page is copyright its contributors and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.


CategoryFreedomBox