From 9e76fb47ff206635b0b1821a128b1f98591dc616 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 29 Jun 2026 19:37:56 +0200 Subject: [PATCH 1/5] Allow users register publications --- ...0260629_191636_a7e0d45dcc7e_orcid_rorid.py | 40 ++++++++++++++ app/db/model.py | 16 ++++++ app/schemas/agent.py | 54 +++++++++++++++++++ app/service/publication.py | 4 +- scripts/export/build_database_archive.sh | 4 +- tests/test_publication.py | 9 +++- 6 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py diff --git a/alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py b/alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py new file mode 100644 index 000000000..db44561b4 --- /dev/null +++ b/alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py @@ -0,0 +1,40 @@ +"""orcid_rorid + +Revision ID: a7e0d45dcc7e +Revises: 92914b98dec8 +Create Date: 2026-06-29 19:16:36.548218 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +from sqlalchemy import Text +import app.db.types + +# revision identifiers, used by Alembic. +revision: str = "a7e0d45dcc7e" +down_revision: Union[str, None] = "92914b98dec8" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("organization", sa.Column("ror_id", sa.String(length=9), nullable=True)) + op.create_index(op.f("ix_organization_ror_id"), "organization", ["ror_id"], unique=True) + op.add_column("person", sa.Column("orcid", sa.String(length=19), nullable=True)) + op.create_index(op.f("ix_person_orcid"), "person", ["orcid"], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_person_orcid"), table_name="person") + op.drop_column("person", "orcid") + op.drop_index(op.f("ix_organization_ror_id"), table_name="organization") + op.drop_column("organization", "ror_id") + # ### end Alembic commands ### diff --git a/app/db/model.py b/app/db/model.py index 01fb0e42e..e6dcd7e2d 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -6,6 +6,7 @@ from pgvector.sqlalchemy import Vector from sqlalchemy import ( BigInteger, + CheckConstraint, DateTime, Enum, ForeignKey, @@ -333,11 +334,17 @@ class Person(Agent): family_name: Mapped[str | None] sub_id: Mapped[uuid.UUID | None] = mapped_column(unique=True, index=True) + orcid: Mapped[str] = mapped_column(String(19), nullable=True, unique=True, index=True) + __mapper_args__ = { # noqa: RUF012 "polymorphic_identity": __tablename__, "polymorphic_load": "selectin", } + __table_args__ = ( + CheckConstraint(r"orcid ~ '^\d{4}-\d{4}-\d{4}-\d{4}$'", name="orcid_format_check"), + ) + class Organization(Agent): __tablename__ = AgentType.organization.value @@ -346,11 +353,20 @@ class Organization(Agent): # what is the difference between name and label here ? alternative_name: Mapped[str] + ror_id: Mapped[str] = mapped_column( + String(9), + nullable=True, + unique=True, + index=True, + ) + __mapper_args__ = { # noqa: RUF012 "polymorphic_identity": __tablename__, "polymorphic_load": "selectin", } + __table_args__ = (CheckConstraint(r"ror_id ~ '^0[a-z0-9]{8}$'", name="ror_format_check"),) + class Consortium(Agent): __tablename__ = AgentType.consortium.value diff --git a/app/schemas/agent.py b/app/schemas/agent.py index ec8401969..e2ddc0e51 100644 --- a/app/schemas/agent.py +++ b/app/schemas/agent.py @@ -1,4 +1,8 @@ +import re import uuid +from typing import Annotated + +from pydantic import AfterValidator from app.db.types import AgentType from app.schemas.base import ( @@ -7,6 +11,50 @@ from app.schemas.identifiable import IdentifiableCreate, IdentifiableRead, NestedIdentifiableRead from app.schemas.utils import make_update_schema +ORCID_REGEX = re.compile(r"^\d{4}-\d{4}-\d{4}-\d{4}$") + + +def validate_orcid(value: str) -> str: + if value is None: + return value + + value = value.strip() + + # normalize URI form + if value.startswith("https://orcid.org/"): + value = value.rsplit("/", 1)[-1] + + if not ORCID_REGEX.match(value): + msg = f"Invalid ORCID: {value}" + raise ValueError(msg) + + return value + + +ORCID = Annotated[str, AfterValidator(validate_orcid)] + +ROR_REGEX = re.compile(r"^0[a-z0-9]{8}$") + + +def validate_ror(value: str) -> str: + if value is None: + return value + + value = value.strip().lower() + + # normalize URL form + if value.startswith("https://ror.org/"): + value = value.rsplit("/", 1)[-1] + + if not ROR_REGEX.match(value): + msg = f"Invalid ROR ID: {value}" + raise ValueError(msg) + + return value + + +ROR = Annotated[str, AfterValidator(validate_ror)] + class PersonBase(Schema): given_name: str | None = None @@ -16,6 +64,7 @@ class PersonBase(Schema): class PersonCreate(PersonBase, IdentifiableCreate): legacy_id: str | None = None + orcid: ORCID | None = None PersonAdminUpdate = make_update_schema( @@ -28,11 +77,13 @@ class PersonCreate(PersonBase, IdentifiableCreate): class NestedPersonRead(PersonBase, NestedIdentifiableRead): type: AgentType sub_id: uuid.UUID | None + orcid: ORCID | None class PersonRead(PersonBase, IdentifiableRead): type: AgentType sub_id: uuid.UUID | None + orcid: ORCID | None class OrganizationBase(Schema): @@ -42,6 +93,7 @@ class OrganizationBase(Schema): class OrganizationCreate(OrganizationBase, IdentifiableCreate): legacy_id: str | None = None + ror_id: ROR | None = None OrganizationAdminUpdate = make_update_schema( @@ -53,6 +105,7 @@ class OrganizationCreate(OrganizationBase, IdentifiableCreate): class NestedOrganizationRead(OrganizationBase, NestedIdentifiableRead): type: AgentType + ror_id: ROR | None class OrganizationRead( @@ -60,6 +113,7 @@ class OrganizationRead( IdentifiableRead, ): type: AgentType + ror_id: ROR | None class ConsortiumBase(Schema): diff --git a/app/service/publication.py b/app/service/publication.py index b5618c24d..550853551 100644 --- a/app/service/publication.py +++ b/app/service/publication.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import aliased, joinedload, raiseload from app.db.model import Person, Publication -from app.dependencies.auth import AdminContextDep +from app.dependencies.auth import AdminContextDep, UserContextDep from app.dependencies.common import ( FacetsDep, PaginationQuery, @@ -62,7 +62,7 @@ def admin_read_one(db: SessionDep, id_: uuid.UUID) -> PublicationRead: def create_one( db: SessionDep, json_model: PublicationCreate, - user_context: AdminContextDep, + user_context: UserContextDep, ) -> PublicationRead: return router_create_one( db=db, diff --git a/scripts/export/build_database_archive.sh b/scripts/export/build_database_archive.sh index 313dc1d16..a8b999d71 100755 --- a/scripts/export/build_database_archive.sh +++ b/scripts/export/build_database_archive.sh @@ -2,7 +2,7 @@ # Automatically generated, do not edit! set -euo pipefail SCRIPT_VERSION="1" -SCRIPT_DB_VERSION="92914b98dec8" +SCRIPT_DB_VERSION="a7e0d45dcc7e" echo "DB dump (version $SCRIPT_VERSION for db version $SCRIPT_DB_VERSION)" @@ -273,7 +273,7 @@ install -m 755 /dev/stdin "$WORK_DIR/load.sh" <<'EOF_LOAD_SCRIPT' # Automatically generated, do not edit! set -euo pipefail SCRIPT_VERSION="1" -SCRIPT_DB_VERSION="92914b98dec8" +SCRIPT_DB_VERSION="a7e0d45dcc7e" echo "DB load (version $SCRIPT_VERSION for db version $SCRIPT_DB_VERSION)" diff --git a/tests/test_publication.py b/tests/test_publication.py index 2fe548cc5..a67e423e6 100644 --- a/tests/test_publication.py +++ b/tests/test_publication.py @@ -44,8 +44,13 @@ def model_id(publication): return publication.id -def test_create_one(client_admin, json_data): - data = assert_request(client_admin.post, url=ROUTE, json=json_data).json() +def test_create_one_admin(clients, json_data): + data = assert_request(clients.admin.post, url=ROUTE, json=json_data).json() + _assert_read_response(data, json_data) + + +def test_create_one_user(clients, json_data): + data = assert_request(clients.user_1.post, url=ROUTE, json=json_data).json() _assert_read_response(data, json_data) From 396f835077b51a1d9394777c80054913ad042834 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 29 Jun 2026 20:21:42 +0200 Subject: [PATCH 2/5] Revert migration --- ...0260629_191636_a7e0d45dcc7e_orcid_rorid.py | 40 ------------------- scripts/export/build_database_archive.sh | 4 +- 2 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py diff --git a/alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py b/alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py deleted file mode 100644 index db44561b4..000000000 --- a/alembic/versions/20260629_191636_a7e0d45dcc7e_orcid_rorid.py +++ /dev/null @@ -1,40 +0,0 @@ -"""orcid_rorid - -Revision ID: a7e0d45dcc7e -Revises: 92914b98dec8 -Create Date: 2026-06-29 19:16:36.548218 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -from sqlalchemy import Text -import app.db.types - -# revision identifiers, used by Alembic. -revision: str = "a7e0d45dcc7e" -down_revision: Union[str, None] = "92914b98dec8" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.add_column("organization", sa.Column("ror_id", sa.String(length=9), nullable=True)) - op.create_index(op.f("ix_organization_ror_id"), "organization", ["ror_id"], unique=True) - op.add_column("person", sa.Column("orcid", sa.String(length=19), nullable=True)) - op.create_index(op.f("ix_person_orcid"), "person", ["orcid"], unique=True) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_person_orcid"), table_name="person") - op.drop_column("person", "orcid") - op.drop_index(op.f("ix_organization_ror_id"), table_name="organization") - op.drop_column("organization", "ror_id") - # ### end Alembic commands ### diff --git a/scripts/export/build_database_archive.sh b/scripts/export/build_database_archive.sh index a8b999d71..313dc1d16 100755 --- a/scripts/export/build_database_archive.sh +++ b/scripts/export/build_database_archive.sh @@ -2,7 +2,7 @@ # Automatically generated, do not edit! set -euo pipefail SCRIPT_VERSION="1" -SCRIPT_DB_VERSION="a7e0d45dcc7e" +SCRIPT_DB_VERSION="92914b98dec8" echo "DB dump (version $SCRIPT_VERSION for db version $SCRIPT_DB_VERSION)" @@ -273,7 +273,7 @@ install -m 755 /dev/stdin "$WORK_DIR/load.sh" <<'EOF_LOAD_SCRIPT' # Automatically generated, do not edit! set -euo pipefail SCRIPT_VERSION="1" -SCRIPT_DB_VERSION="a7e0d45dcc7e" +SCRIPT_DB_VERSION="92914b98dec8" echo "DB load (version $SCRIPT_VERSION for db version $SCRIPT_DB_VERSION)" From e68fcc210c6b6f0238866aa8f4c16a7bfa640a38 Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 29 Jun 2026 20:23:34 +0200 Subject: [PATCH 3/5] Revert unrelated changes --- app/db/model.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/db/model.py b/app/db/model.py index e6dcd7e2d..01fb0e42e 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -6,7 +6,6 @@ from pgvector.sqlalchemy import Vector from sqlalchemy import ( BigInteger, - CheckConstraint, DateTime, Enum, ForeignKey, @@ -334,17 +333,11 @@ class Person(Agent): family_name: Mapped[str | None] sub_id: Mapped[uuid.UUID | None] = mapped_column(unique=True, index=True) - orcid: Mapped[str] = mapped_column(String(19), nullable=True, unique=True, index=True) - __mapper_args__ = { # noqa: RUF012 "polymorphic_identity": __tablename__, "polymorphic_load": "selectin", } - __table_args__ = ( - CheckConstraint(r"orcid ~ '^\d{4}-\d{4}-\d{4}-\d{4}$'", name="orcid_format_check"), - ) - class Organization(Agent): __tablename__ = AgentType.organization.value @@ -353,20 +346,11 @@ class Organization(Agent): # what is the difference between name and label here ? alternative_name: Mapped[str] - ror_id: Mapped[str] = mapped_column( - String(9), - nullable=True, - unique=True, - index=True, - ) - __mapper_args__ = { # noqa: RUF012 "polymorphic_identity": __tablename__, "polymorphic_load": "selectin", } - __table_args__ = (CheckConstraint(r"ror_id ~ '^0[a-z0-9]{8}$'", name="ror_format_check"),) - class Consortium(Agent): __tablename__ = AgentType.consortium.value From f4999bd8effd7387b29680f20ef633f0170c6f7c Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Mon, 29 Jun 2026 20:26:42 +0200 Subject: [PATCH 4/5] Revert unrelated changes --- app/schemas/agent.py | 54 -------------------------------------------- 1 file changed, 54 deletions(-) diff --git a/app/schemas/agent.py b/app/schemas/agent.py index e2ddc0e51..ec8401969 100644 --- a/app/schemas/agent.py +++ b/app/schemas/agent.py @@ -1,8 +1,4 @@ -import re import uuid -from typing import Annotated - -from pydantic import AfterValidator from app.db.types import AgentType from app.schemas.base import ( @@ -11,50 +7,6 @@ from app.schemas.identifiable import IdentifiableCreate, IdentifiableRead, NestedIdentifiableRead from app.schemas.utils import make_update_schema -ORCID_REGEX = re.compile(r"^\d{4}-\d{4}-\d{4}-\d{4}$") - - -def validate_orcid(value: str) -> str: - if value is None: - return value - - value = value.strip() - - # normalize URI form - if value.startswith("https://orcid.org/"): - value = value.rsplit("/", 1)[-1] - - if not ORCID_REGEX.match(value): - msg = f"Invalid ORCID: {value}" - raise ValueError(msg) - - return value - - -ORCID = Annotated[str, AfterValidator(validate_orcid)] - -ROR_REGEX = re.compile(r"^0[a-z0-9]{8}$") - - -def validate_ror(value: str) -> str: - if value is None: - return value - - value = value.strip().lower() - - # normalize URL form - if value.startswith("https://ror.org/"): - value = value.rsplit("/", 1)[-1] - - if not ROR_REGEX.match(value): - msg = f"Invalid ROR ID: {value}" - raise ValueError(msg) - - return value - - -ROR = Annotated[str, AfterValidator(validate_ror)] - class PersonBase(Schema): given_name: str | None = None @@ -64,7 +16,6 @@ class PersonBase(Schema): class PersonCreate(PersonBase, IdentifiableCreate): legacy_id: str | None = None - orcid: ORCID | None = None PersonAdminUpdate = make_update_schema( @@ -77,13 +28,11 @@ class PersonCreate(PersonBase, IdentifiableCreate): class NestedPersonRead(PersonBase, NestedIdentifiableRead): type: AgentType sub_id: uuid.UUID | None - orcid: ORCID | None class PersonRead(PersonBase, IdentifiableRead): type: AgentType sub_id: uuid.UUID | None - orcid: ORCID | None class OrganizationBase(Schema): @@ -93,7 +42,6 @@ class OrganizationBase(Schema): class OrganizationCreate(OrganizationBase, IdentifiableCreate): legacy_id: str | None = None - ror_id: ROR | None = None OrganizationAdminUpdate = make_update_schema( @@ -105,7 +53,6 @@ class OrganizationCreate(OrganizationBase, IdentifiableCreate): class NestedOrganizationRead(OrganizationBase, NestedIdentifiableRead): type: AgentType - ror_id: ROR | None class OrganizationRead( @@ -113,7 +60,6 @@ class OrganizationRead( IdentifiableRead, ): type: AgentType - ror_id: ROR | None class ConsortiumBase(Schema): From 488425ffa4355b69b0c8246e66d883aafb6db33a Mon Sep 17 00:00:00 2001 From: Eleftherios Zisis Date: Tue, 30 Jun 2026 09:51:07 +0200 Subject: [PATCH 5/5] Add link to issue --- app/service/publication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/service/publication.py b/app/service/publication.py index 550853551..b0d84d048 100644 --- a/app/service/publication.py +++ b/app/service/publication.py @@ -62,7 +62,7 @@ def admin_read_one(db: SessionDep, id_: uuid.UUID) -> PublicationRead: def create_one( db: SessionDep, json_model: PublicationCreate, - user_context: UserContextDep, + user_context: UserContextDep, # See: https://github.com/openbraininstitute/obi-one/issues/867 ) -> PublicationRead: return router_create_one( db=db,