Skip to content

Commit 975a131

Browse files
Michał Pasternakclaude
authored andcommitted
Rename miniblog -> siteblog, django-sites-blog -> django-site-blog
Renames the Python package, app label, URL namespace, template directory, locale path, admin reverse names, and PyPI distribution name in lockstep. Migrations history was collapsed to a single 0001_initial regenerated from scratch — this is the package's first standalone release, so no downstream migration graph needed to be preserved. - AppConfig pins `default_auto_field = BigAutoField` so the package's schema is independent of the consuming project's `DEFAULT_AUTO_FIELD`. - `django-model-utils` pinned to `>=4.5,<5` because `SplitField` in 5.x removed the `no_excerpt_field` opt-out, causing a duplicate `_article_body_excerpt` column when the migration declares it explicitly. README, CHANGELOG, and CLAUDE.md document the reason. - Downstream projects upgrading from `miniblog` need to drop the old tables and `django_migrations` / `django_content_type` rows on their side before installing this version (documented in CHANGELOG). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7d4418f commit 975a131

29 files changed

Lines changed: 226 additions & 128 deletions

CHANGELOG.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- `Article.sites` ManyToManyField to `django.contrib.sites.models.Site`.
1515
Empty M2M means "visible everywhere"; populated M2M restricts visibility.
1616
- `ArticleDetailView` (slug-based, filters by `SITE_ID`).
17-
- `miniblog` URL namespace with `article-detail` route.
17+
- `siteblog` URL namespace with `article-detail` route.
18+
- Single squashed `0001_initial` migration for the `Article` model,
19+
capturing every field (including the `model_utils.SplitField`
20+
excerpt column `_article_body_excerpt`), the `sites` M2M, and the
21+
explicit `objects` / `on_site` managers in one shot. Earlier history
22+
in `iplweb/bpp` had four incremental migrations; collapsing them
23+
here is intentional because this is the package's first standalone
24+
release, so there is no downstream migration graph to preserve.
25+
- `BigAutoField` as the package-pinned `default_auto_field`
26+
(via `SiteblogConfig.default_auto_field`) so the schema stays
27+
consistent regardless of the consuming project's
28+
`DEFAULT_AUTO_FIELD` setting.
1829
- Minimal template scaffolding (`article_detail.html`, `base.html`).
1930
- Admin `filter_horizontal` + `list_filter` for the `sites` field.
2031

32+
### Notes for downstream projects upgrading from `miniblog` (`django-sites-blog`)
33+
34+
This is a breaking rename. A project that already had the old
35+
`miniblog` app installed needs to handle the transition on its own
36+
side, before installing `django-site-blog`:
37+
38+
- Drop the `miniblog_article` and `miniblog_article_sites` tables
39+
(after archiving content if needed).
40+
- `DELETE FROM django_migrations WHERE app = 'miniblog';`
41+
- `DELETE FROM django_content_type WHERE app_label = 'miniblog';`
42+
- Then install `django-site-blog`, set `INSTALLED_APPS = [..., "siteblog"]`,
43+
and run `migrate` so the new `0001_initial` builds the
44+
`siteblog_article` schema.
45+
46+
If preserving article content matters, copy rows from `miniblog_article`
47+
into `siteblog_article` after the new migration runs. The schemas are
48+
compatible field-for-field except for `id` (was `AutoField`, now
49+
`BigAutoField`) — that's safe for `INSERT ... SELECT id, ...`.
50+
2151
### Removed
2252

2353
- Hard dependency on the bpp project (`bpp.models.struktura.Uczelnia`).

CLAUDE.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Package vs. app name mismatch (read this first)
6+
7+
The PyPI distribution is **`django-site-blog`** but the actual Django app — the Python package, the app label, the URL namespace, the migration `app_label`, the template directory, the admin reverse names — is all **`siteblog`**. Examples:
8+
9+
- Source lives at `src/siteblog/`, not `src/django_site_blog/`.
10+
- `INSTALLED_APPS = ["siteblog"]` (see `tests/settings.py:22`, `example/example_project/settings.py:19`).
11+
- URL namespace: `app_name = "siteblog"` (`src/siteblog/urls.py:5`); reverse via `siteblog:article-detail`.
12+
- Admin reverse: `admin:siteblog_article_change` / `admin:siteblog_article_changelist`.
13+
- Migrations live under `src/siteblog/migrations/` with `app_label="siteblog"`.
14+
- `pyproject.toml` ships package data under `siteblog = [...]`.
15+
16+
Do **not** rename `siteblog` to `django_site_blog` casually — it would require renaming migrations, every reverse string, the URL namespace, and a migration that rewrites `app_label`. If a rename is on the table, treat it as a dedicated piece of work.
17+
18+
The package was previously named `miniblog` (PyPI: `django-sites-blog`). The rename happened in 2026-05; the migration files and table names switched from the `miniblog` to the `siteblog` app label as part of that change. Downstream installs that were on the old name need to `UPDATE django_migrations SET app='siteblog' WHERE app='miniblog';`, the same for `django_content_type.app_label`, and rename the `miniblog_article` table to `siteblog_article` before re-running migrations.
19+
20+
## Architecture (the big picture)
21+
22+
Single-model reusable Django app. The whole package is one model (`Article`), one view (`ArticleDetailView`), one admin, two templates, four migrations.
23+
24+
### Sites-scoping model — the headline feature
25+
26+
`Article.sites` is an M2M to `django.contrib.sites.models.Site` with **inverted-default semantics**:
27+
28+
- **Empty M2M ⇒ article is visible on every site.**
29+
- **Non-empty M2M ⇒ article is restricted to the listed sites.**
30+
31+
This is enforced in `ArticleDetailView.get_queryset` (`src/siteblog/views.py:15-20`) with a single OR query:
32+
33+
```python
34+
qs.filter(Q(sites__isnull=True) | Q(sites__id=site_id)).distinct()
35+
```
36+
37+
**`Article.on_site` exists but does NOT implement these semantics.** It is a standard `CurrentSiteManager("sites")` and treats empty-M2M articles as invisible everywhere. Two managers coexist on the model on purpose:
38+
39+
- `Article.objects` — the OR-default manager-less query used by the view.
40+
- `Article.on_site` — pure current-site-only (no empty-equals-all fallback).
41+
42+
If a future change adds a list view, decide which semantics it should follow and document the choice. Don't silently switch the detail view to `on_site` — it would break the empty-M2M default.
43+
44+
### Status, URLs, content
45+
46+
- Status comes from `model_utils.StatusModel` with `Choices(("draft", _("draft")), ("published", _("published")))`. The view filters to `status="published"`; drafts 404 on the public URL.
47+
- `get_absolute_url()` is mode-aware: drafts return the admin change URL, published articles return the public detail URL (`src/siteblog/models.py:52-55`). Keep this dual behaviour if you touch it — tests at `tests/test_models.py:22-37` lock it in.
48+
- `article_body` is a `model_utils.fields.SplitField`. The split marker comes from `settings.SPLIT_MARKER` (default `"<!-- split -->"`), exposed for the admin help text only — actual splitting is `model_utils`' responsibility.
49+
- `article_body.content` is rendered with `|safe` in `templates/siteblog/article_detail.html:15`. Authors enter HTML directly; there is no sanitisation layer.
50+
51+
### Quirks worth knowing
52+
53+
- `Article.__str__` returns the Polish literal `f'Artykuł "{title}" - {status_label}"'` (`src/siteblog/models.py:58`). The status label is translated; the surrounding word is not. Inherited from the upstream `bpp` extraction — `tests/test_models.py:8-18` accepts both Polish and English labels.
54+
- Polish translations ship at `src/siteblog/locale/pl/LC_MESSAGES/`.
55+
- The project descends from `iplweb/bpp @ 85bca5785` (see `CHANGELOG.md`). The hard dependency on `bpp.models.struktura.Uczelnia` and the `post_save` cache-invalidation signal were removed during extraction.
56+
57+
## Common commands
58+
59+
The project is `uv`-managed; CI uses uv exclusively.
60+
61+
```bash
62+
uv sync --all-extras # install dev + test extras
63+
DJANGO_SETTINGS_MODULE=tests.settings uv run pytest # full test suite
64+
uv run pytest tests/test_views.py::test_detail_view_hidden_on_other_site # one test
65+
uv run ruff check . # lint
66+
uv run ruff format --check . # format check (CI uses this)
67+
uv run ruff format . # apply formatting
68+
pre-commit install # one-time, then runs on commit
69+
pre-commit run --all-files # manual full pass
70+
```
71+
72+
Running the example project (a real Django project that depends on the in-tree app):
73+
74+
```bash
75+
cd example
76+
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py migrate
77+
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py createsuperuser
78+
DJANGO_SETTINGS_MODULE=example_project.settings uv run python manage.py runserver
79+
```
80+
81+
There are **two distinct Django settings modules** — don't conflate them:
82+
83+
- `tests.settings` — in-memory sqlite, no CSRF middleware, used by pytest.
84+
- `example_project.settings` — on-disk sqlite at `example/db.sqlite3`, `DEBUG=True`, used by the runnable demo.
85+
86+
## Test layout
87+
88+
- `tests/conftest.py` sets `DJANGO_SETTINGS_MODULE=tests.settings`.
89+
- `tests/urls.py` is the ROOT_URLCONF for tests: admin at `/admin/` + siteblog mounted at root (so detail URLs are `/{slug}/`, see `tests/test_views.py`).
90+
- `tests/settings.py:5` hardcodes `SITE_ID=1`. The sites-scoping tests in `test_views.py` override it per-test via the pytest-django `settings` fixture; do the same when adding scoping tests.
91+
- The default `Site(id=1)` row is created implicitly by the `sites` app's `post_migrate` signal — tests rely on this rather than creating it explicitly.
92+
93+
## Supported Django × Python matrix
94+
95+
CI (`.github/workflows/tests.yml`) tests:
96+
97+
- Django 5.2 LTS × Python 3.10 / 3.11 / 3.12 / 3.13 / 3.14
98+
- Django 6.0 × Python 3.12 / 3.13 / 3.14
99+
100+
`pyproject.toml` requires `django>=5.2`. Pre-commit pins `django-upgrade --target-version 5.2`, so anything that auto-rewrites to Django 5.2-only idioms is expected and fine.
101+
102+
## Migrations
103+
104+
There is exactly one migration: `0001_initial.py`. It was regenerated from scratch at rename time (the four-migration history inherited from `iplweb/bpp` was discarded because this is the package's first standalone release, so there is no downstream migration graph to preserve). `0001_initial` captures every field on `Article`, including the `_article_body_excerpt` column that `model_utils.fields.SplitField` requires, the `sites` M2M, and the explicit `objects` / `on_site` managers in one `CreateModel`.
105+
106+
`SiteblogConfig.default_auto_field = "django.db.models.BigAutoField"` is the **package's** pin, so the schema stays consistent regardless of what the consuming project sets for `DEFAULT_AUTO_FIELD`.
107+
108+
## django-model-utils pin (this matters)
109+
110+
`pyproject.toml` requires `django-model-utils>=4.5,<5`. The 4.x `SplitField.deconstruct()` injects `no_excerpt_field=True` into the migration so `contribute_to_class` skips the dynamic excerpt-column add at apply time — and the migration declares `_article_body_excerpt` explicitly instead. In 5.x that opt-out was removed, so `contribute_to_class` always adds the excerpt column **on top of** the one declared in the migration, producing a `duplicate column name: _article_body_excerpt` SQL error at `migrate`. Until upstream `django-model-utils` provides a working SplitField under 5.x, do not lift the `<5` ceiling without also regenerating `0001_initial` and verifying `manage.py migrate` round-trips on a fresh DB.

README.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# django-sites-blog
1+
# django-site-blog
22

3-
[![Tests](https://github.com/iplweb/django-sites-blog/actions/workflows/tests.yml/badge.svg)](https://github.com/iplweb/django-sites-blog/actions/workflows/tests.yml)
4-
[![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/iplweb/django-sites-blog)
5-
[![Django](https://img.shields.io/badge/django-5.2%20LTS%20%7C%206.0-0c4b33)](https://github.com/iplweb/django-sites-blog)
6-
[![License](https://img.shields.io/github/license/iplweb/django-sites-blog)](LICENSE)
3+
[![Tests](https://github.com/iplweb/django-site-blog/actions/workflows/tests.yml/badge.svg)](https://github.com/iplweb/django-site-blog/actions/workflows/tests.yml)
4+
[![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/iplweb/django-site-blog)
5+
[![Django](https://img.shields.io/badge/django-5.2%20LTS%20%7C%206.0-0c4b33)](https://github.com/iplweb/django-site-blog)
6+
[![License](https://img.shields.io/github/license/iplweb/django-site-blog)](LICENSE)
77

88
Small Django blog / news app with multi-site support
99
(`django.contrib.sites`).
@@ -19,7 +19,7 @@ research portal, project subsites — from a single codebase using
1919
`django.contrib.sites`. Most blog/news apps either ignore that scenario
2020
or expect a separate deployment per site.
2121

22-
`django-sites-blog` keeps one article store and lets editors say, per
22+
`django-site-blog` keeps one article store and lets editors say, per
2323
article, whether it should appear on _every_ site (the default) or be
2424
restricted to a chosen subset. One schema, one query, no middleware
2525
gymnastics.
@@ -51,18 +51,22 @@ gymnastics.
5151
Verified against the CI matrix in
5252
[`.github/workflows/tests.yml`](.github/workflows/tests.yml). Also
5353
requires [django-model-utils](https://pypi.org/project/django-model-utils/)
54-
≥ 4.5 (for `SplitField`, `TimeStampedModel`, `StatusModel`).
54+
`>=4.5,<5` (for `SplitField`, `TimeStampedModel`, `StatusModel`).
55+
The 5.x release of `django-model-utils` changed `SplitField` in a way that
56+
conflicts with explicit `_article_body_excerpt` declarations in migrations
57+
(produces a duplicate-column error at `migrate`); the upper bound is
58+
deliberate until upstream resolves it.
5559

5660
## Installation
5761

5862
```bash
59-
uv add django-sites-blog
63+
uv add django-site-blog
6064
```
6165

6266
or with pip:
6367

6468
```bash
65-
pip install django-sites-blog
69+
pip install django-site-blog
6670
```
6771

6872
Add the app and `django.contrib.sites` to `INSTALLED_APPS`:
@@ -71,7 +75,7 @@ Add the app and `django.contrib.sites` to `INSTALLED_APPS`:
7175
INSTALLED_APPS = [
7276
# ...
7377
"django.contrib.sites",
74-
"miniblog",
78+
"siteblog",
7579
]
7680

7781
SITE_ID = 1 # required by django.contrib.sites
@@ -82,7 +86,7 @@ Include the URLs in your project's `urls.py`:
8286
```python
8387
urlpatterns = [
8488
# ...
85-
path("articles/", include("miniblog.urls")),
89+
path("articles/", include("siteblog.urls")),
8690
]
8791
```
8892

@@ -121,16 +125,16 @@ behaviour), use the `Article.on_site` `CurrentSiteManager` instead.
121125

122126
The package ships two minimal templates:
123127

124-
- `miniblog/article_detail.html` — uses `{% extends "miniblog/base.html" %}`.
125-
- `miniblog/base.html` — a bare HTML skeleton; override it in your
126-
project by placing a `miniblog/base.html` ahead of the package's
128+
- `siteblog/article_detail.html` — uses `{% extends "siteblog/base.html" %}`.
129+
- `siteblog/base.html` — a bare HTML skeleton; override it in your
130+
project by placing a `siteblog/base.html` ahead of the package's
127131
in your `TEMPLATES` `DIRS`.
128132

129133
## Development
130134

131135
```bash
132-
git clone https://github.com/iplweb/django-sites-blog.git
133-
cd django-sites-blog
136+
git clone https://github.com/iplweb/django-site-blog.git
137+
cd django-site-blog
134138
uv sync --all-extras
135139
DJANGO_SETTINGS_MODULE=tests.settings uv run pytest
136140
```

example/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Example project for `django-sites-blog`
1+
# Example project for `django-site-blog`
22

3-
Bare-minimum Django project that installs `miniblog` and exposes its
3+
Bare-minimum Django project that installs `siteblog` and exposes its
44
URLs at `/articles/`.
55

66
```bash

example/example_project/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"django.contrib.sites",
1717
"django.contrib.admin",
1818
"django.contrib.staticfiles",
19-
"miniblog",
19+
"siteblog",
2020
]
2121

2222
MIDDLEWARE = [

example/example_project/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33

44
urlpatterns = [
55
path("admin/", admin.site.urls),
6-
path("articles/", include("miniblog.urls")),
6+
path("articles/", include("siteblog.urls")),
77
]

pyproject.toml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["setuptools>=75.0"]
33
build-backend = "setuptools.build_meta"
44

55
[project]
6-
name = "django-sites-blog"
6+
name = "django-site-blog"
77
version = "0.1.0"
88
description = "Small Django blog/news app with multi-site support (django.contrib.sites)."
99
readme = "README.md"
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
1313
authors = [
1414
{ name = "Michał Pasternak", email = "michal.dtz@gmail.com" },
1515
]
16-
keywords = ["django", "blog", "news", "miniblog", "sites", "multi-site", "cms"]
16+
keywords = ["django", "blog", "news", "siteblog", "sites", "multi-site", "cms"]
1717
classifiers = [
1818
"Development Status :: 3 - Alpha",
1919
"Environment :: Web Environment",
@@ -32,7 +32,7 @@ classifiers = [
3232
]
3333
dependencies = [
3434
"django>=5.2",
35-
"django-model-utils>=4.5",
35+
"django-model-utils>=4.5,<5",
3636
]
3737

3838
[project.optional-dependencies]
@@ -46,16 +46,16 @@ dev = [
4646
]
4747

4848
[project.urls]
49-
Homepage = "https://github.com/iplweb/django-sites-blog"
50-
Repository = "https://github.com/iplweb/django-sites-blog"
51-
Issues = "https://github.com/iplweb/django-sites-blog/issues"
52-
Changelog = "https://github.com/iplweb/django-sites-blog/blob/main/CHANGELOG.md"
49+
Homepage = "https://github.com/iplweb/django-site-blog"
50+
Repository = "https://github.com/iplweb/django-site-blog"
51+
Issues = "https://github.com/iplweb/django-site-blog/issues"
52+
Changelog = "https://github.com/iplweb/django-site-blog/blob/main/CHANGELOG.md"
5353

5454
[tool.setuptools.packages.find]
5555
where = ["src"]
5656

5757
[tool.setuptools.package-data]
58-
miniblog = ["templates/**/*", "locale/**/*"]
58+
siteblog = ["templates/**/*", "locale/**/*"]
5959

6060
[tool.ruff]
6161
target-version = "py310"
@@ -65,7 +65,7 @@ line-length = 88
6565
select = ["E", "F", "W", "I"]
6666

6767
[tool.ruff.lint.per-file-ignores]
68-
"src/miniblog/migrations/*" = ["E501"]
68+
"src/siteblog/migrations/*" = ["E501"]
6969

7070
[tool.pytest.ini_options]
7171
DJANGO_SETTINGS_MODULE = "tests.settings"

src/miniblog/apps.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/miniblog/migrations/0002_auto_20180101_2017.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/miniblog/migrations/0003_alter_article_article_body.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)