Skip to content

Commit

Permalink
Add catalog and legacy organization
Browse files Browse the repository at this point in the history
  • Loading branch information
florimondmanca committed Jul 19, 2022
1 parent 6d2c8d9 commit 3c26e97
Show file tree
Hide file tree
Showing 20 changed files with 217 additions and 7 deletions.
2 changes: 2 additions & 0 deletions server/application/auth/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
)
from server.domain.auth.repositories import UserRepository
from server.domain.common.types import ID
from server.domain.organizations.entities import LEGACY_ORGANIZATION_SIRET

from .commands import ChangePassword, CreateUser, DeleteUser
from .passwords import PasswordEncoder, generate_api_token
Expand Down Expand Up @@ -35,6 +36,7 @@ async def create_user(

user = User(
id=id_,
organization_siret=LEGACY_ORGANIZATION_SIRET,
email=email,
password_hash=password_hash,
role=role,
Expand Down
3 changes: 3 additions & 0 deletions server/application/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

from server.domain.auth.entities import UserRole
from server.domain.common.types import ID
from server.domain.organizations.types import Siret


class UserView(BaseModel):
id: ID
organization_siret: Siret
email: str
role: UserRole


class AuthenticatedUserView(BaseModel):
id: ID
organization_siret: Siret
email: str
role: UserRole
api_token: str
2 changes: 2 additions & 0 deletions server/application/catalog_records/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from pydantic import BaseModel

from server.domain.common.types import ID
from server.domain.organizations.types import Siret


class CatalogRecordView(BaseModel):
id: ID
organization_siret: Siret
created_at: dt.datetime
6 changes: 5 additions & 1 deletion server/application/datasets/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from server.domain.datasets.entities import DataFormat, Dataset
from server.domain.datasets.exceptions import DatasetDoesNotExist
from server.domain.datasets.repositories import DatasetRepository
from server.domain.organizations.entities import LEGACY_ORGANIZATION_SIRET
from server.domain.tags.repositories import TagRepository
from server.seedwork.application.messages import MessageBus

Expand All @@ -25,7 +26,10 @@ async def create_dataset(command: CreateDataset, *, id_: ID = None) -> ID:
id_ = repository.make_id()

catalog_record_id = await catalog_record_repository.insert(
CatalogRecord(id=catalog_record_repository.make_id())
CatalogRecord(
id=catalog_record_repository.make_id(),
organization_siret=LEGACY_ORGANIZATION_SIRET,
)
)
catalog_record = await catalog_record_repository.get_by_id(catalog_record_id)
assert catalog_record is not None
Expand Down
1 change: 1 addition & 0 deletions server/config/di.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ async def create_todo(...):
"server.infrastructure.tags.module.TagsModule",
"server.infrastructure.licenses.module.LicensesModule",
"server.infrastructure.organizations.module.OrganizationsModule",
"server.infrastructure.catalogs.module.CatalogsModule",
"server.infrastructure.auth.module.AuthModule",
]

Expand Down
2 changes: 2 additions & 0 deletions server/domain/auth/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from server.seedwork.domain.entities import Entity

from ..common.types import ID
from ..organizations.types import Siret


class UserRole(enum.Enum):
Expand All @@ -12,6 +13,7 @@ class UserRole(enum.Enum):

class User(Entity):
id: ID
organization_siret: Siret
email: str
password_hash: str
role: UserRole
Expand Down
2 changes: 2 additions & 0 deletions server/domain/catalog_records/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

from ..common import datetime as dtutil
from ..common.types import ID
from ..organizations.types import Siret


class CatalogRecord(Entity):
id: ID
organization_siret: Siret
created_at: dt.datetime = Field(default_factory=dtutil.now)
Empty file.
9 changes: 9 additions & 0 deletions server/domain/catalogs/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from server.domain.common.types import ID
from server.seedwork.domain.entities import Entity

from ..organizations.types import Siret


class Catalog(Entity):
id: ID
organization_siret: Siret
6 changes: 6 additions & 0 deletions server/domain/organizations/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@
class Organization(Entity):
name: str
siret: Siret


# A fake SIRET used for the organization that holds legacy users and whose catalog holds
# legacy datasets. "Legacy" means "before the multi-org system powered by DataPass.
# Going forward SIRET numbers will come from DataPass via the user's organization(s).
LEGACY_ORGANIZATION_SIRET = Siret("000 000 000 00000")
18 changes: 16 additions & 2 deletions server/infrastructure/auth/repositories.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import uuid
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional

from sqlalchemy import Column, Enum, String, delete, select
from sqlalchemy import CHAR, Column, Enum, ForeignKey, String, delete, select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import relationship

from server.application.auth.passwords import API_TOKEN_LENGTH
from server.domain.auth.entities import User, UserRole
from server.domain.auth.exceptions import UserDoesNotExist
from server.domain.auth.repositories import UserRepository
from server.domain.common.types import ID
from server.domain.organizations.types import Siret

from ..database import Base, Database

if TYPE_CHECKING:
from ..organizations.models import OrganizationModel


class UserModel(Base):
__tablename__ = "user"

id: uuid.UUID = Column(UUID(as_uuid=True), primary_key=True)
organization_siret: Siret = Column(
CHAR(14), ForeignKey("organization.siret"), nullable=False
)
organization: "OrganizationModel" = relationship(
"OrganizationModel",
back_populates="users",
cascade="delete",
)
email = Column(String, nullable=False, unique=True, index=True)
password_hash = Column(String, nullable=False)
role = Column(Enum(UserRole, name="user_role_enum"), nullable=False)
Expand Down Expand Up @@ -69,6 +82,7 @@ async def insert(self, entity: User) -> ID:
async with self._db.session() as session:
instance = UserModel(
id=entity.id,
organization_siret=entity.organization_siret,
email=entity.email,
password_hash=entity.password_hash,
role=entity.role,
Expand Down
11 changes: 10 additions & 1 deletion server/infrastructure/catalog_records/repositories.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import datetime as dt
from typing import TYPE_CHECKING, Optional

from sqlalchemy import Column, DateTime, ForeignKey, func, select
from sqlalchemy import CHAR, Column, DateTime, ForeignKey, func, select
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import relationship

from server.domain.catalog_records.entities import CatalogRecord
from server.domain.catalog_records.repositories import CatalogRecordRepository
from server.domain.common.types import ID
from server.domain.organizations.types import Siret

from ..database import Base, Database

if TYPE_CHECKING:
from ..catalogs.models import CatalogModel
from ..datasets.models import DatasetModel


class CatalogRecordModel(Base):
__tablename__ = "catalog_record"

id: ID = Column(UUID(as_uuid=True), primary_key=True)
organization_siret: Siret = Column(
CHAR(14), ForeignKey("catalog.organization_siret"), nullable=False
)
catalog: "CatalogModel" = relationship(
"CatalogModel", back_populates="catalog_records"
)
dataset_id: ID = Column(UUID(as_uuid=True), ForeignKey("dataset.id"))
dataset: "DatasetModel" = relationship(
"DatasetModel",
Expand All @@ -33,6 +41,7 @@ class CatalogRecordModel(Base):
def make_entity(instance: CatalogRecordModel) -> CatalogRecord:
return CatalogRecord(
id=instance.id,
organization_siret=instance.organization_siret,
created_at=instance.created_at,
)

Expand Down
Empty file.
37 changes: 37 additions & 0 deletions server/infrastructure/catalogs/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import datetime as dt
from typing import TYPE_CHECKING, List

from sqlalchemy import CHAR, Column, DateTime, ForeignKey, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship

from server.domain.common.types import ID
from server.domain.organizations.types import Siret

from ..database import Base

if TYPE_CHECKING:
from ..catalog_records.repositories import CatalogRecordModel
from ..organizations.models import OrganizationModel


class CatalogModel(Base):
__tablename__ = "catalog"

id: ID = Column(UUID(as_uuid=True), primary_key=True)
created_at: dt.datetime = Column(
DateTime(timezone=True), server_default=func.clock_timestamp(), nullable=False
)
organization_siret: Siret = Column(
CHAR(14), ForeignKey("organization.siret"), nullable=False
)
organization: "OrganizationModel" = relationship(
"OrganizationModel",
back_populates="catalog",
cascade="delete",
)

catalog_records: List["CatalogRecordModel"] = relationship(
"CatalogRecordModel",
back_populates="catalog",
)
7 changes: 7 additions & 0 deletions server/infrastructure/catalogs/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from server.seedwork.application.modules import Module

from . import models # noqa # Trigger SQLAlchemy table discovery.


class CatalogsModule(Module):
pass
20 changes: 20 additions & 0 deletions server/infrastructure/catalogs/transformers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from server.domain.catalogs.entities import Catalog

from .models import CatalogModel


def make_entity(instance: CatalogModel) -> Catalog:
return Catalog(
id=instance.id,
organization_siret=instance.organization_siret,
)


def make_instance(entity: Catalog) -> CatalogModel:
return CatalogModel(
**entity.dict(
exclude={
"created_at", # Managed by DB
}
),
)
17 changes: 17 additions & 0 deletions server/infrastructure/organizations/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
from typing import TYPE_CHECKING, List

from sqlalchemy import CHAR, Column, String
from sqlalchemy.orm import relationship

from server.domain.organizations.types import Siret

from ..database import Base

if TYPE_CHECKING:
from ..auth.repositories import UserModel
from ..catalogs.models import CatalogModel


class OrganizationModel(Base):
__tablename__ = "organization"

siret: Siret = Column(CHAR(14), primary_key=True)
name: str = Column(String(), nullable=False)

catalog: "CatalogModel" = relationship(
"CatalogModel",
back_populates="organization",
uselist=False,
)
users: List["UserModel"] = relationship(
"UserModel",
back_populates="organization",
)
63 changes: 63 additions & 0 deletions server/migrations/versions/cfb6eef87415_add_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""add-catalog
Revision ID: cfb6eef87415
Revises: d9ea6ea6708f
Create Date: 2022-07-19 11:54:40.398243
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "cfb6eef87415"
down_revision = "d9ea6ea6708f"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"catalog",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
server_default=sa.text("uuid_generate_v4()"),
nullable=False,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("clock_timestamp()"),
nullable=False,
),
sa.Column("organization_siret", sa.CHAR(length=14), nullable=False),
sa.ForeignKeyConstraint(
["organization_siret"],
["organization.siret"],
),
sa.PrimaryKeyConstraint("id"),
)

# Create initial organization.
op.execute(
"INSERT INTO organization (siret, name) "
"VALUES ('00000000000000', 'Organisation par défaut');"
)

# Create its catalog.
op.execute("INSERT INTO catalog (organization_siret) VALUES ('00000000000000');")

# Add all users to the initial organization.
op.add_column("user", sa.Column("organization_siret", sa.CHAR(14)))
op.execute("UPDATE \"user\" SET organization_siret = '00000000000000';")
op.alter_column("user", "organization_siret", nullable=False)

# Add all catalog records to the initial catalog.
op.add_column("catalog_record", sa.Column("organization_siret", sa.CHAR(14)))
op.execute("UPDATE catalog_record SET organization_siret = '00000000000000';")
op.alter_column("catalog_record", "organization_siret", nullable=False)


def downgrade():
op.drop_table("catalog")
8 changes: 7 additions & 1 deletion tests/api/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ async def test_create_user(
user = response.json()
id_ = user["id"]
assert isinstance(id_, str)
assert user == {"id": id_, "email": "[email protected]", "role": "USER"}
assert user == {
"id": id_,
"organization_siret": "0" * 14,
"email": "[email protected]",
"role": "USER",
}


@pytest.mark.asyncio
Expand All @@ -106,6 +111,7 @@ async def test_login(client: httpx.AsyncClient, temp_user: TestUser) -> None:
user = response.json()
assert user == {
"id": str(temp_user.id),
"organization_siret": "0" * 14,
"email": temp_user.email,
"role": temp_user.role.value,
"api_token": temp_user.api_token,
Expand Down
Loading

0 comments on commit 3c26e97

Please sign in to comment.