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-recurrencedjango-crispy-forms, with a template pack configureddjango-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:
-
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 withhtmx_calendar.forms.AddEventForm(calendar=calendar)). -
Provide the modal:
<dialog id="cal-dialog"><div id="dialog"></div></dialog>. -
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 torequest.site - Model level:
CalendarPluginModel.clean()rejects cross-site assignments - Render time:
calendar_permission_requiredhard-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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77e6e7b035a024290b5f85562ba328fbcf9b87973ade22693a5e26b33f6af06e
|
|
| MD5 |
90c9abb4537966f530cc1bbcf78ae7c7
|
|
| BLAKE2b-256 |
4c5479d363a94cab730151d3b104cc19364e53b177cb7cd163be8ebe3e1acde9
|
File details
Details for the file django_htmx_calendar-0.3.1-py3-none-any.whl.
File metadata
- Download URL: django_htmx_calendar-0.3.1-py3-none-any.whl
- Upload date:
- Size: 83.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe2980d88017635c3806ef7ba8d95ff7c376c1b2273920b691a428e6de262609
|
|
| MD5 |
464d1e9235d7fd9429297303e1e80b72
|
|
| BLAKE2b-256 |
ece537c84e3ae7cc28765d22cb485ad7ae682f7e262850dc5cc0ebfe11e586a2
|