after running opencode /init

This commit is contained in:
2026-09-30 17:12:54 +02:00
parent f4caf01361
commit 66c2ab944a
7 changed files with 159 additions and 29 deletions
+146
View File
@@ -0,0 +1,146 @@
# AGENTS.md
Personal blog app: legacy **Pyramid 2.0 WSGI** + **SQLAlchemy 1.4** + **SQLite**, server-rendered
**Jinja2**, Bootstrap 3.4.1 from CDN. UI, templates, routes and DB status values are all in **French** —
match that when adding user-facing text or filtering by post status.
## Commands
```bash
pip install -e ".[testing]" # deps come from setup.py install_requires; NO requirements.txt/lockfile
initialize_ctp_blogr_db development.ini # inserts the 'admin' user (see warning below)
pserve development.ini # waitress on http://localhost:9280, debugtoolbar on, template reload on
pserve production.ini # binds *:9280, debugtoolbar off, log level WARN
pshell development.ini # ctp_blogr/pshell.py already calls request.tm.begin() for you
pytest # pytest.ini: testpaths=ctp_blogr, python_files=test*.py
pytest ctp_blogr/tests.py::TestMyViewSuccessCondition::test_passing_view # single test
alembic -c development.ini upgrade head # there is NO alembic.ini — the [alembic] section lives in the .ini files
```
**`pytest` is not currently installed in the venv** (`python -m pytest` → "No module named pytest"). Run
`pip install -e ".[testing]"` first, or there is no automated check at all.
**No linter, formatter, typechecker, pre-commit, Makefile, or CI config exists.** Do not invent a
`lint`/`typecheck` step or run `make`. `pytest` is the only automated check.
## Stack: trust the installed env, not the metadata
Actually installed and in use: Python 3.9.6, Pyramid 2.0.2, SQLAlchemy 1.4.54, WTForms 3.2.1,
WebHelpers2 2.1, Markdown 3.9, pyramid_jinja2 2.10.1, passlib 1.7.4, zope.sqlalchemy 2.0.
`setup.py` pins only `SQLAlchemy==1.4.54` and `zope.sqlalchemy==2.0`; everything else floats.
`pyramid_layout` is **no longer** in `install_requires` (removed in the working tree) and is not imported
anywhere, though it is still present in the shared venv for sibling projects.
The package was renamed twice (`pyramid_blogr` → `cao_blogr` → `ctp_blogr`), so stale names survive in
metadata: `.coveragerc` says `source = pyramid_blogr`, `rtd.txt` says `pylons-sphinx-themes`,
`CHANGES.txt` says 0.1 while `setup.py` says 1.0. (`README.md` and `MANIFEST.in` were corrected in the
working tree — don't "fix" them back.)
## Architecture
```
ctp_blogr/__init__.py main() — Configurator, root_factory=security.RootFactory,
config.include('pyramid_jinja2') then '.models' then '.routes' then config.scan()
ctp_blogr/models/ SQLAlchemy models + session/engine factories. includeme() hooks
pyramid_tm (explicit_manager) and pyramid_retry, and registers
`request.dbsession` (reified, transaction-managed)
ctp_blogr/services/ all DB queries live here (BlogRecordService, UserService)
ctp_blogr/views/ scanned by config.scan(); default.py, blog.py, notfound.py
ctp_blogr/forms.py WTForms; still holds dead portfolio forms (HistoForm, AllocationForm, ActifForm…)
ctp_blogr/templates/ Jinja2; static/blog/*.png is image content written into post bodies
```
Write path: view → service → `request.dbsession`. **Never call `session.commit()`** — `pyramid_tm`
commits or aborts at the end of the request. Pass `request` into services; they read `request.dbsession`
and `request.authenticated_userid` themselves.
Renderer paths are relative to the *calling package*, so `views/*.py` use `'../templates/x.jinja2'`.
One outlier remains: `views/default.py::user_edit` uses the asset spec `'ctp_blogr:templates/user_edit.jinja2'`.
Both styles work — match the file you're editing.
## Schema & migrations — the biggest trap
`alembic upgrade head` **does not create the blog schema.** Verified on an empty DB: the only migration
(`ctp_blogr/alembic/versions/20230129_b8f8216d72c7.py`) creates a single portfolio-era `allocation` table.
The real `users` / `entries` / `tags` tables exist *only* inside the committed `ctp_blogr.sqlite`, created ad
hoc. (`actifs`, `histo` and `allocation` are also in that file, hand-made; `alembic_version` is pinned at
`b8f8216d72c7`.)
- **Never delete or recreate `ctp_blogr.sqlite`** — it is the only source of the schema and of all content.
- To add a table/column: add the model, then either write a new hand-authored migration or mirror
`ctp_blogr/tests.py::BaseTest.init_database` (`Base.metadata.create_all(engine)`).
- `scripts/initialize_db.py` only inserts the `admin` user; it does **not** create tables (its
"run alembic" hint is misleading).
**`ctp_blogr.sqlite` is git-tracked even though `.gitignore` lists `*.sqlite`.** Any local run dirties
`git status`. Don't stage it in an unrelated commit.
## Auth model
Username-based, no roles column. `services/user.py::groupfinder` grants `group:administrators` to the
literal username `admin` and `[]` to everyone else. `security.py::RootFactory` allows `view` to
`Authenticated` and `manage` to `group:administrators`; `DENY_ALL` catches the rest.
Permissions: `users` → `manage` (admin only); `blog_edit`, `user_edit`, `tags`, `tag_edit` → `view`.
Passwords: `passlib.apps.custom_app_context`. `verify_password` transparently upgrades a matching
cleartext value to a hash, so `User.password` can legitimately contain a plaintext legacy value.
`set_password()` takes **bytes** — callers must `.encode('utf8')`.
`initialize_ctp_blogr_db` writes the admin password as **plaintext** (`scripts/initialize_db.py`), relying on
that upgrade path. It is not idempotent — a second run violates the `users.name` unique constraint.
## Known-broken / surprising behavior — don't mistake these for your regression
Verified by running `pserve` in both dev and prod on 2026-09-30:
- **Every logged-in page returns HTTP 500.** Uncommitted `routes.py` deleted the `portfolio` route, but
`templates/layout.jinja2:50` still calls `request.route_url('portfolio')` in the logged-in dropdown →
`KeyError: 'No such route named portfolio'` during render. Anonymous pages are fine. This is the
regression in progress in the working tree — fix the template or restore the route.
- **Post id 2 links to a dead `/portfolio`.** `views/blog.py:26` hardcodes `return_url = "/portfolio"` for
`blog_id == '2'`; that URL is now a 404.
- **`/portal` → HTTP 404.** Route and view were removed in the working tree (no template was ever
committed either).
- **Protected pages return HTTP 200 with the login page, not 403.** `login` is double-registered as
`@forbidden_view_config`, so the 403 status is lost when the view returns a plain dict. Do not use HTTP
status codes to test authorization — check the body, or log in.
- **`pyramid_debugtoolbar` squashes exceptions into HTTP 200** in `development.ini`, so status codes there
are unreliable too. Use `production.ini` when you need trustworthy status codes.
- **`production.ini` sets `url_scheme = https` while waitress serves plain HTTP.** Every generated URL and
redirect becomes `https://localhost:9280/`, which breaks a local plain-HTTP run (confirmed: login redirects
to `https://localhost:9280/`). Use `development.ini` for local work.
- **Post id 95 is load-bearing.** `views/default.py::home` does `BlogRecordService.by_id(request, 95)` and
reads `.title` / `.body` unconditionally, so `/` raises if that row is gone or renumbered.
`home.jinja2` also links `blog_edit id=95`. Row 95 currently holds a `privé` Chappatte cartoon whose
`body` is used as an image URL. (`get_last_created` is named for 10 but limits to 15.)
- **Post bodies assume the literal substring `static/`.** `views/blog.py` rewrites `static/` to an absolute
URL before running Markdown, so image references in existing post bodies depend on it.
## Conventions
- Post status values are French strings: `brouillon`, `privé`, `publié`. Anonymity filtering happens in
`services/blog_record.py` (`by_criteria`, `by_tag`, `get_last_created` all restrict to `publié` when
`request.authenticated_userid is None`) — the list on `/` is the only place this is *not* applied.
- `blog_bytag`'s `{retour}` matchdict is a **route name**, fed straight into `request.route_url(retour)`
(`views/blog.py:106`). Templates pass `'home'` / `'blog_search'`, never a path.
- Post titles must not contain `/`: create/edit replaces it with `.` because the title feeds the URL slug
(`webhelpers2.text.urlify`). DB also enforces `title` and `users.name` as unique.
- Views use `id='0'` as a magic sentinel meaning "create new" (`blog_edit`, `user_edit`, `tag_edit`).
- Forms are plain WTForms bound directly to model instances with `form.populate_obj(entry)`. Before that,
handlers delete the bound primary key to avoid clobbering it — `del form.id` in `views/blog.py`,
`del form.name` in `views/default.py::user_edit`. Preserve that.
- Every flash message is French and rendered through `layout.jinja2`'s queue loop.
- `ctp_blogr/tests.py` has only two tests and both assert the same tautology (`apropos` returns
`page_title == 'A propos'`). Treat the suite as a smoke test, not coverage.
## Secrets & repo hygiene
Hardcoded secrets already committed — do not copy this pattern and do not add new ones:
`ctp_blogr/__init__.py` holds the session-factory secret (`SignedCookieSessionFactory`) and the
`AuthTktAuthenticationPolicy` secret inline; `scripts/initialize_db.py` hardcodes the admin password.
The root-level `*.py` files (`gmail_*_purge.py`, `imap-*.py`, `smtp-*.py`, `email_avnes_purge.py`) are
standalone one-off IMAP/SMTP cleanup utilities with **plaintext mail credentials**. They do not import the
app, are not part of the Pyramid package, and are unrelated to any blog work — leave them alone.
+1 -1
View File
@@ -1,2 +1,2 @@
include *.txt *.ini *.cfg *.rst
recursive-include cao_blogr *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
recursive-include ctp_blogr *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
+5 -5
View File
@@ -1,10 +1,10 @@
# README #
**cao_blogr** est une application pour créer un blog simple. Elle est inspirée du tutorial [**pyramid_blogr**](https://docs.pylonsproject.org/projects/pyramid-blogr/en/latest/index.html).
**ctp_blogr** est une application pour créer un blog simple.
## Fonctionnalités ##
Bien que **cao_blogr** soit une application minimale et simple, elle possède toutes les fonctions nécessaire pour gérer un blog :
Bien que **ctp_blogr** soit une application minimale et simple, elle possède toutes les fonctions nécessaire pour gérer un blog :
- Gestion des utilisateurs du blog (admin ou rédacteur)
- Authentification et autorisation des utilisateurs
@@ -15,11 +15,11 @@ Bien que **cao_blogr** soit une application minimale et simple, elle possède to
## Add-on requis ##
- Python 3.7.1
- Pyramid 1.10
- Python 3.9.6
- Pyramid 2.0.2
- SQLite 3.35.5
- pyramid-jinja2 2.7 : view templating
- wtforms 2.2.1 : form library
- wtforms 3.2.1 : form library
- webhelpers2 2.0 : various web building related helpers
- Markdown 3.4.1 :
+1 -9
View File
@@ -12,12 +12,4 @@ def includeme(config):
config.add_route('tag_edit', '/tag_edit/{id}')
config.add_route('users', '/users')
config.add_route('user_edit', '/user_edit/{name}')
# portfolio
config.add_route('actif_edit', '/actif_edit/{no_id}')
config.add_route('actif2_edit', '/actif2_edit/{no_id}')
config.add_route('allocation_edit', '/allocation_edit/{no_cat}')
config.add_route('histo_list', '/histo_list')
config.add_route('histo_edit', '/histo_edit/{no_id}')
config.add_route('portfolio', '/portfolio')
# personal
config.add_route('portal', '/portal')
+6 -6
View File
@@ -6,7 +6,7 @@ from ..forms import BlogCreateForm, BlogUpdateForm, BlogSearchForm, TagForm
import markdown
import datetime #<- will be used to set default dates on models
@view_config(route_name='blog', renderer='ctp_blogr:templates/blog.jinja2')
@view_config(route_name='blog', renderer='../templates/blog.jinja2')
def blog(request):
# get post id from request
blog_id = request.matchdict['id']
@@ -35,7 +35,7 @@ def blog(request):
}
@view_config(route_name='blog_edit', renderer='ctp_blogr:templates/blog_edit.jinja2', permission='view')
@view_config(route_name='blog_edit', renderer='../templates/blog_edit.jinja2', permission='view')
def blog_edit(request):
# get post id from request
blog_id = request.matchdict['id']
@@ -98,7 +98,7 @@ def blog_edit(request):
'blog_id': blog_id,
'entry': entry, }
@view_config(route_name='blog_bytag', renderer='ctp_blogr:templates/blog_bytag.jinja2')
@view_config(route_name='blog_bytag', renderer='../templates/blog_bytag.jinja2')
def blog_bytag(request):
# get tag parameters from request
tag = request.matchdict['tag']
@@ -117,7 +117,7 @@ def blog_bytag(request):
}
@view_config(route_name='blog_search', renderer='ctp_blogr:templates/blog_search.jinja2')
@view_config(route_name='blog_search', renderer='../templates/blog_search.jinja2')
def blog_search(request):
criteria = ''
@@ -139,7 +139,7 @@ def blog_search(request):
'tags': tags,
}
@view_config(route_name='tags', renderer='ctp_blogr:templates/tags.jinja2', permission='view')
@view_config(route_name='tags', renderer='../templates/tags.jinja2', permission='view')
def tags(request):
# get the list of tags
@@ -150,7 +150,7 @@ def tags(request):
'tags': tags,
}
@view_config(route_name='tag_edit', renderer='ctp_blogr:templates/tag_edit.jinja2', permission='view')
@view_config(route_name='tag_edit', renderer='../templates/tag_edit.jinja2', permission='view')
def tag_edit(request):
# get tag parameters from request
tag_id = request.matchdict['id']
-7
View File
@@ -151,10 +151,3 @@ def user_edit(request):
'url_retour': url_retour,
'name': name,
}
@view_config(route_name='portal', renderer='../templates/portal.jinja2')
def portal(request):
return {
'page_title': "A propos",
}
-1
View File
@@ -16,7 +16,6 @@ requires = [
'waitress',
'alembic',
'pyramid_retry',
'pyramid_layout',
'pyramid_tm',
'SQLAlchemy==1.4.54',
'transaction',