Your career: deleting an entry that is on a CV leaves a dangling row that makes the CV's page answer 500 #381

Closed
opened 2026-10-01 10:46:17 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, by reading the CV entry model and rendering code and then deleting a career entry in a test client.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at documents/models.py:315-316 (CVItem.__str__), documents/rendering.py:133-145 (build_sections) and documents/rendering.py:310 (the skip in _section_blocks); resume/models.py, resume/views.py, the templates and api/routers/documents.py have not moved.

A CV entry (CVItem) points at its career entry through a generic foreign key, and nothing removes it when that career entry is deleted. The row left behind cannot be printed: its __str__ returns a lazy translation proxy, which str() refuses. From then on the CV's page, the entry's Tailor and Remove pages, and the API's CV detail all answer 500, so the person has no way in the interface to remove the broken row. The PDF still exports, but with an empty heading where the job was.

  • Where:
    • documents/models.py:275-276, CVItem.__str__: return str(self.item) if self.item else _("Missing entry"), where _ is gettext_lazy.
    • resume/models.py:33-52, ResumeItem: the only reverse relation is translations = GenericRelation("resume.Translation"); none to documents.CVItem, and no post_delete receiver for any career model in documents/signals.py or elsewhere.
    • resume/views.py:301-309, ResumeItemDeleteView: a plain DeleteView.
    • templates/documents/cv_detail.html:104 ({{ item }}), templates/documents/cv_item_form.html:11 and templates/partials/confirm_delete.html:11 (blocktranslate with name=object), api/routers/documents.py:43 ("label": str(item)).
    • documents/rendering.py:159-171, build_sections: wraps the None entry in an Entry instead of skipping it; _section_blocks (rendering.py:336) already skips entry.item is None for the text formats.
  • What happens: a person has a job on one or more CV variants and deletes it from Your career. The delete answers 302 and the CVItem row stays. GET /documents/cvs/<id>/, /documents/cv-entries/<id>/edit/ and /documents/cv-entries/<id>/delete/ then raise TypeError: __str__ returned non-string (type __proxy__); GET /api/v1/cvs/<id> builds the same str(item) (so the MCP server's CV tool fails too). render_cv_html produces an Experience section holding <h3 class="role"></h3> and <p class="org"><bdi></bdi></p>, which is what Export PDF sends to an employer.
  • Why it matters: deleting an old job is an ordinary tidy-up, and it breaks every CV that carried it with no way back through the interface short of deleting the whole CV.
  • Fix:
    • Declare cv_entries = GenericRelation("documents.CVItem") on ResumeItem beside translations, so deleting a career entry cascades to its CV rows (a schema-free change).
    • Make CVItem.__str__ return str(_("Missing entry")).
    • Skip entries whose item is None in build_sections, as _section_blocks already does.
    • A data migration that deletes existing CVItem rows whose target no longer exists.

Done when

  • A test in tests/test_documents.py deletes an Experience that is on a CV through resume:item_delete and asserts that the CV has no entries left and that documents:cv_detail answers 200; it fails today with the TypeError.
  • A test asserts that str() of a CVItem whose target is gone returns a str, and that build_sections leaves such an entry out.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, by reading the CV entry model and rendering code and then deleting a career entry in a test client. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `documents/models.py:315-316` (`CVItem.__str__`), `documents/rendering.py:133-145` (`build_sections`) and `documents/rendering.py:310` (the skip in `_section_blocks`); `resume/models.py`, `resume/views.py`, the templates and `api/routers/documents.py` have not moved. A CV entry (`CVItem`) points at its career entry through a generic foreign key, and nothing removes it when that career entry is deleted. The row left behind cannot be printed: its `__str__` returns a lazy translation proxy, which `str()` refuses. From then on the CV's page, the entry's *Tailor* and *Remove* pages, and the API's CV detail all answer 500, so the person has no way in the interface to remove the broken row. The PDF still exports, but with an empty heading where the job was. - **Where:** - `documents/models.py:275-276`, `CVItem.__str__`: `return str(self.item) if self.item else _("Missing entry")`, where `_` is `gettext_lazy`. - `resume/models.py:33-52`, `ResumeItem`: the only reverse relation is `translations = GenericRelation("resume.Translation")`; none to `documents.CVItem`, and no `post_delete` receiver for any career model in `documents/signals.py` or elsewhere. - `resume/views.py:301-309`, `ResumeItemDeleteView`: a plain `DeleteView`. - `templates/documents/cv_detail.html:104` (`{{ item }}`), `templates/documents/cv_item_form.html:11` and `templates/partials/confirm_delete.html:11` (`blocktranslate with name=object`), `api/routers/documents.py:43` (`"label": str(item)`). - `documents/rendering.py:159-171`, `build_sections`: wraps the `None` entry in an `Entry` instead of skipping it; `_section_blocks` (`rendering.py:336`) already skips `entry.item is None` for the text formats. - **What happens:** a person has a job on one or more CV variants and deletes it from *Your career*. The delete answers 302 and the `CVItem` row stays. `GET /documents/cvs/<id>/`, `/documents/cv-entries/<id>/edit/` and `/documents/cv-entries/<id>/delete/` then raise `TypeError: __str__ returned non-string (type __proxy__)`; `GET /api/v1/cvs/<id>` builds the same `str(item)` (so the MCP server's CV tool fails too). `render_cv_html` produces an *Experience* section holding `<h3 class="role"></h3>` and `<p class="org"><bdi></bdi></p>`, which is what *Export PDF* sends to an employer. - **Why it matters:** deleting an old job is an ordinary tidy-up, and it breaks every CV that carried it with no way back through the interface short of deleting the whole CV. - **Fix:** - Declare `cv_entries = GenericRelation("documents.CVItem")` on `ResumeItem` beside `translations`, so deleting a career entry cascades to its CV rows (a schema-free change). - Make `CVItem.__str__` return `str(_("Missing entry"))`. - Skip entries whose item is `None` in `build_sections`, as `_section_blocks` already does. - A data migration that deletes existing `CVItem` rows whose target no longer exists. ## Done when - A test in `tests/test_documents.py` deletes an `Experience` that is on a CV through `resume:item_delete` and asserts that the CV has no entries left and that `documents:cv_detail` answers 200; it fails today with the `TypeError`. - A test asserts that `str()` of a `CVItem` whose target is gone returns a `str`, and that `build_sections` leaves such an entry out.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:46:17 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#381
No description provided.