opencode /init done
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# AGENTS.md
|
||||
|
||||
Single-package Pyramid 2.0 blog app (`ctp_blogr`). Python 3.14, SQLite, Jinja2, WTForms,
|
||||
SQLAlchemy 2.1, `pyramid_tm` transactions. UI strings, comments, and flash messages are
|
||||
**French**; match that when editing views/templates.
|
||||
|
||||
## Environment
|
||||
|
||||
The venv is the **parent** directory (`/Users/phuoc/pyramid2`), not inside the repo.
|
||||
`ctp_blogr` is installed there in editable mode, so run commands from the repo root with
|
||||
the venv's python (already on PATH as `python`). The venv is shared and also carries a
|
||||
stale editable install of a `wiki_test` project whose source is gone — don't `pip install`
|
||||
into it casually, and don't assume the venv contents match this repo's `setup.py`.
|
||||
|
||||
No linter, formatter, type-checker, pre-commit, CI workflow, or task runner exists.
|
||||
Don't invent one; don't claim you ran one.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
python -m pytest # 2 tests, all in ctp_blogr/tests.py
|
||||
python -m pytest ctp_blogr/tests.py::TestMyViewSuccessCondition # single test/class
|
||||
pserve development.ini # waitress on localhost:9280 + debugtoolbar
|
||||
pserve production.ini # binds *:9280, url_scheme https
|
||||
pshell development.ini # preloaded: request, tm, dbsession, models
|
||||
python -c "from pyramid.paster import get_app; get_app('development.ini')" # boot check
|
||||
```
|
||||
|
||||
- `pytest.ini` sets `testpaths = ctp_blogr` and `python_files = test*.py`, so a new test
|
||||
file must be named `test*.py` inside the package to be collected.
|
||||
- **`coverage` is broken**: `.coveragerc` still says `source = pyramid_blogr` (old package
|
||||
name). `coverage run -m pytest` collects nothing. Fix the name before trusting any
|
||||
coverage number; don't report "no coverage" as a finding.
|
||||
- `initialize_ctp_blogr_db` is declared in `setup.py` but `ctp_blogr/scripts/initialize_db.py`
|
||||
does not exist (only `scripts/__init__.py`) — the console script fails with
|
||||
`ModuleNotFoundError`. There is no working DB-init command.
|
||||
|
||||
## Architecture / wiring
|
||||
|
||||
- `ctp_blogr/__init__.py::main` is the PasteDeploy app factory. It hardcodes the session
|
||||
secret and auth-ticket secret inline — rotating them logs everyone out.
|
||||
- Auth: `AuthTktAuthenticationPolicy` + `ACLAuthorizationPolicy` with
|
||||
`root_factory='ctp_blogr.security.RootFactory'`. The whole ACL is 3 lines in `security.py`:
|
||||
`view` → `Authenticated`, `manage` → `group:administrators`, then `DENY_ALL`.
|
||||
- **Admin is not a DB flag.** `groupfinder` (`services/user.py`) hardcodes
|
||||
`userid == 'admin'` → `['group:administrators']`. Any other user gets `[]`, i.e. logged in
|
||||
but no manage rights. Only `/users` requires `manage`.
|
||||
- Views bind to routes via `@view_config(route_name=...)`; routes are declared centrally in
|
||||
`routes.py`. Adding a URL means editing both.
|
||||
- `models/__init__.py` includes `pyramid_tm` + `pyramid_retry` and reifies `request.dbsession`.
|
||||
`settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'` is set there — removing it
|
||||
breaks commit/abort behaviour.
|
||||
- Write path: view → `request.dbsession.add(...)` → implicit commit by `pyramid_tm`. Do **not**
|
||||
call `commit()` yourself.
|
||||
- Query layer lives in `services/` (`BlogRecordService`, `UserService`) — all classmethods
|
||||
taking `request`. Keep DB access out of views.
|
||||
- Read filtering is duplicated per-method: anonymous users only ever see `status == 'publié'`
|
||||
(`by_criteria`, `by_tag`, `get_last_created`). `by_id` does **not** filter — any post is
|
||||
readable by direct URL. Preserve that asymmetry or you change access behaviour.
|
||||
- Post bodies are Markdown; `views/blog.py` runs `markdown.markdown(..., extensions=['footnotes'])`
|
||||
and rewrites `static/` → `<app_url>/static/` so relative image paths resolve.
|
||||
- Renderer strings are inconsistent on purpose-ish: most views use `'../templates/x.jinja2'`
|
||||
(relative to the package), but `default.py::user_edit` uses the asset spec
|
||||
`'ctp_blogr:templates/user_edit.jinja2'`. Both work; don't "normalize" one without
|
||||
checking the jinja2 search path.
|
||||
- `forms.py` contains dead forms from another app (`HistoForm`, `AllocationForm`, `ActifForm`,
|
||||
`Actif2Form`) matching orphaned DB tables `histo`, `allocation`, `actifs`. Ignore them.
|
||||
|
||||
## Database
|
||||
|
||||
- **`ctp_blogr.sqlite` is committed and is the real dev database** (~99 posts, 8 tags, users
|
||||
`admin` and `phuoc`). Both `development.ini` and `production.ini` point at it via
|
||||
`%(here)s`. `.gitignore` lists `*.sqlite`, but the file is already tracked, so the ignore
|
||||
rule does nothing — every write view's `pyramid_tm` commit is a real diff to review.
|
||||
- The DB contains an `alembic_version` table (rev `b8f8216d72c7`) but there is **no
|
||||
`alembic.ini` and no `migrations/` directory in this repo**. Alembic is installed in the
|
||||
venv, yet nothing here is wired to it. So the schema cannot be regenerated.
|
||||
- **The DB file is the source of truth for schema.** Changing a model means: apply the
|
||||
equivalent `ALTER TABLE` to `ctp_blogr.sqlite` by hand, and tell the user migrations are
|
||||
missing rather than pretending `Base.metadata.create_all()` covers them. `create_all()` is
|
||||
only used by the in-memory test fixture (`tests.py::BaseTest.init_database`).
|
||||
- `Tags` is defined in `models/blog_record.py` but is **not** imported in `models/__init__.py`
|
||||
(only `User` and `BlogRecord` are, before `configure_mappers()`). It still works only because
|
||||
importing `blog_record` defines the class as a side effect. Import it explicitly if you touch
|
||||
mapper config.
|
||||
|
||||
## Known-broken (verified, do not re-diagnose)
|
||||
|
||||
- `views/default.py:142` calls bare `delete(request, user.id)` — `delete` is neither imported
|
||||
nor defined. Submitting the delete button on `/user_edit` raises `NameError`. The intended
|
||||
call is `UserService.delete(request, user.id)`.
|
||||
- `views/default.py:18` — `home` hardcodes `BlogRecordService.by_id(request, 95)` and then reads
|
||||
`entry.title` / `entry.body` with no `None` check. The home page raises `AttributeError` on any
|
||||
database where id 95 doesn't exist (e.g. a freshly created one). `home.jinja2:18` hardcodes the
|
||||
same id for the "select another drawing" link.
|
||||
- `services/blog_record.py` uses `query.get(_id)` — legacy SQLAlchemy 1.x API. Works on 2.1.3 but
|
||||
emits `LegacyAPIWarning`; prefer `session.get(Model, id)` when touching that line.
|
||||
- `routes.py:5` — `'/blog/{id:\d+}/{slug}'` is a non-raw string, so Python 3.12+ emits
|
||||
`SyntaxWarning: "\d" is an invalid escape sequence`. Prefix with `r` if you edit it.
|
||||
- `models/meta.py` uses `sqlalchemy.ext.declarative.declarative_base` (`MovedIn20Warning`).
|
||||
- **Unauthorized requests return HTTP 200, not 403** (verified with and without
|
||||
`pyramid_debugtoolbar`). `forbidden_view_config` renders `login.jinja2` and the status stays
|
||||
200. Don't write tests asserting 403, and don't assume 200 means "the page worked" — check
|
||||
for the login form when smoke-testing protected routes (`/users`, `/tags`, `/blog_edit/{id}`,
|
||||
`/tag_edit/{id}`, `/user_edit/{name}`).
|
||||
|
||||
## Testing
|
||||
|
||||
`ctp_blogr/tests.py` has only two tests and they are duplicates that both assert the same
|
||||
success case (`test_failing_view` is a copy-paste of `test_passing_view`). There is no coverage
|
||||
of auth, forms, or the DB. `BaseTest` sets up `sqlite:///:memory:` with
|
||||
`Base.metadata.create_all()` and wraps the session in `transaction.manager`, aborting in
|
||||
teardown. `WebTest` is installed and is the right tool for new view tests — see
|
||||
`pyramid.testing.DummyRequest` usage there for the lighter alternative.
|
||||
|
||||
## Conventions
|
||||
|
||||
- 4-space indent, no type hints, no docstrings on views. Comments are frequently French and
|
||||
slightly garbled English/French mixes (`# interdire le car '/' dans le titre`) — leave them.
|
||||
- `@view_config` renderer paths and view return dicts follow the existing pattern; views return
|
||||
plain dicts consumed by the matching Jinja2 template.
|
||||
- Form submission is detected by string key presence: `if 'form.submitted' in request.params`,
|
||||
plus `'form.update_created'` and `'form.deleted'` for the extra buttons. Buttons must set
|
||||
`name="form.submitted"` etc.
|
||||
- `del form.id` before `form.populate_obj(entry)` is a deliberate guard against overwriting the
|
||||
primary key. Keep it.
|
||||
- `/blog_bytag/{tag}/{retour}` — `retour` is a **route name** (`request.route_url(retour)`), not a
|
||||
URL. Callers pass e.g. `retour='home'`.
|
||||
- One commit, currently on `master`. `main` and `origin/*` point at the same commit, so branch
|
||||
choice is not settled — ask before creating branches or pushing to the self-hosted
|
||||
`https://git.caotek.fr/phuoc/ctp_blogr.git`. Commit only when asked.
|
||||
Reference in New Issue
Block a user