# django-todo: a pluggable task board for an existing Django site

> django-todo is a reusable Django app that adds multi-user, multi-group task lists, tickets, comments and email notification to a site you already run, with CSV import, export and a public submission form. It is BSD-3-Clause licensed and installable with one pip line, but it assumes your project already has users, groups, authentication and a template block to hook into.

**shacker/django-todo** — A multi-user, multi-group todo/ticketing system for Django projects. Includes CSV import and integrated mail tracking.

- Repository: https://github.com/shacker/django-todo
- Website: http://django-todo.org
- Stars: 852 · Forks: 286
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/shacker-django-todo

## An app to drop into a site that already has users

django-todo is a pluggable, multi-user, multi-group task management and assignment application for Django, designed to be dropped into an existing site as a reusable app. That framing matters more than the feature list, because it tells you the shape of the work involved. This is not an application you run, it is a component you wire into something you already run.

The documentation says it explicitly: django-todo is a Django app, not a project site, and it needs a site to live in. You can install it into an existing Django project, or clone the separate demo site, the GTD project, which shows the whole thing assembled.

The model is group centric. The assumption is that your organization has multiple groups of employees, each with multiple users, where actual users and groups map to Django Users and Groups. A user may belong to multiple groups, and each group can have multiple todo lists. Users can view and modify all lists belonging to their groups, but only users with is_staff can add or delete lists. Identical list names can exist in different groups but not in the same group.

There is a hard prerequisite that catches people out: you must have at least one Group set up in the Django admin, and that group must have at least one user as a member. This is true even if you are the sole user of the application.

The application is auth-only, meaning every view requires a login, and you must set up a login system and at least one group before deploying.

## The install is short, the integration is not

Installation itself is quick. Put the todo package somewhere on your Python path or install it from PyPI:

```bash
    pip install django-todo
```

For an unreleased version straight from git, pin to a commit or tag because branch heads drift:

```bash
    pip install git+https://github.com/shacker/django-todo.git@<commit-or-tag>
```

The project also documents a uv workflow, with `uv add git+https://github.com/shacker/django-todo.git@<commit-or-tag>` for a pinned git install and `uv add --editable /path/to/local/checkout` for local development.

Then the app has to be registered, migrated, routed and linked. Add it to INSTALLED_APPS:

```python
INSTALLED_APPS = (
    ...
    'todo',
)
```

Create the database tables with `python manage.py migrate todo`, add the URL conf entry `path('todo/', include('todo.urls', namespace="todo")),`, and add navigation links to your site:

```html
<a href="{% url 'todo:lists' %}">Todo Lists</a>
<a href="{% url 'todo:mine' %}">My Tasks</a>
```

The part that decides whether the integration takes an hour or a day is the template contract. The views insert additional CSS and JavaScript into page heads, so your base template must provide an extrahead block and an extra_js block. django-todo ships its own todo/base.html that extends your master base.html, and all content lives inside a content block. If your project names that block something else, you have to override and alter the provided templates.

Two other integrations are easy to miss. The app uses the Django messages system, so your base.html needs something that displays messages. And the Django sites framework must be installed with a default site specified, for example SITE_ID = 1.

## Email notification, comment threads and per-list senders

Notification is a first class feature rather than an add-on. Emails are generated to the assigned-to person when new tasks are created, and every participant in a comment thread receives email when new comments are added.

The email setup has one trap that will bite you on the first test. The site must be set up to send email, and in particular DEFAULT_FROM_EMAIL must be a sender address your mail provider will actually relay, because the Django default of webmaster@localhost is rejected outright by most providers. No django-todo specific settings are required for basic notifications.

Per-task-list senders and mail tracking are the exception, handled through TODO_MAIL_BACKENDS, which you only need if you want those features. Mail tracking is one of the headline features: it unifies a task list with an email box, so mail sent to a list address can be tracked as part of the task flow rather than living in a separate inbox.

The public ticket form is the other half of the story. It lets people who have no account submit a ticket, which is what turns this from a personal to-do tracker into an intake system for an organization. To use it you first create the list that those tickets should be filed into, then add its slug to TODO_DEFAULT_LIST_SLUG in settings. The submission can be redirected to a named URL, and tasks can be assigned to a default user by username. The README warns that if the slug is unset, tickets default to the first list found, which is probably not what you want.

Attachments and search round out the feature set, along with drag and drop task prioritization, comments on tasks, a separate My Tasks view across lists, batch CSV import and export, and multiple file attachments per task. Attachment file types are limited by default and can be changed in settings, and file size limits are left to your web server configuration, which the README notes is not handled separately by the app.

## Settings that decide who sees what

The settings surface is small and mostly about the public submission path and the staff restriction. TODO_STAFF_ONLY restricts access to all todo lists and views to is_staff users, and when it is False or unset all users can see all views, though finer permissions are still enforced inside the views, such as requiring staff for adding and deleting lists.

The public form settings are the ones with real consequences. TODO_DEFAULT_ASSIGNEE is the username that submitted public tickets are assigned to, and it must be a valid username in your system or the tickets go to Anyone. TODO_DEFAULT_LIST_SLUG names the list that receives them. TODO_PUBLIC_SUBMIT_REDIRECT is the named URL a submitter is sent to after filing, and it defaults to the site root, which is why the default is described as the least useful value.

A block of the settings file looks like this:

```python
# Restrict access to ALL todo lists/views to `is_staff` users.
# If False or unset, all users can see all views (but more granular permissions are still enforced
# within views, such as requiring staff for adding and deleting lists).
TODO_STAFF_ONLY = True

# If you use the "public" ticket filing option, to whom should these tickets be assigned?
# Must be a valid username in your system. If unset, unassigned tickets go to "Anyone."
TODO_DEFAULT_ASSIGNEE = 'johndoe'
```

Attachments are separately switchable, and the settings file also covers limiting filetypes. The full list of optional parameters is in the settings section of the documentation, and the ones shown above are the ones that change behaviour rather than cosmetics.

The templates are deliberately replaceable. The provided ones are described as fairly bare-bones starting points, they now ship as Bootstrap examples, and there is no hard dependency on Bootstrap. To override one, create a todo folder in your project's templates directory and copy the template from the django-todo source into it.

## Dependencies and a version story that needs reading twice

Here is where a careful reader has to slow down, because the README and the package metadata tell different stories.

The README lists requirements as Django 2.0+, Python 3.6+, jQuery in the full version rather than the slim build because of drag and drop prioritization, Bootstrap v5, bleach and django-autocomplete-light, which is optional but required for task merging. It also says django-todo v2 makes use of features only available in Django 2.0 and will not work in previous versions.

The pyproject.toml for version 2.7.0 says something different and considerably stricter. It sets requires-python to 3.11 or newer, lists Django 5.2 or newer as a dependency, and the classifiers name Django 5.2 and Python 3.11 specifically. The runtime dependencies are Django, factory-boy, titlecase, bleach, django-autocomplete-light and html2text.

So the README's 2.0+ and 3.6+ lines describe the historical floor of the v2 line, while the actual installable requirement today is Django 5.2 and Python 3.11. If you are starting a new project, follow the metadata, not the prose.

There is also a drift in the Python packaging itself. The project is built with setuptools, and setup.py in the repository root is now just a two-line shim that calls setup() with no arguments, because the real configuration moved into pyproject.toml. The repository also carries a pytest.ini, a test_settings.py and an uv.lock, so the test setup and a lock file are present even though the README does not describe the test workflow.

One requirement is easy to under-emphasise. jQuery must be the full build, not the slim one, because the drag and drop prioritization depends on it. A slim jQuery in an existing project is a silent breakage that shows up as broken drag and drop rather than a console error.

## Maintenance signals and where this fits against the alternatives

The repository is not archived and the last push was on 2026-08-29. There are no GitHub releases, so versioning happens through the package metadata alone, where the current version is 2.7.0 and the author is Scot Hacker. The licence is BSD-3-Clause, declared in pyproject.toml with a LICENSE file at the repository root, and there is a SECURITY.md alongside the code.

For an internal tool, a package with no release tags and a single author is a fair trade. You get a mature, readable app with a small dependency set, but you also take on the risk of depending on one person's continued attention, and a pip install from PyPI gives you a version number rather than a changelog to read.

The alternative approach is worth naming. Building a ticket board yourself on top of Django generic views gives you full control over the data model and the UI, and avoids the assumptions django-todo makes, but it means writing the group permissions, the CSV import, the email notification and the public anonymous form yourself. django-todo is faster to adopt and you inherit its opinionated model of users mapping to Django Groups and lists belonging to groups. The generic views route is slower and you end up with code you fully understand.

A hosted ticketing product is the third option, and the difference is where the data lives and who operates the upgrade path. With django-todo, the tasks live in your database, the users are your users, and the upgrade path is a pip install. That is the case for it when the organization already runs Django and wants everything in one place.

## Conclusion

Adopt django-todo if you already run a Django site with real users and groups and want an internal task or ticket system rather than a separate service. Do not adopt it for a public-facing project, since every view requires login and the public form is the only anonymous path, and skip it if you need a modern release train, because the package metadata asks for Django 5.2 and Python 3.11 while the README still lists Django 2.0 and Python 3.6. Verify first that your base template exposes an extrahead and an extra_js block, that a Group with at least one member exists, and that DEFAULT_FROM_EMAIL is an address your provider will relay.

## FAQ

### How do I install django-todo into an existing Django project?

Install the package with `pip install django-todo`, add 'todo' to INSTALLED_APPS, run `python manage.py migrate todo`, add `path('todo/', include('todo.urls', namespace="todo")),` to your URL conf and link to the todo:lists and todo:mine views. Your base template must also provide extrahead and extra_js blocks.

### Which Django and Python versions does django-todo require?

The package metadata for version 2.7.0 sets requires-python to 3.11 or newer and depends on Django 5.2 or newer. The README still lists Django 2.0+ and Python 3.6+, which describes the historical floor of the v2 line rather than the current requirement.

### Does django-todo work for anonymous users submitting tickets?

Only through the public ticket filing form. Every view requires a login, and to enable the public form you must create the target list and set TODO_DEFAULT_LIST_SLUG, plus TODO_DEFAULT_ASSIGNEE and TODO_PUBLIC_SUBMIT_REDIRECT.

### Why do my email notifications from django-todo get rejected?

DEFAULT_FROM_EMAIL must be an address your mail provider will actually relay, because the Django default of webmaster@localhost is rejected outright by most providers. Per-task-list senders and mail tracking are configured separately through TODO_MAIL_BACKENDS.

### What license is django-todo released under?

BSD-3-Clause, declared in the pyproject.toml metadata with the text in the LICENSE file at the repository root. The project is authored by Scot Hacker.

## Sources

- [Issues](https://github.com/shacker/django-todo/issues)
- [License: BSD-3-Clause](https://github.com/shacker/django-todo/blob/master/LICENSE)
- [Project website](http://django-todo.org)
- [README](https://github.com/shacker/django-todo/blob/master/README.md)
- [shacker/django-todo on GitHub](https://github.com/shacker/django-todo)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/shacker-django-todo
