This is a short guide to help you get started with Postorius development.

Directory layout

Postorius is a Django application, so if you have developed with Django before, the file structure probably looks familiar. These are the basics:
auth/                   # Custom authorization code (List owners and
                        # moderators)   # Some variables available in all templates
doc/                    # Sphinx documentation       # A custom form class to build html forms with
                        # fieldsets                # All kinds of classes to build and validate forms
management/             # Commands to use with Django's script               # Code to connect to mailman.client and provide
                        # a Django-style model API for lists, mm-users and
                        # domains
static/                 # Static files (CSS, JS, images)
templates/              # HTML Templates
tests/                  # Postorius test files                 # URL routes                # Some handy utilities
views/            # View classes and functions for all views connected
                        # in          # Generic class-based views; Currently holds a
                        # class for views based on a single mailing list

Development Workflow

The source code is hosted on Launchpad, which also means that we are using Bazaar for version control.

Changes are usually not made directly in the project’s trunk branch, but in feature-related personal branches, which get reviewed and then merged into the trunk.

The ideal workflow would be like this:

  1. File a bug to suggest a new feature or report a bug (or just pick one of the existing bugs).
  2. Create a new branch with your code changes.
  3. Make a “merge proposal” to get your code reviewed and merged.

Launchpad has a nice tour which describes all this in detail.

Writing View Code

When the work on Postorius was started, the standard way to write view code in Django was to write a single function for each different view. Since then Django has introduced so-called ‘class-based views’ which make it much easier to reuse view functionality. While using simple functions is not discuraged, it makes sense to check if using a class-based approach could make sense.

Check Postorius’ views/ and views/ for examples!


Three of Django’s default User roles are relvant for Postorius:

  • Superuser: Can do everything.
  • AnonymousUser: Can view list index and info pages.
  • Authenticated users: Can view list index and info pages. Can (un)subscribe from lists.

Apart from these default roles, there are two others relevant in Postorius:

  • List owners: Can change list settings, moderate messages and delete their lists.
  • List moderators: Can moderate messages.

There are a number of decorators to protect views from unauthorized users.

  • @user_passes_test(lambda u: u.is_superuser) (redirects to login form)
  • @login_required (redirects to login form)
  • @list_owner_required (returns 403)
  • @list_moderator_required (returns 403)
  • @superuser_or_403 (returns 403)
  • @loggedin_or_403 (returns 403)
  • @basic_auth_login

Check out views/ or views/ for examples!

The last one (basic_auth_login) checks the request header for HTTP Basic Auth credentials and uses those to authenticate against Django’s session-based mechanism. It can be used in cases where a view is accessed from other clients than the web browser.

Please make sure to put it outside the other auth decorators.


def my_view_func(request):

Won’t work, because list_owner_required will not recognize the user:

def my_view_func(request):

Accessing the Mailman API

Postorius uses mailman.client to connect to Mailman’s REST API. In order to directly use the client, cd to your project folder and execute python mmclient. This will open a python shell (IPython, if that’s available) and provide you with a client object connected to to your local Mailman API server (it uses the credentials from your

A quick example:

$ python mmclient

>>> client
<Client (user:pwd) http://localhost:8001/3.0/>

>>> print client.system['mailman_version']
GNU Mailman 3.0.0b2+ (Here Again)

>>> mailman_dev = client.get_list('')
>>> print mailman_dev settings
{u'owner_address': u'',
 u'default_nonmember_action': u'hold', ...}

For detailed information how to use mailman.client, check out its documentation.


Currently only some of the Postorius code is covered by a test. We should change that!

All test modules reside in the postorius/src/postorius/tests directory and this is where you should put your own tests, too. To make the django test runner find your tests, make sure to add them to the folder’s

from postorius.tests import test_utils
from postorius.tests.test_list_members import ListMembersViewTest
from postorius.tests.test_list_settings import ListSettingsViewTest
from postorius.tests.my_own_tests import MyOwnUnitTest

__test__ = {
    "Test Utils": test_utils,
    "List Members": ListMembersViewTest,
    "List Settings": ListSettingsViewTest,
    "My Own Test": MyOwnUnitTest,

Running the tests

To run the tests go to your project folder and run python test postorius from there.

Testing mailman.client results

Most of Postorius’ code involves some results from calls to the mailman.client library. mailman.client is itself covered by tests, so Postorius’ own tests don’t need to check if mailman.client returns correct results. Instead we can just mock them! This has the big advantage that you can run the test suite without having to worry about the state of the local Mailman database. It also makes the tests run faster, because we spare ourselves the HTTP calls to the local Mailman REST API.

This approach has the obvious downside that the Postorius tests will not recognize any changes to the Mailman API. So at some point there should be some separate integration tests to test the whole chain. But let’s not worry about that for now.

Mocking mailman.client objects

Postorius uses Michael Foord’s mock library for mocking. There are some shortcuts you can use to quickly create mock objects that behave a little bit like objects retreived from mailman.client, like:

  • create_mock_domain
  • create_mock_list
  • create_mock_member

These create_mock_* functions are very simple tools that return MagigMock objects with the properties passed to them in a dictionary. They also set some defaults for properties that you didn’t pass to its create_mock_* function. For instance, a mock list created with create_mock_list() will always have members, moderators and owners properties.