Metadata-Version: 2.4
Name: splent_feature_edition_staff
Version: 0.2.0
License-Expression: LGPL-3.0-or-later
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: COPYING
License-File: COPYING.LESSER
Requires-Dist: splent_framework>=1.20.0
Dynamic: license-file

# splent_feature_edition_staff

The `edition_staff` feature of a [SPLENT](https://splent.io) product line.

A SPLENT feature is a Python package that a product composes with others.
Its models, routes, services, templates and hooks are registered when the
product starts. What this feature provides and requires is listed in the
`[tool.splent.contract]` section of `pyproject.toml`, which
`splent feature:contract --write` keeps in sync with the source.

## What it does

Editorial roles scoped to one edition, in the WordPress manner. A person is
the editor, an author or a contributor of an edition, and only of that
edition; their account keeps the plain role `user`. The site administrator
(role `admin`) is never stored per edition and passes every check.

| Role        | In the events and editions admin of their edition               |
|-------------|----------------------------------------------------------------|
| editor      | adds, edits, publishes and deletes any event, moves one to another edition they hold a role in, edits the edition with its gallery, posters and sponsors, adopts the site-level events dated within it and manages its staff |
| author      | adds events, edits, publishes and deletes the ones they wrote, and moves them to another edition they hold a role in |
| contributor | adds events and edits the ones they wrote, as drafts while the event is in an edition they contribute to, and moves them to another edition they hold a role in |

Moving an event asks for what adding one in the destination asks for, so
nobody but the site administrator moves an event into an edition they hold
no role in. Taking events out of the site level, where an event with no
edition sits, or into it belongs to the site administrator, as do creating,
deleting and flagging an edition as the current one. Whoever saves an event
into an edition they are a contributor of saves it as a draft, also when
they move it there.

Adopting events is the one exception, and it is deliberate. On the editions
screen the editor of an edition may adopt the events with no edition dated
within it, which attaches every one of them to the edition. From then on
they are the edition's events, and its staff manage them like any other.
That is how an editor brings site-level events into their edition.

The edition selector of the events form offers each account the editions it
may add events to, and "No edition" to the site administrator only, so the
form offers no move that the save refuses. This feature answers the
`event-edition-choices` signal of `splent_feature_editions` for that.

The edit form of an edition sends its current edition flag, so a save by
anyone but the site administrator whose flag differs from the edition's own
changes nothing and goes back to the form, with the reason above it, and the
form shows them the flag without letting them change it. The form carries
the flag as it was when it was drawn, so a stale form, drawn before the site
administrator moved the flag, is refused the same way and cannot put the
flag back.

The site shows the flagged edition as current while it is published. When no
published edition is flagged, it shows the published edition that starts
last, and of two that start on the same day the one with the higher number.
So the editor of an edition may not unpublish the flagged edition, and a
save that changes whether the edition is published, its start date or its
number is refused the same way when it would change the edition
`splent_feature_editions` calls current. The enforcer asks that feature what
the current edition would be after the save, which it works out without
saving anything. The form shows the editor the Published box locked where it
decides the current edition, and says why. Since the editor may open the
form and only that save is refused, the save is sent back to it with the
reason rather than answered with a bare 403, and the form shows the edition
as it is stored.

The check and the save are separate statements. The enforcer asks
`splent_feature_editions` what the current edition would be after the save,
and the editions view then saves, with no lock held from one to the other,
so a change another request commits in between goes unseen. Two saves that
each leave the current edition alone when checked can move it together, one
editor publishing a draft that starts a week before the current edition
while the editor of the current edition moves its start date to before the
draft's, for instance. And a save of the flagged edition that lands while
the site administrator flags another one puts the flag back on it, since
every save of a flagged edition clears the other flags. The window is one
request long, and the site administrator sees the outcome on the editions
list and sets the current edition again there.

The editions list does not offer the staff of an edition the buttons that
add an edition, set the current one or delete one, nor the events admin its
"+ New edition" button, since the access policy refuses those screens to
them. On the row of each edition it draws Edit, Photos, Posters, Sponsors
and Adopt events only where the account's role in that edition opens them,
since the list asks the policy about the edition of the row, and the policy
answers as the screen's check does. So the editor of an edition gets them on
its row only, and an author or a contributor on no row.

The events list does the same with Move, Edit and delete, asking the policy
about the event of each row. The editor of an edition gets them on the rows
of its events, an author on the rows of the events they wrote, a
contributor Move and Edit there without delete, and none of them on the
events of another edition or of the site level. The list itself stays open
to all of them, read only where no button shows.

The feature refines `splent_feature_events` with the author of each event
(`Event.created_by`), stamped with the account that adds it. The events
admin saves every column its form sends, so a save whose form names anyone
but the event's author, or for a new event anyone but the account adding it,
gets 403 and changes nothing. Only the site administrator names the author.

The staff are managed at `/admin/staff`, which lists the editions the
account may manage, and at `/admin/editions/<id>/staff`, where an email is
given a role in that edition (the account is created when it does not
exist). The site administrator manages every edition and an editor the ones
they edit; the Edition staff link in the sidebar is shown to exactly them.
Both need an active account, so a deactivated one whose session is still
live gets 403 on these screens and no link.

## Access

Two pieces decide who uses the events and editions admin, and neither of
those features knows about them. Both read the same list of screens,
`STAFF_ENDPOINTS` in `enforcer.py`, which names every screen the staff of an
edition may open and the check each one runs.

The enforcer narrows them. A `before_request` hook runs ahead of the screens
under `events.admin` and `editions.admin` and answers 403 when the account's
role in the edition at hand does not allow the action, such as an author
editing someone else's event, an editor opening another edition, anyone but
the site administrator creating an edition, or an author saving their event
into an edition they hold no role in. A deactivated account whose session is
still live gets 403 there, the site administrator's included. The gallery,
posters and sponsors screens of an edition ask for the same capability as
editing the edition, `manage_edition`, so only its editor opens them. The
enforcer refuses every account that is neither the site administrator nor on
the staff of some edition, a panel account with the `staff` role included,
and it refuses by default: a screen that is not in `STAFF_ENDPOINTS`, such
as one a later version of events or editions adds, stays closed to the staff
of an edition until it is added there with its check.

The access policy lets the staff in. The screens of events and editions ask
for a panel role (`admin` or `staff`), which the staff of an edition do not
have. In `init_feature` this feature registers `staff_of_some_edition` with
the framework's `register_access_policy`, for the endpoints under
`events.admin` and under `editions.admin`. It passes for the site
administrator before reading anything, so the roles let them in even when
the staff table cannot be read. On the screens in `STAFF_ENDPOINTS` it
answers True for an account that holds a role in some edition and False for
one that holds none, so a panel account with the `staff` role and no edition
gets 403 and does not see the Events and Editions links, which the enforcer
would answer with 403 anyway. The edit form of an edition, adopting events,
the gallery, the posters and the sponsors have a check that reads nothing
but the edition, and there a question that names an edition in its
`view_args` gets that check's answer for the account in that edition, so a
button for one edition follows what its screen allows there. Editing,
moving and deleting an event have a check that reads the event, and there a
question that names an event in its `view_args` gets the answer of the check
the enforcer runs on that screen, which reads the form only on the save of
that event, so a button for one event follows what its screen allows for
it. Every other screen under those prefixes is one the enforcer keeps for
the site administrator, and there the policy refuses everyone else, so a
link or a control that asks for one of them, such as the current edition
flag of the editions form, is shown to the site administrator only.

| Account                      | Events and editions admin | Events and Editions links |
|------------------------------|---------------------------|---------------------------|
| site administrator           | every screen              | shown                     |
| editor, author, contributor of an edition | what the role table allows in that edition | shown |
| `staff` role, no edition     | 403                       | hidden                    |
| deactivated account          | 403, or sent to log in when its session has ended | hidden |
| any other account            | 403                       | hidden                    |

Every other panel screen still asks for its own role, so the staff of an
edition reach the events and editions admin and nothing else. With versions
of events and editions that only ask for a login, the policy is never
consulted and the enforcer alone narrows those screens.

## Use it in a product

```bash
splent feature:add splent_feature_edition_staff
```

It needs `splent_feature_editions` 0.2.0 or later, the first version that
sends `event-edition-choices`, has `EditionsService.current_if_saved` and
`edition_form_data`, and asks about the edition of each row of its list,
which the selector, the enforcer and the row buttons use. With an older
version the enforcer logs that editions is too old and refuses every save of
an edition by its staff with 403, since it cannot tell whether the save
moves the current edition. The contract's `requires.features` names a
feature without a version, so the floor is stated here.
`splent_feature_editions` does not depend on this feature.

The buttons of each row of the events list follow the policy from the first
version of `splent_feature_events` after 0.4.0, which asks about the event
of each row. With 0.4.0 the list draws every button, and the enforcer
answers one the account's role does not open with 403.

## Develop

```bash
splent feature:test splent_feature_edition_staff
splent feature:contract splent_feature_edition_staff
```

## Release

```bash
splent feature:release splent_feature_edition_staff --dry-run --bump patch
splent feature:release splent_feature_edition_staff
```

The first command rehearses the release and changes nothing. The second
publishes it to the git forge and to PyPI, and a PyPI version is permanent.
When both features change, release `splent_feature_editions` first, so the
version this feature needs exists when this one is pinned.
