Put reminders on the plugin surface, with one service each to create, change, move, complete and reopen them #331

Closed
opened 2026-09-30 20:30:06 +00:00 by tiagoagueda · 0 comments
Owner

A plugin may import only what postulo.plugins.api names, and Writing a plugin tells it never to write by saving a model. Interviews are on the surface with the services that move and settle them. Reminder (src/postulo/applications/models.py: an application or none, summary, due_at, done_at, notified_at) is not, nor is postpone_reminder, so postulo-dav cannot keep reminders in step with a CalDAV task list (postulo/postulo-dav#12), asked for as: "The TODO component (tasks) is what best resembles … a reminder."

What exists today

  • No service exists to make a reminder. Five places create one by hand: the reminder form (ReminderCreateView), Snooze on a quiet application (src/postulo/applications/views.py:865), POST /api/v1/reminders (src/postulo/api/routers/reminders.py:45), and inside schedule_interview, a day ahead (src/postulo/applications/services.py:428), and the offer path, for an answer date (services.py:666).
  • Changing one is a form save in the edit view and field assignments in the API's PATCH, each followed by postpone_reminder (services.py:567) when the time moved, which clears notified_at so it is announced again. reschedule_interview moves its reminder by hand (services.py:468).
  • Completing is reminder.complete(), called from Done, the API, the two interview services and the offer path. Reopening exists only in the offer path, which clears done_at by hand (services.py:677); neither the page nor the API can undo Done, and ReminderPatch deliberately has no done_at.

Proposal

  1. Services in src/postulo/applications/services.py, each taking an actor as record_event and reschedule_interview do:
    • create_reminder(owner, *, summary, due, application=None, actor=""). It refuses another person's application and cuts the summary to the column's 250 characters, as suggest does. A date for due means nine in the morning (REMINDER_HOUR, views.py:678, as New reminder on a calendar day) in the owner's zone (the instance's when they set none), not the active one: a scheduled sync runs outside any request, and only the request middleware activates a zone (src/postulo/core/middleware.py:129).
    • change_reminder(reminder, *, summary=…, application=…, actor=""), with the same refusal and cut.
    • postpone_reminder(reminder, due, *, actor=""), today's service with an actor, taking a date as create_reminder does.
    • complete_reminder(reminder, *, at=None, actor=""): the moment it was done, defaulting to now and never in the future, since a sync learns of a tick up to a day late.
    • reopen_reminder(reminder, *, actor="").
  2. Every caller moves onto them: the create and edit forms, Done, Snooze, the API's add, change and complete, and the interview and offer paths. The importer and the demo seed, which restore or invent rows, stay as they are. The API router's handlers already bear the names complete_reminder and change_reminder (URL names the isolation sweep uses), so it imports the services under others.
  3. On the surface: Reminder, read with Reminder.objects.for_user(user), and the five services, as one group in _ELSEWHERE and __all__ in src/postulo/plugins/api.py.

Decisions

  1. Where the actor goes. Reminder has no actor column; ApplicationEvent has one. (a) Add one, naming who last touched the reminder when it was not the person, shown beside it in words, on the API's ReminderOut, and in the export and import (REMINDER_FIELDS, src/postulo/core/export.py:229; a FORMAT_VERSION bump with its note). (b) Take the actor and only log it. Recommend (a): a reminder ticked off by a phone should say so.
  2. change_status on the surface. Recommend keeping it off. A phone's tick is a person's gesture read by a machine; suggest(..., suggested_status=...) is already on the surface, and accepting it moves the status through change_status with the plugin as the actor (src/postulo/applications/suggestions.py), so the person confirms. Alternative: put it on with an actor, letting a plugin move a status unasked.
  3. Reopening from the page and the API. (a) The service only. (b) Also Not done beside a done reminder, and POST /reminders/{id}/reopen beside /complete. Recommend (b): otherwise a phone can untick what the page cannot.
  4. Does reopening announce it again? Recommend no, unless it is also moved: that is postpone_reminder's rule, and a tick undone by mistake should not ring twice.

Needed by

postulo/postulo-dav#12: reminders both ways with a task list, and a task made on the phone becoming a reminder.

Done when

  • Each service is tested: another owner's application refused, the summary cut, a date landing at nine in the owner's zone outside any request, a future at refused, reopening, a move clearing the stamp.
  • No view, API route or other service creates, changes, completes, moves or reopens a reminder by itself (the importer and the demo seed aside).
  • Reminder and the five services are on api.__all__, each with its line in both of Writing a plugin's tables (tests/test_wiki_surface.py), named in tests/test_plugin_surface.py beside what a sync needs, with api.Reminder the core's own class. The published postulo.plugins.testing check needs no change: it reads imports, not names.
  • With (b) of decision 3: the POST view excused in the page-coverage walk with its reason, as reminder_complete is (tests/test_page_coverage.py); rows for the view and the API call in tests/security/test_isolation_sweep.py; the call under Every call on the wiki's The API (tests/test_wiki_surface.py).
  • New strings (the actor line, Not done) are in the working catalogues: en, fr-fr, pt-pt, and pt-br as drafts.
  • Tracking applications says a reminder can be reopened and shows who changed it.

From the list of DAV plugin improvements of 2026-09-30.

A plugin may import only what `postulo.plugins.api` names, and *Writing a plugin* tells it never to write by saving a model. Interviews are on the surface with the services that move and settle them. `Reminder` (`src/postulo/applications/models.py`: an application or none, `summary`, `due_at`, `done_at`, `notified_at`) is not, nor is `postpone_reminder`, so postulo-dav cannot keep reminders in step with a CalDAV task list (postulo/postulo-dav#12), asked for as: "The TODO component (tasks) is what best resembles … a reminder." ## What exists today - **No service exists to make a reminder.** Five places create one by hand: the reminder form (`ReminderCreateView`), *Snooze* on a quiet application (`src/postulo/applications/views.py:865`), `POST /api/v1/reminders` (`src/postulo/api/routers/reminders.py:45`), and inside `schedule_interview`, a day ahead (`src/postulo/applications/services.py:428`), and the offer path, for an answer date (`services.py:666`). - **Changing** one is a form save in the edit view and field assignments in the API's `PATCH`, each followed by `postpone_reminder` (`services.py:567`) when the time moved, which clears `notified_at` so it is announced again. `reschedule_interview` moves its reminder by hand (`services.py:468`). - **Completing** is `reminder.complete()`, called from *Done*, the API, the two interview services and the offer path. **Reopening** exists only in the offer path, which clears `done_at` by hand (`services.py:677`); neither the page nor the API can undo *Done*, and `ReminderPatch` deliberately has no `done_at`. ## Proposal 1. Services in `src/postulo/applications/services.py`, each taking an `actor` as `record_event` and `reschedule_interview` do: - `create_reminder(owner, *, summary, due, application=None, actor="")`. It refuses another person's application and cuts the summary to the column's 250 characters, as `suggest` does. A date for `due` means nine in the morning (`REMINDER_HOUR`, `views.py:678`, as *New reminder* on a calendar day) in the owner's zone (the instance's when they set none), not the active one: a scheduled sync runs outside any request, and only the request middleware activates a zone (`src/postulo/core/middleware.py:129`). - `change_reminder(reminder, *, summary=…, application=…, actor="")`, with the same refusal and cut. - `postpone_reminder(reminder, due, *, actor="")`, today's service with an actor, taking a date as `create_reminder` does. - `complete_reminder(reminder, *, at=None, actor="")`: the moment it was done, defaulting to now and never in the future, since a sync learns of a tick up to a day late. - `reopen_reminder(reminder, *, actor="")`. 2. Every caller moves onto them: the create and edit forms, *Done*, *Snooze*, the API's add, change and complete, and the interview and offer paths. The importer and the demo seed, which restore or invent rows, stay as they are. The API router's handlers already bear the names `complete_reminder` and `change_reminder` (URL names the isolation sweep uses), so it imports the services under others. 3. On the surface: `Reminder`, read with `Reminder.objects.for_user(user)`, and the five services, as one group in `_ELSEWHERE` and `__all__` in `src/postulo/plugins/api.py`. ## Decisions 1. **Where the actor goes.** `Reminder` has no actor column; `ApplicationEvent` has one. (a) Add one, naming who last touched the reminder when it was not the person, shown beside it in words, on the API's `ReminderOut`, and in the export and import (`REMINDER_FIELDS`, `src/postulo/core/export.py:229`; a `FORMAT_VERSION` bump with its note). (b) Take the actor and only log it. Recommend (a): a reminder ticked off by a phone should say so. 2. **`change_status` on the surface.** Recommend keeping it off. A phone's tick is a person's gesture read by a machine; `suggest(..., suggested_status=...)` is already on the surface, and accepting it moves the status through `change_status` with the plugin as the actor (`src/postulo/applications/suggestions.py`), so the person confirms. Alternative: put it on with an actor, letting a plugin move a status unasked. 3. **Reopening from the page and the API.** (a) The service only. (b) Also *Not done* beside a done reminder, and `POST /reminders/{id}/reopen` beside `/complete`. Recommend (b): otherwise a phone can untick what the page cannot. 4. **Does reopening announce it again?** Recommend no, unless it is also moved: that is `postpone_reminder`'s rule, and a tick undone by mistake should not ring twice. ## Needed by postulo/postulo-dav#12: reminders both ways with a task list, and a task made on the phone becoming a reminder. ## Done when - Each service is tested: another owner's application refused, the summary cut, a date landing at nine in the owner's zone outside any request, a future `at` refused, reopening, a move clearing the stamp. - No view, API route or other service creates, changes, completes, moves or reopens a reminder by itself (the importer and the demo seed aside). - `Reminder` and the five services are on `api.__all__`, each with its line in both of *Writing a plugin*'s tables (`tests/test_wiki_surface.py`), named in `tests/test_plugin_surface.py` beside what a sync needs, with `api.Reminder` the core's own class. The published `postulo.plugins.testing` check needs no change: it reads imports, not names. - With (b) of decision 3: the POST view excused in the page-coverage walk with its reason, as `reminder_complete` is (`tests/test_page_coverage.py`); rows for the view and the API call in `tests/security/test_isolation_sweep.py`; the call under *Every call* on the wiki's *The API* (`tests/test_wiki_surface.py`). - New strings (the actor line, *Not done*) are in the working catalogues: en, fr-fr, pt-pt, and pt-br as drafts. - *Tracking applications* says a reminder can be reopened and shows who changed it. From the list of DAV plugin improvements of 2026-09-30.
tiagoagueda added this to the 0.5.0 milestone 2026-10-25 17:04:56 +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#331
No description provided.