Files
ctp_blogr/AGENTS.md
T
2026-10-05 17:14:43 +02:00

8.4 KiB

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

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.