Skip to main content

django-htmx-calendar

A reusable Django calendar app with HTMX-powered navigation, recurring events, per-calendar status workflow, and optional CMS integrations.

1. What it is

django-htmx-calendar provides:

  • Monthly, weekly, daily, and yearly calendar views
  • Recurring events via django-recurrence
  • Add, edit and delete events in a modal, plus a read-only event summary (date and time, location, description, photo)
  • Per-calendar event status workflow (draft → pending → published)
  • HTMX-powered navigation without full page reloads
  • A pluggable permission system
  • Optional djangoCMS plugin and Wagtail StreamField block

2. Requirements

  • Python 3.11+
  • Django 4.2+
  • HTMX, loaded in your base template (tested with HTMX 2)
  • django-recurrence
  • django-crispy-forms, with a template pack configured
  • django-cotton (see below)
  • Pillow (event photos)

A note on django-cotton

The calendar's templates are built from django-cotton components (<c-monthly-grid /> and friends), so django-cotton is installed as a dependency and must be in INSTALLED_APPS. We may make it optional in a future release.

3. Installation

pip install django-htmx-calendar

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "django.contrib.sites",   # required
    "django_cotton",
    "crispy_forms",
    "recurrence",
    "htmx_calendar",
]

Add the middleware that sets request.site:

MIDDLEWARE = [
    ...
    "django.contrib.sites.middleware.CurrentSiteMiddleware",  # required
]

Include URLs, and the JavaScript catalog that the recurrence widget loads from /jsi18n/:

# urls.py
from django.urls import include, path
from django.views.i18n import JavaScriptCatalog

urlpatterns = [
    path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
    path("cal/", include("htmx_calendar.urls")),
]

Add the context processors (request is needed for permission checks in templates):

TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            ...
            "django.template.context_processors.request",
            "htmx_calendar.context_processors.calendrier",
        ],
    },
}]

Event photos are stored with your default storage under events/, so MEDIA_ROOT and MEDIA_URL must be configured and served.

Run migrations:

python manage.py migrate

Upgrading from calendrier (0.1.x)

The app was renamed from calendrier to htmx_calendar in 0.2.0. Replace calendrier with htmx_calendar in INSTALLED_APPS, URL includes, imports and template paths, then run migrate: migration 0010 renames the database tables and content types. Setting names keep their CALENDRIER_ prefix.

4. Quick start

from django.contrib.sites.models import Site
from htmx_calendar.models import Calendar

site = Site.objects.get_current()
Calendar.objects.create(site=site, name="My Events")

Visit /cal/my-events/monthly/.

5. Permission configuration

By default, views are publicly readable and any active is_staff user may add, edit, delete and publish events. Override this via:

# settings.py
CALENDRIER_PERMISSION_CHECK = "myapp.auth.my_calendar_check"

Signature:

def my_calendar_check(user, calendar, action) -> bool:
    ...

Available actions (import from htmx_calendar.permissions): ACTION_VIEW, ACTION_ADD_EVENT, ACTION_EDIT_EVENT, ACTION_DELETE_EVENT, ACTION_PUBLISH_EVENT, ACTION_APPROVE_EVENT.

The permission check decides:

  • who sees the "Add event" button and can open the add, edit and delete views;
  • whether a new event is published straight away (ACTION_PUBLISH_EVENT) or left pending;
  • who sees unpublished events in the event summary, and its Edit button (ACTION_EDIT_EVENT).

Multi-site example (one editors group per site):

EDITOR_GROUPS = {"example.com": "ExampleEditors", "other.org": "OtherEditors"}

def my_calendar_check(user, calendar, action):
    from htmx_calendar.permissions import ACTION_VIEW
    if action == ACTION_VIEW:
        return True
    if not user.is_authenticated:
        return False
    group_name = EDITOR_GROUPS.get(calendar.site.domain)
    return group_name is not None and user.groups.filter(name=group_name).exists()

Single-site example:

def my_calendar_check(user, calendar, action):
    from htmx_calendar.permissions import ACTION_VIEW
    if action == ACTION_VIEW:
        return True
    return user.is_authenticated and user.is_active

In templates, check a permission with the calendar_can tag:

{% load cal_tags %}
{% calendar_can calendar "add_event" as can_add %}
{% if can_add %}{% endif %}

6. Template customisation

Set your project's base template:

CALENDRIER_BASE_TEMPLATE = "myapp/base.html"

htmx_calendar/base.html extends it and fills three blocks: extra_head (the package assets), modal (a <dialog id="cal-dialog"> holding the #dialog element the forms and event summary load into) and content. If your base uses different block names, create templates/htmx_calendar/base.html in your project and map them.

Embedding the calendar in your own page

To show a calendar inside another page rather than on its standalone URL:

  1. Load the assets and the form media in the page's head: {% load cal_tags %}{% htmx_calendar_assets %} and {{ add_event_form.media }} (build the form with htmx_calendar.forms.AddEventForm(calendar=calendar)).

  2. Provide the modal: <dialog id="cal-dialog"><div id="dialog"></div></dialog>.

  3. Load a grid into a placeholder:

    <div hx-get="{% url 'htmx_calendar:monthly' calendar_slug='my-events' %}"
         hx-trigger="load"
         hx-swap="outerHTML"></div>
    

Overriding components

Every piece of the UI is a cotton component in templates/components/ (header_section.html, monthly_grid.html, monthly_day.html, daily_grid.html, …). To change one, add a template with the same path in a template directory or app that comes before htmx_calendar in template loading order.

CSS custom properties — override on .calendrier-root in your own CSS:

.calendrier-root {
  --cal-primary: #your-brand-color;
  --cal-border-radius: 0.25rem;
}

7. djangoCMS integration

pip install "django-htmx-calendar[cms]"

Add cms to INSTALLED_APPS and add CurrentSiteMiddleware to your middleware stack (required — get_form reads request.site to scope the calendar queryset):

MIDDLEWARE = [
    ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.sites.middleware.CurrentSiteMiddleware",  # required
    ...
]

The CalendarPlugin appears in the plugin picker automatically. It enforces site isolation at three layers:

  • Admin UI: get_form() filters the calendar queryset to request.site
  • Model level: CalendarPluginModel.clean() rejects cross-site assignments
  • Render time: calendar_permission_required hard-checks site ID

8. Wagtail integration

pip install "django-htmx-calendar[wagtail]"

Calendar is automatically registered as a Wagtail snippet, scoped to the current site in the Wagtail admin.

Add a calendar to any StreamField:

from htmx_calendar.wagtail_blocks import CalendarChooserBlock

body = StreamField([("calendar", CalendarChooserBlock())])

To extend the admin queryset scoping, subclass CalendarSnippetViewSet:

from htmx_calendar.wagtail_hooks import CalendarSnippetViewSet

class MyCalendarViewSet(CalendarSnippetViewSet):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.filter(...)  # additional filtering

Known limitation: block-level validation cannot access the parent page's site. The render-time calendar_permission_required check is the final safety net for cross-site data.

9. Settings reference

Setting Type Default Description
CALENDRIER_PERMISSION_CHECK str (dotted path) None (uses default) Dotted path to permission check callable (user, calendar, action) -> bool
CALENDRIER_BASE_TEMPLATE str "base.html" Template name that htmx_calendar/base.html extends

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_htmx_calendar-0.3.1.tar.gz (85.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_htmx_calendar-0.3.1-py3-none-any.whl (83.0 kB view details)

Uploaded Python 3

File details

Details for the file django_htmx_calendar-0.3.1.tar.gz.

File metadata

  • Download URL: django_htmx_calendar-0.3.1.tar.gz
  • Upload date:
  • Size: 85.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.11

File hashes

Hashes for django_htmx_calendar-0.3.1.tar.gz
Algorithm Hash digest
SHA256 77e6e7b035a024290b5f85562ba328fbcf9b87973ade22693a5e26b33f6af06e
MD5 90c9abb4537966f530cc1bbcf78ae7c7
BLAKE2b-256 4c5479d363a94cab730151d3b104cc19364e53b177cb7cd163be8ebe3e1acde9

See more details on using hashes here.

File details

Details for the file django_htmx_calendar-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for django_htmx_calendar-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 fe2980d88017635c3806ef7ba8d95ff7c376c1b2273920b691a428e6de262609
MD5 464d1e9235d7fd9429297303e1e80b72
BLAKE2b-256 ece537c84e3ae7cc28765d22cb485ad7ae682f7e262850dc5cc0ebfe11e586a2

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page