Summary
Introduce contributing labs — organizations identified by ROR (Research Organization Registry) IDs — as a new form of attribution that complements the existing ORCID-based Contributor. Users gain lab affiliation(s) on their profile; those
affiliations seed the contributing labs of the score sets and experiments they create; each dataset keeps its own editable, point-in-time snapshot. v1 mirrors the existing ORCID UX (paste a ROR ID, resolve the org name from the ROR public API). The data model is built
forward-compatible so a future "lab members can view the lab's private datasets" capability is purely additive.
Problem
MaveDB credits people via ORCID (the Contributor model), but has no organization-level attribution anywhere — there is no lab, institution, affiliation, or ROR concept in either the API or the UI. As a result:
- A dataset cannot be credited to the lab/group that produced it, only to individually named ORCID contributors.
- There is no notion of a user's institutional affiliation, so nothing to attribute submissions to by default.
- There is no foundation for the maintainers' stated goal of letting a lab's members view the lab's private (unpublished) datasets without each being individually named as a contributor.
Today, private-dataset visibility is decided in a single choke-point per entity (the has_permission functions in the score set and experiment permissions modules), which considers only the creator (created_by_id), the ORCID contributors list, and admin/mapper
roles. There is no organizational dimension to hook into.
Proposed behavior
Model shape: forward-compatible lightweight
- A
Lab is a first-class entity identified by a ROR ID, with a cached organization name and location — the organizational twin of the ORCID Contributor.
- A user can be a member of one or more labs ("affiliations"), stored as a real membership relationship keyed on the user's id.
- A score set / experiment carries a
contributing_labs list. This is a snapshot: it is seeded at creation time from the submitter's current affiliations, remains editable (admins may override, e.g. when submitting on someone's behalf), and is not rewritten
when a user later changes their affiliations.
Entry & resolution (mirrors the ORCID flow)
- Users add a lab by pasting a ROR ID (or ROR URL). The ID is format-validated, then the org name/location is resolved from the ROR public API and cached on first sight — exactly analogous to how an ORCID ID is pasted and the contributor name is fetched from ORCID.
- Contributing labs are displayed alongside contributors on dataset detail pages, with a ROR badge/link (org name linking to its ror.org record), paralleling the existing ORCID link.
Explicitly out of scope for v1 (potential upgrade path)
These are intentionally deferred; the v1 schema is designed so each is additive:
- Lab-based private-dataset visibility (the roster goal): later, add a single
user_is_lab_member flag to the input block in the score set and experiment permissions modules and OR it into the read decision (and the private-vs-not denial path). This is confined to
the permissions modules and requires no router changes.
- Roles on lab membership (member vs. admin) via an extra column on the membership association — the same association-object-with-attribute pattern already used elsewhere (e.g. the
primary flag on publication associations).
- Lab detail page and roster/member management UI, including invite/join flows.
- ORCID-employment-derived lab suggestions: read a contributor's ORCID employment records (which increasingly carry ROR IDs) and suggest labs to attach.
- Live ROR typeahead search (type an org name, pick from ROR matches) replacing the paste-a-ROR-ID UX, reusing the existing autocomplete + tag-field pattern already used for publications, genes, and taxonomies.
Acceptance criteria
Implementation notes
Reference patterns to mirror (from the existing ORCID/identifier machinery):
Data model (new Alembic migration under the versions directory, following the existing <12-hex>_<slug>.py filename convention and chained to the current head):
Lab model mirroring the Contributor model: id, ror_id (unique, indexed — store the ROR ID path segment such as 03yrm5c26 and render it as the https://ror.org/… URL), plus cached name and location/country.
- A user↔lab membership association keyed on the user id (a user may belong to multiple labs), exposed as a
labs/affiliations relationship on the User model. Key membership on the user id, not ORCID, to match how ownership is checked and to keep the future
permission flag consistent.
contributing_labs many-to-many joins for score sets and experiments, mirroring the existing scoreset_contributors / experiment_contributors association tables and relationships. Propagate to the parent experiment set via the existing experiment before_insert
listener, the same way contributors are propagated.
ROR resolution & validation (mirror the ORCID lib and the identifier validation):
- A ROR lib module analogous to the ORCID lib:
fetch_ror_org(ror_id) calling the ROR v2 public API (https://api.ror.org/v2/organizations/{id} — public, CC0, no auth, simpler than ORCID's OAuth) and returning the org name + location.
- A
find_or_create_lab(db, ror_id) helper mirroring find_or_create_contributor: look up by ror_id, otherwise fetch and cache; raise a NonexistentRorOrgError (mirroring NonexistentOrcidUserError) when the ROR ID is unknown.
- Validate ROR ID format with
idutils.is_ror (already available) via the existing identifier-validation pattern; add a ROR ID regex on the UI side.
View models (mirror the contributor view models):
- A lab view-model family:
LabBase (ror_id) → LabCreate (with field/model validators via idutils) → SavedLab (adds record_type, name, location) → Lab.
- Add
contributing_labs (optional list of LabCreate) to the score set and experiment create models and the saved-side list of Lab, including the field-deserialization entry that mirrors how contributors are deserialized.
- Add
affiliations to the user view model.
API / routers (mirror the contributor wiring):
- In the score set and experiment routers, resolve
contributing_labs via find_or_create_lab, mirroring the existing find_or_create_contributor call sites. When the create payload omits contributing_labs, default from the current user's affiliations.
- A ROR router exposing
GET /ror/organizations/{ror_id} (mirroring the ORCID user lookup) so the UI can resolve a pasted ROR ID to a name.
- User-router endpoints to get and set the current user's lab affiliations.
UI (mirror the ORCID contributor flow):
- A ROR API client (
lookupRorOrg) mirroring the ORCID client, and a ROR lib module with the ROR ID regex + normalization.
- An
addLab() form helper mirroring addContributor() (regex-validate, dedupe, look up the org name), with a label like "{name} ({rorId})".
- A "Contributing labs" tag-field on the experiment and score set form components, placed next to the Contributors field and seeded from the user's affiliations.
- An account/profile settings field to manage the user's lab affiliations (paste a ROR ID). (The specific account-settings component should be located during implementation — not yet mapped.)
- A "Contributing labs" row on the attribution card and a ROR badge/link component mirroring the ORCID link component (ROR icon + org name + ror.org link). Regenerate the OpenAPI types.
Testing
- API tests are required for every API-touching piece; mock the ROR API the way ORCID is mocked. Cover:
find_or_create_lab (find / create / cache / unknown-id), create-with-labs, seeding-from-affiliations, membership-change-does-not-mutate-existing-datasets,
affiliation get/set, the ROR lookup endpoint, and view-model round-trips. Run under Poetry.
- Verify the migration up/down against a scratch database.
- No automated UI tests (no infrastructure); manually smoke-test the two forms and the detail pages.
Summary
Introduce contributing labs — organizations identified by ROR (Research Organization Registry) IDs — as a new form of attribution that complements the existing ORCID-based
Contributor. Users gain lab affiliation(s) on their profile; thoseaffiliations seed the contributing labs of the score sets and experiments they create; each dataset keeps its own editable, point-in-time snapshot. v1 mirrors the existing ORCID UX (paste a ROR ID, resolve the org name from the ROR public API). The data model is built
forward-compatible so a future "lab members can view the lab's private datasets" capability is purely additive.
Problem
MaveDB credits people via ORCID (the
Contributormodel), but has no organization-level attribution anywhere — there is no lab, institution, affiliation, or ROR concept in either the API or the UI. As a result:Today, private-dataset visibility is decided in a single choke-point per entity (the
has_permissionfunctions in the score set and experiment permissions modules), which considers only the creator (created_by_id), the ORCIDcontributorslist, andadmin/mapperroles. There is no organizational dimension to hook into.
Proposed behavior
Model shape: forward-compatible lightweight
Labis a first-class entity identified by a ROR ID, with a cached organization name and location — the organizational twin of the ORCIDContributor.contributing_labslist. This is a snapshot: it is seeded at creation time from the submitter's current affiliations, remains editable (admins may override, e.g. when submitting on someone's behalf), and is not rewrittenwhen a user later changes their affiliations.
Entry & resolution (mirrors the ORCID flow)
Explicitly out of scope for v1 (potential upgrade path)
These are intentionally deferred; the v1 schema is designed so each is additive:
user_is_lab_memberflag to the input block in the score set and experiment permissions modules and OR it into the read decision (and the private-vs-not denial path). This is confined tothe permissions modules and requires no router changes.
primaryflag on publication associations).Acceptance criteria
labs, a user↔lab membership association, andcontributing_labsjoin tables for both score sets and experiments; Alembicupgradeanddowngraderound-trip.contributing_labson create seeds the dataset from the submitter's current affiliations; an admin can override the seeded value.Implementation notes
Reference patterns to mirror (from the existing ORCID/identifier machinery):
Data model (new Alembic migration under the versions directory, following the existing
<12-hex>_<slug>.pyfilename convention and chained to the current head):Labmodel mirroring theContributormodel:id,ror_id(unique, indexed — store the ROR ID path segment such as03yrm5c26and render it as thehttps://ror.org/…URL), plus cachednameandlocation/country.labs/affiliationsrelationship on theUsermodel. Key membership on the user id, not ORCID, to match how ownership is checked and to keep the futurepermission flag consistent.
contributing_labsmany-to-many joins for score sets and experiments, mirroring the existingscoreset_contributors/experiment_contributorsassociation tables and relationships. Propagate to the parent experiment set via the existing experimentbefore_insertlistener, the same way contributors are propagated.
ROR resolution & validation (mirror the ORCID lib and the identifier validation):
fetch_ror_org(ror_id)calling the ROR v2 public API (https://api.ror.org/v2/organizations/{id}— public, CC0, no auth, simpler than ORCID's OAuth) and returning the org name + location.find_or_create_lab(db, ror_id)helper mirroringfind_or_create_contributor: look up byror_id, otherwise fetch and cache; raise aNonexistentRorOrgError(mirroringNonexistentOrcidUserError) when the ROR ID is unknown.idutils.is_ror(already available) via the existing identifier-validation pattern; add a ROR ID regex on the UI side.View models (mirror the contributor view models):
LabBase(ror_id) →LabCreate(with field/model validators viaidutils) →SavedLab(addsrecord_type,name,location) →Lab.contributing_labs(optional list ofLabCreate) to the score set and experiment create models and the saved-side list ofLab, including the field-deserialization entry that mirrors how contributors are deserialized.affiliationsto the user view model.API / routers (mirror the contributor wiring):
contributing_labsviafind_or_create_lab, mirroring the existingfind_or_create_contributorcall sites. When the create payload omitscontributing_labs, default from the current user's affiliations.GET /ror/organizations/{ror_id}(mirroring the ORCID user lookup) so the UI can resolve a pasted ROR ID to a name.UI (mirror the ORCID contributor flow):
lookupRorOrg) mirroring the ORCID client, and a ROR lib module with the ROR ID regex + normalization.addLab()form helper mirroringaddContributor()(regex-validate, dedupe, look up the org name), with a label like"{name} ({rorId})".Testing
find_or_create_lab(find / create / cache / unknown-id), create-with-labs, seeding-from-affiliations, membership-change-does-not-mutate-existing-datasets,affiliation get/set, the ROR lookup endpoint, and view-model round-trips. Run under Poetry.