From 0db453642de77f44994be158a9df3febb17b70b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Wed, 3 Jun 2026 11:44:38 +0200 Subject: [PATCH 1/8] Add university data fields to inferred_user table and enumeration --- pyproject.toml | 2 +- .../8b780aea403a_add_university_data.py | 48 +++++++++++++++++++ welearn_database/data/enumeration.py | 6 +++ welearn_database/data/models/user_related.py | 11 ++++- 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 welearn_database/alembic/versions/8b780aea403a_add_university_data.py diff --git a/pyproject.toml b/pyproject.toml index 0f228f5..f46b529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "welearn-database" -version = "1.4.4" +version = "1.4.5" description = "All stuff related to relationnal database from the WeLearn project" authors = [ {name = "Théo",email = "theo.nardin@cri-paris.org"} diff --git a/welearn_database/alembic/versions/8b780aea403a_add_university_data.py b/welearn_database/alembic/versions/8b780aea403a_add_university_data.py new file mode 100644 index 0000000..899c491 --- /dev/null +++ b/welearn_database/alembic/versions/8b780aea403a_add_university_data.py @@ -0,0 +1,48 @@ +"""add_university_data + +Revision ID: 8b780aea403a +Revises: f1ce0ad2845b +Create Date: 2026-06-03 11:34:02.441435 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "8b780aea403a" +down_revision: Union[str, None] = "f1ce0ad2845b" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "inferred_user", + sa.Column("university_title", sa.String(), nullable=True), + schema="user_related", + ) + op.add_column( + "inferred_user", + sa.Column( + "role", + postgresql.ENUM( + "student", + "teacher", + "staff", + name="university_role", + schema="user_related", + ), + nullable=True, + ), + schema="user_related", + ) + + +def downgrade() -> None: + op.drop_column("inferred_user", "university_title", schema="user_related") + op.drop_column("inferred_user", "role", schema="user_related") + op.execute("DROP TYPE user_related.university_role") diff --git a/welearn_database/data/enumeration.py b/welearn_database/data/enumeration.py index 99b78e1..c13ffc6 100644 --- a/welearn_database/data/enumeration.py +++ b/welearn_database/data/enumeration.py @@ -43,3 +43,9 @@ class ExternalIdType(StrEnum): class FilterType(StrEnum): SDG = auto() SOURCE = auto() + + +class UniversityRole(StrEnum): + STUDENT = auto() + TEACHER = auto() + STAFF = auto() diff --git a/welearn_database/data/models/user_related.py b/welearn_database/data/models/user_related.py index 149a407..e229baf 100644 --- a/welearn_database/data/models/user_related.py +++ b/welearn_database/data/models/user_related.py @@ -5,7 +5,7 @@ from sqlalchemy.dialects.postgresql import ENUM, TIMESTAMP from sqlalchemy.orm import Mapped, mapped_column, relationship -from welearn_database.data.enumeration import DbSchemaEnum, FilterType +from welearn_database.data.enumeration import DbSchemaEnum, FilterType, UniversityRole from welearn_database.data.models.document_related import WeLearnDocument from . import Base @@ -216,6 +216,15 @@ class InferredUser(Base): types.Uuid, primary_key=True, nullable=False, server_default="gen_random_uuid()" ) origin_referrer: Mapped[str | None] + university_title: Mapped[str | None] + role: Mapped[str] = mapped_column( + ENUM( + *(r.value.lower() for r in UniversityRole), + name="university_role", + schema="user_related", + ), + nullable=False, + ) created_at: Mapped[datetime] = mapped_column( TIMESTAMP(timezone=False), nullable=False, From 98c83c35e00b59b9296e6cefe9c8c4ad9a81d608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Wed, 3 Jun 2026 12:02:56 +0200 Subject: [PATCH 2/8] Refactor exceptions and add university role validation in user model --- tests/test_user_related.py | 29 ++++++++++++++++++- .../data/models/document_related.py | 1 - welearn_database/data/models/user_related.py | 10 ++++++- welearn_database/exceptions.py | 23 +++++++++++++-- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/tests/test_user_related.py b/tests/test_user_related.py index 1a02eee..eb1330d 100644 --- a/tests/test_user_related.py +++ b/tests/test_user_related.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import sessionmaker from tests.helpers import handle_schema_with_sqlite -from welearn_database.data.enumeration import FilterType +from welearn_database.data.enumeration import FilterType, UniversityRole from welearn_database.data.models import Base from welearn_database.data.models.user_related import ( APIKeyManagement, @@ -22,6 +22,7 @@ from welearn_database.data.models.user_related import ( UserProfile, ) +from welearn_database.exceptions import EarlyEnumerationVerificationError class TestUserRelatedCRUD(TestCase): @@ -63,6 +64,32 @@ def test_create_and_read_inferred_user(self): ) self.assertIsNotNone(result) + def test_create_and_read_inferred_user_with_university_data(self): + inferred_user = InferredUser( + id=uuid.uuid4(), + origin_referrer="test_ref", + university_title="test_university", + role=UniversityRole.STUDENT.value, + ) + self.session.add(inferred_user) + self.session.commit() + result = ( + self.session.query(InferredUser) + .filter_by(origin_referrer="test_ref") + .first() + ) + self.assertEqual(result.university_title, "test_university") + self.assertEqual(result.role, UniversityRole.STUDENT.value) + + def test_create_and_read_inferred_user_with_invorrect_university_data(self): + with self.assertRaises(EarlyEnumerationVerificationError): + InferredUser( + id=uuid.uuid4(), + origin_referrer="test_ref", + university_title="test_university", + role="incorrect_role", + ) + def test_create_and_read_session(self): inferred_user = InferredUser(id=uuid.uuid4()) self.session.add(inferred_user) diff --git a/welearn_database/data/models/document_related.py b/welearn_database/data/models/document_related.py index d990202..10657be 100644 --- a/welearn_database/data/models/document_related.py +++ b/welearn_database/data/models/document_related.py @@ -11,7 +11,6 @@ LargeBinary, UniqueConstraint, func, - text, types, ) from sqlalchemy.dialects.postgresql import ARRAY, ENUM, TIMESTAMP diff --git a/welearn_database/data/models/user_related.py b/welearn_database/data/models/user_related.py index e229baf..65cbeb1 100644 --- a/welearn_database/data/models/user_related.py +++ b/welearn_database/data/models/user_related.py @@ -3,11 +3,12 @@ from sqlalchemy import ForeignKey, func, types from sqlalchemy.dialects.postgresql import ENUM, TIMESTAMP -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from welearn_database.data.enumeration import DbSchemaEnum, FilterType, UniversityRole from welearn_database.data.models.document_related import WeLearnDocument +from ...exceptions import EarlyEnumerationVerificationError from . import Base schema_name = DbSchemaEnum.USER_RELATED.value @@ -232,6 +233,13 @@ class InferredUser(Base): server_default="NOW()", ) + @validates("role") + def validate_role(self, key, value): + valid_roles = {r.value.lower() for r in UniversityRole} + if value not in valid_roles: + raise EarlyEnumerationVerificationError(f"Invalid role: {value}") + return value + class EndpointRequest(Base): __tablename__ = "endpoint_request" diff --git a/welearn_database/exceptions.py b/welearn_database/exceptions.py index 3d1c8aa..d197295 100644 --- a/welearn_database/exceptions.py +++ b/welearn_database/exceptions.py @@ -1,4 +1,10 @@ -class InvalidURLScheme(Exception): +class WeLearnDatabaseException(Exception): + """ + Base class for all WeLearnDatabase exceptions + """ + + +class InvalidURLScheme(WeLearnDatabaseException): """ Scheme detected in URL is not accepted """ @@ -7,7 +13,7 @@ def __init__(self, msg="URL schema is not accepted", *args): super().__init__(msg, *args) -class InvalidDOI(Exception): +class InvalidDOI(WeLearnDatabaseException): """ Scheme detected in DOI is not accepted """ @@ -16,7 +22,7 @@ def __init__(self, msg="DOI schema is not accepted", *args): super().__init__(msg, *args) -class ContentIsTooShort(Exception): +class ContentIsTooShort(WeLearnDatabaseException): """ The string used as content is too short, it should be at least 25 characters long """ @@ -27,3 +33,14 @@ def __init__( *args, ): super().__init__(msg, *args) + + +class EarlyEnumerationVerificationError(WeLearnDatabaseException): + """ + SQLAlchemy detect an enumeration value that is not in the list of accepted values + """ + + def __init__( + self, msg="Enumeration value is not in the list of accepted values", *args + ): + super().__init__(msg, *args) From c78da428f2a8852faae86fecfb7c2db92d541477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Wed, 3 Jun 2026 12:03:47 +0200 Subject: [PATCH 3/8] Make university_role nullable in user model --- welearn_database/data/models/user_related.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/welearn_database/data/models/user_related.py b/welearn_database/data/models/user_related.py index 65cbeb1..dd7b24e 100644 --- a/welearn_database/data/models/user_related.py +++ b/welearn_database/data/models/user_related.py @@ -224,7 +224,7 @@ class InferredUser(Base): name="university_role", schema="user_related", ), - nullable=False, + nullable=True, ) created_at: Mapped[datetime] = mapped_column( TIMESTAMP(timezone=False), From a6e582830a33a919f95f23b21164913baae70db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Wed, 3 Jun 2026 12:04:11 +0200 Subject: [PATCH 4/8] Bump version to 1.4.5.dev0 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f46b529..1c3df36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "welearn-database" -version = "1.4.5" +version = "1.4.5.dev0" description = "All stuff related to relationnal database from the WeLearn project" authors = [ {name = "Théo",email = "theo.nardin@cri-paris.org"} From 25f8f83ba5653460502f734f41f96a1f55cab041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Wed, 3 Jun 2026 12:14:37 +0200 Subject: [PATCH 5/8] Add university_role enum type to user_related schema --- .../alembic/versions/8b780aea403a_add_university_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/welearn_database/alembic/versions/8b780aea403a_add_university_data.py b/welearn_database/alembic/versions/8b780aea403a_add_university_data.py index 899c491..3596762 100644 --- a/welearn_database/alembic/versions/8b780aea403a_add_university_data.py +++ b/welearn_database/alembic/versions/8b780aea403a_add_university_data.py @@ -20,6 +20,9 @@ def upgrade() -> None: + op.execute(""" + CREATE TYPE user_related.university_role AS ENUM ('student', 'teacher', 'staff') + """) op.add_column( "inferred_user", sa.Column("university_title", sa.String(), nullable=True), From 1ee6ca9a31f09ac0c92ef335b6bb31c491549721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Thu, 4 Jun 2026 11:09:30 +0200 Subject: [PATCH 6/8] Remove university_role ENUM type and update role field to string in inferred_user model --- tests/test_user_related.py | 13 ++----------- .../versions/8b780aea403a_add_university_data.py | 15 ++++----------- welearn_database/data/enumeration.py | 6 ------ welearn_database/data/models/user_related.py | 16 +--------------- 4 files changed, 7 insertions(+), 43 deletions(-) diff --git a/tests/test_user_related.py b/tests/test_user_related.py index eb1330d..883b398 100644 --- a/tests/test_user_related.py +++ b/tests/test_user_related.py @@ -69,7 +69,7 @@ def test_create_and_read_inferred_user_with_university_data(self): id=uuid.uuid4(), origin_referrer="test_ref", university_title="test_university", - role=UniversityRole.STUDENT.value, + role="student", ) self.session.add(inferred_user) self.session.commit() @@ -79,16 +79,7 @@ def test_create_and_read_inferred_user_with_university_data(self): .first() ) self.assertEqual(result.university_title, "test_university") - self.assertEqual(result.role, UniversityRole.STUDENT.value) - - def test_create_and_read_inferred_user_with_invorrect_university_data(self): - with self.assertRaises(EarlyEnumerationVerificationError): - InferredUser( - id=uuid.uuid4(), - origin_referrer="test_ref", - university_title="test_university", - role="incorrect_role", - ) + self.assertEqual(result.role, "student") def test_create_and_read_session(self): inferred_user = InferredUser(id=uuid.uuid4()) diff --git a/welearn_database/alembic/versions/8b780aea403a_add_university_data.py b/welearn_database/alembic/versions/8b780aea403a_add_university_data.py index 3596762..ab5a424 100644 --- a/welearn_database/alembic/versions/8b780aea403a_add_university_data.py +++ b/welearn_database/alembic/versions/8b780aea403a_add_university_data.py @@ -20,9 +20,9 @@ def upgrade() -> None: - op.execute(""" - CREATE TYPE user_related.university_role AS ENUM ('student', 'teacher', 'staff') - """) + # op.execute(""" + # CREATE TYPE user_related.university_role AS ENUM ('student', 'teacher', 'staff') + # """) op.add_column( "inferred_user", sa.Column("university_title", sa.String(), nullable=True), @@ -32,13 +32,7 @@ def upgrade() -> None: "inferred_user", sa.Column( "role", - postgresql.ENUM( - "student", - "teacher", - "staff", - name="university_role", - schema="user_related", - ), + sa.String(), nullable=True, ), schema="user_related", @@ -48,4 +42,3 @@ def upgrade() -> None: def downgrade() -> None: op.drop_column("inferred_user", "university_title", schema="user_related") op.drop_column("inferred_user", "role", schema="user_related") - op.execute("DROP TYPE user_related.university_role") diff --git a/welearn_database/data/enumeration.py b/welearn_database/data/enumeration.py index c13ffc6..99b78e1 100644 --- a/welearn_database/data/enumeration.py +++ b/welearn_database/data/enumeration.py @@ -43,9 +43,3 @@ class ExternalIdType(StrEnum): class FilterType(StrEnum): SDG = auto() SOURCE = auto() - - -class UniversityRole(StrEnum): - STUDENT = auto() - TEACHER = auto() - STAFF = auto() diff --git a/welearn_database/data/models/user_related.py b/welearn_database/data/models/user_related.py index dd7b24e..a4309f8 100644 --- a/welearn_database/data/models/user_related.py +++ b/welearn_database/data/models/user_related.py @@ -218,14 +218,7 @@ class InferredUser(Base): ) origin_referrer: Mapped[str | None] university_title: Mapped[str | None] - role: Mapped[str] = mapped_column( - ENUM( - *(r.value.lower() for r in UniversityRole), - name="university_role", - schema="user_related", - ), - nullable=True, - ) + role: Mapped[str | None] created_at: Mapped[datetime] = mapped_column( TIMESTAMP(timezone=False), nullable=False, @@ -233,13 +226,6 @@ class InferredUser(Base): server_default="NOW()", ) - @validates("role") - def validate_role(self, key, value): - valid_roles = {r.value.lower() for r in UniversityRole} - if value not in valid_roles: - raise EarlyEnumerationVerificationError(f"Invalid role: {value}") - return value - class EndpointRequest(Base): __tablename__ = "endpoint_request" From 20040260bd13c8fa7e949c85c2a0c122be5f4142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Thu, 4 Jun 2026 11:09:51 +0200 Subject: [PATCH 7/8] Bump version to 1.4.5.dev1 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1c3df36..421b1d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "welearn-database" -version = "1.4.5.dev0" +version = "1.4.5.dev1" description = "All stuff related to relationnal database from the WeLearn project" authors = [ {name = "Théo",email = "theo.nardin@cri-paris.org"} From 6e02fbaf3c1f2963d3c450082a05c98550c152fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o?= Date: Thu, 4 Jun 2026 11:12:19 +0200 Subject: [PATCH 8/8] Remove UniversityRole import from user_related and test_user_related --- tests/test_user_related.py | 2 +- welearn_database/data/models/user_related.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_user_related.py b/tests/test_user_related.py index 883b398..79b1869 100644 --- a/tests/test_user_related.py +++ b/tests/test_user_related.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import sessionmaker from tests.helpers import handle_schema_with_sqlite -from welearn_database.data.enumeration import FilterType, UniversityRole +from welearn_database.data.enumeration import FilterType from welearn_database.data.models import Base from welearn_database.data.models.user_related import ( APIKeyManagement, diff --git a/welearn_database/data/models/user_related.py b/welearn_database/data/models/user_related.py index a4309f8..bd358ef 100644 --- a/welearn_database/data/models/user_related.py +++ b/welearn_database/data/models/user_related.py @@ -3,12 +3,11 @@ from sqlalchemy import ForeignKey, func, types from sqlalchemy.dialects.postgresql import ENUM, TIMESTAMP -from sqlalchemy.orm import Mapped, mapped_column, relationship, validates +from sqlalchemy.orm import Mapped, mapped_column, relationship -from welearn_database.data.enumeration import DbSchemaEnum, FilterType, UniversityRole +from welearn_database.data.enumeration import DbSchemaEnum, FilterType from welearn_database.data.models.document_related import WeLearnDocument -from ...exceptions import EarlyEnumerationVerificationError from . import Base schema_name = DbSchemaEnum.USER_RELATED.value