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

# splent_feature_orcid

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

Sign in and sign up with ORCID, and connect an ORCID iD to an account from
its profile. identities runs the sign-in for every provider, and this feature
describes ORCID to it: its endpoints under `ORCID_BASE_URL` (the sandbox in
development), its scope (`ORCID_SCOPE`), and how to read who signed in from
its userinfo endpoint. An ORCID iD always signs in to the account it is
linked to, and never to an existing account because an email matches. A new
account is opened once the person checks the name ORCID gives and types an
address, which ORCID never verifies: where the product confirms addresses, a
link goes to it, and the account is opened when the person follows it and
signs in with the same iD once more (identities' README). It needs `auth`,
`profile` and `identities`, which keeps the linked accounts.

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.

## Settings

| Setting | Default | What it is |
|---|---|---|
| `ORCID_CLIENT_ID` | empty | The client id of the application registered at https://orcid.org/developer-tools |
| `ORCID_CLIENT_SECRET` | empty | Its secret |
| `ORCID_BASE_URL` | `https://orcid.org` | The ORCID the application is registered with. `https://sandbox.orcid.org` for development and tests, whose iDs and credentials only exist there |
| `ORCID_SCOPE` | `/authenticate` | What the sign-in asks ORCID for. `/authenticate` gives the iD and the name, all a sign-in needs. A product that reads or writes records on ORCID asks for more, and registers its application for it |

Without both credentials ORCID sign-in is off: the app logs a warning at
start, and whoever tries is told it is not available. The sign-in, the
token, the userinfo and the link to an iD's record all follow
`ORCID_BASE_URL`. The callback registered with ORCID is
`<OAUTH_REDIRECT_BASE_URL>/orcid/authorize` (identities' setting).

## Where it shows

It fills the slots other features open, by name, so it composes with
whatever the product has and overrides nothing.

| Slot | Opened by | What ORCID draws |
|---|---|---|
| `auth.login.providers` | auth | The ORCID button, under "or continue with" |
| `signup.providers` | signup, when the product has it | The same button, after the sign-up form |
| `profile.edit.sections` | profile | The linked iD, as a link to its record on ORCID, with a disconnect button, or a "Connect ORCID" link |
| `layout.head.css` | the product's app shell | Its stylesheet, `assets/css/orcid.css` |

It takes order 20 in the first three, after GitHub's 10. The button carries
where the reader was heading (`?next=`), and only a path on this site.

ORCID's green is in `assets/css/orcid.css`, never on the elements. The public
shell loads the file from the asset registry (`register_asset`), and the app
shell, which does not render the registry, through `layout.head.css`. A
product that wants another look overrides `.btn.btn-orcid` and the
`orcid-identity__*` classes in a stylesheet of its own.

## A product without signup

A product that does not install `splent_feature_signup` is closed, and its
accounts are opened by an administrator. ORCID follows the rule identities
holds for every provider.

| Visitor | With signup | Without signup |
|---|---|---|
| Signs in with an ORCID iD already linked | Signed in | Signed in |
| Connects ORCID from their profile | Linked | Linked |
| Signs in with an ORCID iD linked to nobody | Asked for an address and the details of a new account | Sent back to the login page with a translated message that asks them to request access from an administrator, or to connect ORCID from their profile if they have an account |

The button goes straight to ORCID either way. An address is asked for
afterwards, by identities, and only when the sign-in opens a new account.

## What it adds to the profile

| What | How | Needs |
|---|---|---|
| `get_orcid()` on profile's `UserProfile`, the linked iD or `None`, which dataset authors and the API read | A refinement declared in `pyproject.toml` (`[tool.splent.refinement]`), applied by the framework when the product starts. It reads the account's ORCID identity in `external_identity` | profile |
| The `orcid` key of `/api/v1/profile/me`, the linked iD or `null` | profile's `register_profile_payload` | profile |
| The name, surname and affiliation ORCID gives, on the form a new account is opened with | `PROFILE_CLAIMS` maps each claim to the profile field it starts. A field no feature asks for is not on the form | academic_profile, for the affiliation |

```json
{"name": "Ada", "surname": "Lovelace", "email": "ada@example.com", "orcid": "0000-0002-1825-0097"}
```

`get_orcid()` is a method and adds no column. It sits beside the other
refiners of `UserProfile`, such as academic_profile, which adds
`affiliation` to the same model, so a product can have both.

`register_profile_payload` is new in the profile released with 0.1.0.
Against an older profile ORCID still starts and leaves the `orcid` key out
with a warning in the log. Only a missing module or function counts as an
older profile. An error inside profile's own code stops the start as it
is, rather than passing for one.

## Which iDs are linked here

`OrcidService`, on the service locator, tells another feature which iDs this
product can vouch for: an iD is linked only once its holder signed in with
it at ORCID. dataset's upload API credits an author's iD only when it is.

| Call | Answers |
|---|---|
| `is_linked(orcid_id)` | Whether the iD, as written or as its URL, is linked to an account here |
| `orcid_of(user_id)` | The account's iD, or `None` |
| `repository.get_by_orcid_id(orcid_id)` | The ORCID identity in `external_identity`, or `None`. What dataset 0.1 calls |

A feature that uses it finds it through the locator and does without it when
ORCID is not installed, so it never imports this feature.

## The orcid table

Earlier versions kept every iD twice: in `external_identity`, which signs
people in, and in an `orcid` table keyed by profile, which `get_orcid()`
read. `orcid0002_into_external_identity` folds the table into
`external_identity` and drops it. Each row becomes the account's ORCID
identity, or completes the one it already has (the iD as its username, the
earlier date). A row that would lose a link or give it to another account
(its profile is gone, the iD is linked to someone else, the account has
another iD) stops the migration before anything changes, and the message
names the rows. The downgrade puts the table back with a row for every
ORCID identity of an account with a profile.

## Translations

The edit-profile row and the messages about ORCID's answers ship with a
Spanish catalog.

## Use it in a product

```bash
splent feature:add splent_feature_orcid
```

## Develop

The tests simulate ORCID (`tests/conftest.py`): the flow's OAuth client
answers the token exchange and `/oauth/userinfo` as each test chooses.

```bash
splent feature:test splent_feature_orcid --with identities --with github
splent feature:contract splent_feature_orcid
```

## Release

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

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.
This version needs the identities that brings the shared flow and
`provider_email_verified`, so the two are released together.
