Metadata-Version: 2.5
Name: django-htmx-calendar
Version: 0.3.1
Summary: A reusable Django calendar app with HTMX-powered navigation and recurring events.
Project-URL: Repository, https://github.com/taut-and-yare/django-htmx-calendar
Author-email: Taut and Yare <dev@yare.fr>
License: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: django-cotton>=2.6
Requires-Dist: django-crispy-forms>=2.0
Requires-Dist: django-recurrence>=1.11
Requires-Dist: django>=4.2
Requires-Dist: pillow>=9.0
Provides-Extra: cms
Requires-Dist: django-cms>=4.1; extra == 'cms'
Provides-Extra: dev
Requires-Dist: crispy-bootstrap5; extra == 'dev'
Requires-Dist: django-cms>=4.1; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-django; extra == 'dev'
Requires-Dist: wagtail>=5.0; extra == 'dev'
Provides-Extra: wagtail
Requires-Dist: wagtail>=5.0; extra == 'wagtail'
Description-Content-Type: text/markdown

# 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](https://django-cotton.com) 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

```bash
pip install django-htmx-calendar
```

Add to `INSTALLED_APPS`:

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

Add the middleware that sets `request.site`:

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

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

```python
# 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):

```python
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:

```bash
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

```python
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:

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

Signature:

```python
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):

```python
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:**

```python
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:

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

## 6. Template customisation

Set your project's base template:

```python
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:

   ```django
   <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:

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

## 7. djangoCMS integration

```bash
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):

```python
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

```bash
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:

```python
from htmx_calendar.wagtail_blocks import CalendarChooserBlock

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

To extend the admin queryset scoping, subclass `CalendarSnippetViewSet`:

```python
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 |
