Skip to content

Commit

Permalink
🗃️(mork) add user models for multi-service deletion tracking
Browse files Browse the repository at this point in the history
Adding models to give the list of user that needs to be deleted across multiple
services (Ashley, EdX, Brevo, Joanie).
The models enable:
- Storing user information pending deletion
- Tracking deletion status per service
- Managing deletion requests with different reasons (GDPR, user-requested)
  • Loading branch information
wilbrdt committed Nov 12, 2024
1 parent 05c8224 commit 04edf1d
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 0 deletions.
62 changes: 62 additions & 0 deletions src/app/mork/factories/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Factory classes for users."""

import factory

from mork.models.users import (
DeletionReason,
DeletionStatus,
ServiceName,
User,
UserServiceStatus,
)


class UserServiceStatusFactory(factory.alchemy.SQLAlchemyModelFactory):
"""Factory for generating UserServiceStatus instances."""

class Meta:
"""Factory configuration."""

model = UserServiceStatus

user_id = factory.Sequence(lambda n: n + 1)
service_name = ServiceName
status = DeletionStatus.TO_DELETE


class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
"""Factory for generating fake users."""

class Meta:
"""Factory configuration."""

model = User

username = factory.Faker("pystr", max_chars=15)
edx_user_id = factory.Sequence(lambda n: n)
email = factory.Faker("email")
reason = DeletionReason.GDPR

@factory.post_generation
def service_statuses(
self,
create: bool,
extracted: dict[ServiceName, DeletionStatus] | None,
**kwargs,
):
"""Post-generation hook to create UserServiceStatus for all services."""
if not create:
return

service_states = {service: DeletionStatus.TO_DELETE for service in ServiceName}

if isinstance(extracted, dict):
service_states.update(extracted)

for service, status in service_states.items():
UserServiceStatusFactory(
user=self,
service_name=service,
status=status,
**kwargs,
)
1 change: 1 addition & 0 deletions src/app/mork/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Nota bene: be sure to import all models that need to be migrated here
from mork.models import Base
from mork.models.tasks import EmailStatus
from mork.models.users import UserServiceStatus, User

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Create users tables
Revision ID: b61049edae38
Revises: 976e462b45b6
Create Date: 2024-11-08 10:38:01.575241
"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "b61049edae38"
down_revision: Union[str, None] = "976e462b45b6"
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.create_table(
"users",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("username", sa.String(length=254), nullable=False),
sa.Column("edx_user_id", sa.Integer(), nullable=False),
sa.Column("email", sa.String(length=254), nullable=False),
sa.Column(
"reason",
sa.Enum("USER_REQUESTED", "GDPR", name="deletion_reason"),
nullable=False,
),
sa.Column(
"created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("edx_user_id"),
sa.UniqueConstraint("username"),
)
op.create_table(
"user_service_statuses",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column(
"service_name",
sa.Enum("ASHLEY", "EDX", "BREVO", "JOANIE", name="service_name"),
nullable=False,
),
sa.Column(
"status",
sa.Enum("TO_DELETE", "DELETING", "DELETED", name="deletion_status"),
nullable=False,
),
sa.Column(
"created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "service_name", name="uq_record_service"),
)
op.create_index(
"idx_service_status",
"user_service_statuses",
["service_name", "status"],
unique=False,
)
op.create_index(
"idx_user_service_status",
"user_service_statuses",
["user_id", "service_name", "status"],
unique=False,
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("idx_user_service_status", table_name="user_service_statuses")
op.drop_index("idx_service_status", table_name="user_service_statuses")
op.drop_table("user_service_statuses")
op.drop_table("users")
# ### end Alembic commands ###
96 changes: 96 additions & 0 deletions src/app/mork/models/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Mork users models."""

import enum
from typing import List
from uuid import uuid4

from sqlalchemy import (
Enum,
ForeignKey,
Index,
Integer,
String,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship

from mork.models import Base


class ServiceName(str, enum.Enum):
"""Enum for service names."""

ASHLEY = "ashley"
EDX = "edx"
BREVO = "brevo"
JOANIE = "joanie"


class DeletionReason(str, enum.Enum):
"""Enum for the reason of the deletion request."""

USER_REQUESTED = "user_requested"
GDPR = "gdpr"


class DeletionStatus(str, enum.Enum):
"""Enum for deletion statuses."""

TO_DELETE = "to_delete"
DELETED = "deleted"


class UserServiceStatus(Base):
"""Table for storing the user status for a service."""

__tablename__ = "user_service_statuses"
__table_args__ = (
UniqueConstraint("user_id", "service_name", name="uq_record_service"),
Index(
"idx_user_service_status",
"user_id",
"service_name",
"status",
),
Index("idx_service_status", "service_name", "status"),
)

id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
user_id: Mapped[UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
service_name: Mapped[ServiceName] = mapped_column(
Enum(ServiceName, name="service_name"), nullable=False
)
status: Mapped[DeletionStatus] = mapped_column(
Enum(DeletionStatus, name="deletion_status"), nullable=False, default=False
)

user: Mapped["User"] = relationship("User", back_populates="service_statuses")


class User(Base):
"""Table for storing the users to delete."""

__tablename__ = "users"

filtered_attrs = []

id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid4
)
username: Mapped[str] = mapped_column(String(254), unique=True, nullable=False)
edx_user_id: Mapped[int] = mapped_column(Integer(), unique=True)
email: Mapped[str] = mapped_column(String(254), nullable=False)
reason: Mapped[DeletionReason] = mapped_column(
Enum(DeletionReason, name="deletion_reason"), nullable=False
)

service_statuses: Mapped[List[UserServiceStatus]] = relationship(
"UserServiceStatus",
cascade="all, delete-orphan",
back_populates="user",
)

0 comments on commit 04edf1d

Please sign in to comment.