Metadata-Version: 2.4
Name: servicetrees
Version: 0.2.0
Summary: Reusable Django apps for content-driven multi-site projects
Author: Vasudeva Server
License-Expression: MIT
Project-URL: Source, https://gitlab.com/vasudeva-server/servicetrees
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Programming Language :: Python :: 3
Classifier: Development Status :: 3 - Alpha
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=5.2
Requires-Dist: Babel>=2.17
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: cryptography>=45
Requires-Dist: django-ckeditor-5>=0.2.18
Requires-Dist: django-polymorphic>=4.1
Requires-Dist: nh3>=0.3
Requires-Dist: Pillow>=11
Requires-Dist: requests>=2.31
Requires-Dist: Unidecode>=1.3
Provides-Extra: xlsx
Requires-Dist: openpyxl>=3.1; extra == "xlsx"
Provides-Extra: video
Requires-Dist: yt-dlp; extra == "video"
Dynamic: license-file

# servicetrees

`servicetrees` is a collection of reusable Django apps for content-driven
websites. It provides the common machinery for running one or many sites from
a Django project: domain-aware settings, editor roles, flexible pages, media,
search, redirects, translations, forms, cookie consent, and OpenID Connect.

It is a toolkit rather than a finished CMS. A project chooses the apps it
needs, supplies its public design and base templates, and can register its own
content types and page sections. The package supplies the data models, editor
workflows, extension points, and conservative security defaults.

## How the pieces fit together

- A Django `Site` identifies each domain or subdomain. `domains` resolves the
  current site and stores its settings and editors.
- `displaypage` provides layout-built pages. Its sections are polymorphic, and
  any project model can opt in to the same section system with `HasSections`.
- `mediaapp`, `links`, `search`, and `translations` provide shared services to
  those pages and to project-owned content models.
- Registries and `ST_<APP>_<SETTING>` settings are the public extension points.
  Projects do not need to fork the package to add content or customize policy.
- Templates and static files are deliberately overridable through Django's
  normal template and static-file discovery.

| App | What it does |
|---|---|
| `servicetrees.domains` | Sites and their settings, site resolution from the host, editors and their roles per site, dashboard and settings page, content registry |
| `servicetrees.displaypage` | Layout builder: `DisplayPage`, polymorphic sections that can belong to any model (`HasSections`), native section types |
| `servicetrees.mediaapp` | Media library: hashed, deduplicated uploads, browser modal, usage tracking |
| `servicetrees.links` | Redirects, broken-link scanning and tracking |
| `servicetrees.search` | Site search over configured models |
| `servicetrees.translations` | Editable UI strings per site (`{% t %}`) |
| `servicetrees.ckeditor` | CKEditor 5 widget and configs |
| `servicetrees.cookie_consent` | Klaro cookie consent |
| `servicetrees.openid_connect` | OpenID Connect login for existing users |
| `servicetrees.userprofiles` | User profiles |
| `servicetrees.webform` | Form builder with encrypted submissions |
| `servicetrees.error_handling` | Logged, deduplicated errors |

App labels are `st_<app>` (tables `st_displaypage_…`); settings are
`ST_<APP>_<NAME>`. Each app's settings and defaults are in its `SETTINGS.md`.

## Status

Unmaintained and provided as-is. There is no commitment to updates, security fixes, or support.

Pull requests are welcome and may be looked at, but we are under no pressure to
review or merge them. Contributions are licensed under the same license as this project.

This repository is [MIT licensed](LICENSE). Font Awesome Free and Klaro, fetched at build time, keep their own licences.

## Install

```sh
pip install servicetrees            # extras: servicetrees[xlsx,video]
```

The package supports Python 3.10+ and Django 5.2. Start with a normal Django
project that has `django.contrib.auth`, `django.contrib.contenttypes`,
`django.contrib.sessions`, and `django.contrib.sites` configured.

From a git checkout, fetch the third-party front-end files first (they are not
in git; wheels include them):

```sh
python scripts/fetch_vendor.py
```

## Setup

```python
INSTALLED_APPS += [
    'django.contrib.sites',
    'polymorphic',
    'django_ckeditor_5',
    'servicetrees.domains',
    'servicetrees.displaypage',
    'servicetrees.mediaapp',
    'servicetrees.links',
    'servicetrees.search',
    'servicetrees.translations',
    'servicetrees.ckeditor',
    'servicetrees.cookie_consent',
    'servicetrees.userprofiles',
    'servicetrees.error_handling',
    'servicetrees.openid_connect',   # optional
    'servicetrees.webform',          # optional
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'servicetrees.domains.middleware.WWWRedirectMiddleware',
    'servicetrees.domains.middleware.DynamicCookieDomainSessionMiddleware',   # replaces SessionMiddleware
    'django.middleware.common.CommonMiddleware',
    'servicetrees.domains.middleware.DynamicCookieDomainCsrfMiddleware',      # replaces CsrfViewMiddleware
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'servicetrees.openid_connect.middleware.OIDCLoginInterceptorMiddleware',  # with openid_connect
    'servicetrees.domains.middleware.SubdomainMiddleware',                    # sets request.site
    'servicetrees.links.middleware.LinkRedirectMiddleware',
]

AUTHENTICATION_BACKENDS = [
    'servicetrees.openid_connect.backend.OIDCAuthBackend',  # with openid_connect
    'django.contrib.auth.backends.ModelBackend',
    'servicetrees.domains.backends.SitePermissionBackend',  # user.has_perm(perm, site)
]

# FileField / ImageField uploads go into the media library (optional).
STORAGES = {
    'default': {'BACKEND': 'servicetrees.mediaapp.storage.MediaLibraryStorage'},
    'staticfiles': {'BACKEND': 'django.contrib.staticfiles.storage.StaticFilesStorage'},
}

ST_DOMAINS_BASE_DOMAIN = 'example.org'
```

Then include the URLs you use, run migrations, and create a `Site` row for the
domain you will serve:

```sh
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
```

For a first integration, begin with `domains`, `displaypage`, and `mediaapp`.
Add the other apps only when the project needs them. Every configurable value
and its default is documented in the relevant app's `SETTINGS.md`.

Context processors: `servicetrees.domains.context_processors.domain_settings`,
`servicetrees.translations.context_processors.translation_overrides`,
`servicetrees.cookie_consent.context_processors.cookie_consent`.

```python
# urls.py: include the apps you installed
from servicetrees.mediaapp import views as media_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('domains/', include('servicetrees.domains.urls')),
    path('displaypage/', include('servicetrees.displaypage.urls')),
    path('api/media/', include('servicetrees.mediaapp.urls')),
    path('links/', include('servicetrees.links.urls')),
    path('search/', include('servicetrees.search.urls')),
    path('translations/', include('servicetrees.translations.urls')),
    path('cookie-consent/', include('servicetrees.cookie_consent.urls')),
    path('profile/', include('servicetrees.userprofiles.urls')),
    path('oidc/', include('servicetrees.openid_connect.urls')),   # with openid_connect
    path('webform/', include('servicetrees.webform.urls')),       # with webform
    # CKEditor uploads into the media library (the editor scripts post to these exact paths)
    path('ckeditor5/image_upload/', media_views.MediaImageUploadView.as_view(), name='ck_editor_5_upload_file'),
    path('ckeditor5/video_upload/', media_views.MediaVideoUploadView.as_view(), name='ckeditor_video_upload'),
]
```

Templates of the editor pages extend `base.html`; the project provides it, with
the blocks `title`, `extra_head`, `extra_css`, `body_class` and `content`. Error
pages use the project's `error.html` if there is one.

## Sections on any model

```python
from servicetrees.displaypage.models import HasSections

class Event(HasSections):
    ...
    def user_can_edit_sections(self, user, site=None):
        return user.has_perm('st_domains.edit_site_content', site)
```

`event.sections` are its sections; the helpers in `servicetrees.displaypage.views`
(`section_add`, `section_reorder`, `section_move`, `section_delete`,
`section_toggle_visibility`) take `owner=event`. Sites register their own
section types with `displaypage.registry.register_section_type`.

## Security notes

- Editor HTML is cleaned on the server when it is saved through the editor
  widget (`servicetrees.ckeditor.sanitize`, nh3). Content written another way,
  such as an import script, is stored as given. `ST_CKEDITOR_SANITIZER` replaces the cleaner.
- Behind reverse proxies, set `ST_TRUSTED_PROXY_COUNT` to the number of proxies
  that append to `X-Forwarded-For`; otherwise `REMOTE_ADDR` is the client IP.
- Each site sees only its own media unless `ST_MEDIAAPP_SITE_SCOPED = False`.
- OpenID Connect links a first login to an existing user by `sub` only, unless
  `ST_OIDC_USERNAME_CLAIMS` names claims your provider's admins control.
- The webform builder and submission views allow staff only unless the project
  passes `can_edit`.
- Serve `MEDIA_ROOT` with `X-Content-Type-Options: nosniff`. A separate media
  domain is safer still.
- The package's scripts send the CSRF token from the page. Put
  `<meta name="csrf-token" content="{{ csrf_token }}">` in the `<head>` of pages
  editors use; without it they fall back to a form's token, then the cookie.

## Editors and roles

A domain user has roles on one site. The default roles, "Edit pages" and
"Manage domain", grant `st_domains.edit_site_content` and `st_domains.manage_site`;
`ST_DOMAINS_ROLES` adds roles or changes what they allow (each role is a group
`site-role:<key>` kept in step after `migrate`).

## Developing from a checkout

Install the repository as an editable dependency, fetch the pinned front-end
assets, and run the host project's checks and tests:

```sh
pip install -e .
python scripts/fetch_vendor.py
python scripts/settings_docs.py
python manage.py check
python manage.py test servicetrees
```

`scripts/settings_docs.py` regenerates the per-app settings references from
their `conf.py` defaults. A wheel build fetches the pinned Font Awesome and
Klaro releases automatically and includes them in the wheel.

## Related packages

The DEBUG-only Django helper for the csstweaks browser extension is maintained
separately as [csstweaks-client](https://gitlab.com/vasudeva-server/css-tweaks-django).

Developed by [Vasudeva Server](https://vasudevaserver.org)
