-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🗃️(mork) add user models for multi-service deletion tracking
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
Showing
4 changed files
with
245 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
src/app/mork/migrations/versions/b61049edae38_create_users_tables.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
) |