diff --git a/app/core/auth/cruds_auth.py b/app/core/auth/cruds_auth.py index fa6e97ba15..decffb3ce0 100644 --- a/app/core/auth/cruds_auth.py +++ b/app/core/auth/cruds_auth.py @@ -116,3 +116,31 @@ async def revoke_refresh_token_by_client_and_user_id( ) await db.commit() return None + + +async def delete_refresh_token_by_user_id( + db: AsyncSession, + user_id: str, +) -> None: + """Delete a refresh token from database""" + + await db.execute( + delete(models_auth.RefreshToken).where( + models_auth.RefreshToken.user_id == user_id, + ), + ) + await db.commit() + + +async def delete_authorization_token_by_user_id( + db: AsyncSession, + user_id: str, +) -> None: + """Delete a refresh token from database""" + + await db.execute( + delete(models_auth.AuthorizationCode).where( + models_auth.AuthorizationCode.user_id == user_id, + ), + ) + await db.commit() diff --git a/app/core/auth/endpoints_auth.py b/app/core/auth/endpoints_auth.py index 6373465abe..fa88a1bdc9 100644 --- a/app/core/auth/endpoints_auth.py +++ b/app/core/auth/endpoints_auth.py @@ -21,6 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth import cruds_auth, models_auth, schemas_auth +from app.core.auth.user_deleter_auth import AuthUserDeleter from app.core.users import cruds_users, models_users from app.core.utils.config import Settings from app.core.utils.security import ( @@ -50,6 +51,7 @@ root="auth", tag="Auth", router=router, + user_deleter=AuthUserDeleter(), ) templates = Jinja2Templates(directory="assets/templates") diff --git a/app/core/auth/user_deleter_auth.py b/app/core/auth/user_deleter_auth.py new file mode 100644 index 0000000000..01b7f59f9c --- /dev/null +++ b/app/core/auth/user_deleter_auth.py @@ -0,0 +1,23 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.auth import cruds_auth +from app.types.module_user_deleter import ModuleUserDeleter + + +class AuthUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_auth.delete_authorization_token_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_auth.delete_refresh_token_by_user_id( + db=db, + user_id=user_id, + ) diff --git a/app/core/core_endpoints/endpoints_core.py b/app/core/core_endpoints/endpoints_core.py index 4e460280d9..d81dcb5048 100644 --- a/app/core/core_endpoints/endpoints_core.py +++ b/app/core/core_endpoints/endpoints_core.py @@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.core_endpoints import cruds_core, models_core, schemas_core +from app.core.core_endpoints.user_deleter_core import CoreUserDeleter from app.core.groups.groups_type import AccountType, GroupType from app.core.users import models_users from app.core.utils.config import Settings @@ -26,6 +27,7 @@ root="", tag="Core", router=router, + user_deleter=CoreUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/core/core_endpoints/user_deleter_core.py b/app/core/core_endpoints/user_deleter_core.py new file mode 100644 index 0000000000..5a279a270d --- /dev/null +++ b/app/core/core_endpoints/user_deleter_core.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CoreUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/google_api/endpoints_google_api.py b/app/core/google_api/endpoints_google_api.py index 6d617ecf49..9ac1bae291 100644 --- a/app/core/google_api/endpoints_google_api.py +++ b/app/core/google_api/endpoints_google_api.py @@ -4,6 +4,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.google_api.google_api import GoogleAPI +from app.core.google_api.user_deleter_google_api import ( + GoogleAPIUserDeleter, +) from app.core.utils.config import Settings from app.dependencies import ( get_db, @@ -17,6 +20,7 @@ root="google-api", tag="GoogleAPI", router=router, + user_deleter=GoogleAPIUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/core/google_api/user_deleter_google_api.py b/app/core/google_api/user_deleter_google_api.py new file mode 100644 index 0000000000..e32289f67a --- /dev/null +++ b/app/core/google_api/user_deleter_google_api.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class GoogleAPIUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/groups/cruds_groups.py b/app/core/groups/cruds_groups.py index 9715debe36..ac06a1ae15 100644 --- a/app/core/groups/cruds_groups.py +++ b/app/core/groups/cruds_groups.py @@ -124,3 +124,15 @@ async def update_group( .values(**group_update.model_dump(exclude_none=True)), ) await db.commit() + + +async def delete_membership_by_user_id( + user_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_groups.CoreMembership).where( + models_groups.CoreMembership.user_id == user_id, + ), + ) + await db.commit() diff --git a/app/core/groups/endpoints_groups.py b/app/core/groups/endpoints_groups.py index a30118fe8a..e1ec8d946c 100644 --- a/app/core/groups/endpoints_groups.py +++ b/app/core/groups/endpoints_groups.py @@ -12,6 +12,7 @@ from app.core.groups import cruds_groups, models_groups, schemas_groups from app.core.groups.groups_type import GroupType +from app.core.groups.user_deleter_groups import GroupsUserDeleter from app.core.users import cruds_users from app.dependencies import ( get_db, @@ -27,6 +28,7 @@ root="groups", tag="Groups", router=router, + user_deleter=GroupsUserDeleter(), ) hyperion_security_logger = logging.getLogger("hyperion.security") diff --git a/app/core/groups/user_deleter_groups.py b/app/core/groups/user_deleter_groups.py new file mode 100644 index 0000000000..17db017df2 --- /dev/null +++ b/app/core/groups/user_deleter_groups.py @@ -0,0 +1,19 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.groups import cruds_groups +from app.types.module_user_deleter import ModuleUserDeleter + + +class GroupsUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_groups.delete_membership_by_user_id( + user_id=user_id, + db=db, + ) diff --git a/app/core/memberships/endpoints_memberships.py b/app/core/memberships/endpoints_memberships.py index b1fe46b682..ac9f8d2392 100644 --- a/app/core/memberships/endpoints_memberships.py +++ b/app/core/memberships/endpoints_memberships.py @@ -8,7 +8,10 @@ from app.core.groups import cruds_groups from app.core.groups.groups_type import GroupType from app.core.memberships import cruds_memberships, schemas_memberships -from app.core.memberships.utils_memberships import validate_user_new_membership +from app.core.memberships.user_deleter_memberships import ( + MembershipsUserDeleter, +) +from app.core.memberships.utils_memberships import validate_user_membership from app.core.users import cruds_users, models_users, schemas_users from app.dependencies import ( get_db, @@ -25,6 +28,7 @@ root="memberships", tag="Memberships", router=router, + user_deleter=MembershipsUserDeleter(), ) diff --git a/app/core/memberships/user_deleter_memberships.py b/app/core/memberships/user_deleter_memberships.py new file mode 100644 index 0000000000..e70136adb2 --- /dev/null +++ b/app/core/memberships/user_deleter_memberships.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class MembershipsUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass # We keep the memberships for stats and history purposes diff --git a/app/core/notification/cruds_notification.py b/app/core/notification/cruds_notification.py index c0f39d1201..8a5da9378e 100644 --- a/app/core/notification/cruds_notification.py +++ b/app/core/notification/cruds_notification.py @@ -217,3 +217,39 @@ async def get_firebase_tokens_by_user_ids( ), ) return list(result.scalars().all()) + + +async def delete_topic_membership_by_user_id( + user_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_notification.TopicMembership).where( + models_notification.TopicMembership.user_id == user_id, + ), + ) + await db.commit() + + +async def delete_message_by_firebase_device_token( + device_token: str, + db: AsyncSession, +): + await db.execute( + delete(models_notification.Message).where( + models_notification.Message.firebase_device_token == device_token, + ), + ) + await db.commit() + + +async def delete_firebase_devices_by_user_id( + user_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_notification.FirebaseDevice).where( + models_notification.FirebaseDevice.user_id == user_id, + ), + ) + await db.commit() diff --git a/app/core/notification/endpoints_notification.py b/app/core/notification/endpoints_notification.py index bd2bac34f4..53a5b8ce2e 100644 --- a/app/core/notification/endpoints_notification.py +++ b/app/core/notification/endpoints_notification.py @@ -10,6 +10,9 @@ schemas_notification, ) from app.core.notification.notification_types import CustomTopic, Topic +from app.core.notification.user_deleter_notification import ( + NotificationUserDeleter, +) from app.core.users import models_users from app.dependencies import ( get_db, @@ -29,6 +32,7 @@ root="notification", tag="Notifications", router=router, + user_deleter=NotificationUserDeleter(), ) diff --git a/app/core/notification/user_deleter_notification.py b/app/core/notification/user_deleter_notification.py new file mode 100644 index 0000000000..7fb08c7470 --- /dev/null +++ b/app/core/notification/user_deleter_notification.py @@ -0,0 +1,33 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.notification import cruds_notification +from app.types.module_user_deleter import ModuleUserDeleter + + +class NotificationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + devices = await cruds_notification.get_firebase_devices_by_user_id( + db=db, + user_id=user_id, + ) + for device in devices: + await cruds_notification.delete_message_by_firebase_device_token( + db=db, + device_token=device.firebase_device_token, + ) + await cruds_notification.delete_firebase_devices_by_user_id( + db=db, + user_id=user_id, + ) + + await cruds_notification.delete_topic_membership_by_user_id( + db=db, + user_id=user_id, + ) diff --git a/app/core/payment/endpoints_payment.py b/app/core/payment/endpoints_payment.py index a3ee1ee3a5..93150fbb0f 100644 --- a/app/core/payment/endpoints_payment.py +++ b/app/core/payment/endpoints_payment.py @@ -12,6 +12,7 @@ from app.core.core_module_list import core_module_list from app.core.payment import cruds_payment, models_payment, schemas_payment +from app.core.payment.user_deleter_payment import PaymentUserDeleter from app.dependencies import get_db from app.modules.module_list import module_list from app.types.module import CoreModule @@ -22,6 +23,7 @@ root="payment", tag="Payments", router=router, + user_deleter=PaymentUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/core/payment/user_deleter_payment.py b/app/core/payment/user_deleter_payment.py new file mode 100644 index 0000000000..4b159b8776 --- /dev/null +++ b/app/core/payment/user_deleter_payment.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PaymentUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/schools/endpoints_schools.py b/app/core/schools/endpoints_schools.py index 04c09dff42..af44a6590e 100644 --- a/app/core/schools/endpoints_schools.py +++ b/app/core/schools/endpoints_schools.py @@ -14,6 +14,7 @@ from app.core.groups.groups_type import AccountType, GroupType from app.core.schools import cruds_schools, models_schools, schemas_schools from app.core.schools.schools_type import SchoolType +from app.core.schools.user_deleter_schools import SchoolsUserDeleter from app.core.users import cruds_users, schemas_users from app.dependencies import ( get_db, @@ -27,6 +28,7 @@ root="schools", tag="Schools", router=router, + user_deleter=SchoolsUserDeleter(), ) diff --git a/app/core/schools/user_deleter_schools.py b/app/core/schools/user_deleter_schools.py new file mode 100644 index 0000000000..f4bdeffd79 --- /dev/null +++ b/app/core/schools/user_deleter_schools.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class SchoolsUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/core/users/cruds_users.py b/app/core/users/cruds_users.py index 0633a37916..d68761fbf2 100644 --- a/app/core/users/cruds_users.py +++ b/app/core/users/cruds_users.py @@ -3,7 +3,7 @@ from collections.abc import Sequence from uuid import UUID -from sqlalchemy import ForeignKey, and_, delete, not_, or_, select, update +from sqlalchemy import ForeignKey, and_, delete, func, not_, or_, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -97,6 +97,7 @@ async def get_users( *excluded_account_type_condition, *excluded_group_condition, school_condition, + models_users.CoreUser.deactivated.is_(False), ), ), ) @@ -111,7 +112,10 @@ async def get_user_by_id( result = await db.execute( select(models_users.CoreUser) - .where(models_users.CoreUser.id == user_id) + .where( + models_users.CoreUser.id == user_id, + models_users.CoreUser.deactivated.is_(False), + ) .options( # The group relationship need to be loaded selectinload(models_users.CoreUser.groups), @@ -128,7 +132,10 @@ async def get_user_by_email( result = await db.execute( select(models_users.CoreUser) - .where(models_users.CoreUser.email == email) + .where( + models_users.CoreUser.email == email, + models_users.CoreUser.deactivated.is_(False), + ) .options( # The group relationship need to be loaded to be able # to check if the user is a member of a specific group @@ -145,7 +152,10 @@ async def update_user( ): await db.execute( update(models_users.CoreUser) - .where(models_users.CoreUser.id == user_id) + .where( + models_users.CoreUser.id == user_id, + models_users.CoreUser.deactivated.is_(False), + ) .values(**user_update.model_dump(exclude_none=True)), ) @@ -298,7 +308,10 @@ async def update_user_password_by_id( ): await db.execute( update(models_users.CoreUser) - .where(models_users.CoreUser.id == user_id) + .where( + models_users.CoreUser.id == user_id, + models_users.CoreUser.deactivated.is_(False), + ) .values(password_hash=new_password_hash), ) await db.commit() @@ -439,3 +452,71 @@ async def fusion_users( # Delete the user_deleted await delete_user(db, user_deleted_id) + + +async def count_deactivated_users(db: AsyncSession) -> int: + """Return the number of deactivated users in the database""" + + result = ( + await db.execute( + select(func.count()).where( + models_users.CoreUser.deactivated, + ), + ) + ).scalar() + return result or 0 + + +async def deactivate_user( + db: AsyncSession, + user_id: str, +): + """Deactivate a user in the database""" + count = await count_deactivated_users(db) + + await db.execute( + update(models_users.CoreUser) + .where(models_users.CoreUser.id == user_id) + .values( + deactivated=True, + email=f"deleted.user{count}@myecl.fr", + name="Deleted User", + firstname=str(count), + nickname=None, + floor=None, + phone=None, + promo=None, + birthday=None, + school_id=SchoolType.no_school.value, + account_type=AccountType.external, + ), + ) + await db.commit() + + +async def delete_email_migration_code_by_user_id( + db: AsyncSession, + user_id: str, +): + """Delete a user from database by id""" + + await db.execute( + delete(models_users.CoreUserEmailMigrationCode).where( + models_users.CoreUserEmailMigrationCode.user_id == user_id, + ), + ) + await db.commit() + + +async def delete_recover_request_by_user_id( + db: AsyncSession, + user_id: str, +): + """Delete a user from database by id""" + + await db.execute( + delete(models_users.CoreUserRecoverRequest).where( + models_users.CoreUserRecoverRequest.user_id == user_id, + ), + ) + await db.commit() diff --git a/app/core/users/endpoints_users.py b/app/core/users/endpoints_users.py index dd5d7c358a..2bba78d284 100644 --- a/app/core/users/endpoints_users.py +++ b/app/core/users/endpoints_users.py @@ -25,6 +25,7 @@ from app.core.schools.schools_type import SchoolType from app.core.users import cruds_users, models_users, schemas_users from app.core.users.tools_users import get_account_type_and_school_id_from_email +from app.core.users.user_deleter_users import UsersUserDeleter from app.core.utils import security from app.core.utils.config import Settings from app.dependencies import ( @@ -53,6 +54,7 @@ root="users", tag="Users", router=router, + user_deleter=UsersUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/core/users/models_users.py b/app/core/users/models_users.py index b3b4682323..c14e8d5fda 100644 --- a/app/core/users/models_users.py +++ b/app/core/users/models_users.py @@ -38,6 +38,7 @@ class CoreUser(Base): phone: Mapped[str | None] floor: Mapped[FloorsType | None] created_on: Mapped[datetime | None] + deactivated: Mapped[bool] = mapped_column(default=False) # We use list["CoreGroup"] with quotes as CoreGroup is only defined after this class # Defining CoreUser after CoreGroup would cause a similar issue diff --git a/app/core/users/user_deleter_users.py b/app/core/users/user_deleter_users.py new file mode 100644 index 0000000000..2c49f942bd --- /dev/null +++ b/app/core/users/user_deleter_users.py @@ -0,0 +1,32 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.users import cruds_users +from app.types.module_user_deleter import ModuleUserDeleter +from app.utils.tools import delete_file_from_data + + +class UsersUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_users.delete_email_migration_code_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_users.delete_recover_request_by_user_id( + db=db, + user_id=user_id, + ) + await cruds_users.deactivate_user( + db=db, + user_id=user_id, + ) + delete_file_from_data( + directory="profile-pictures", + filename=user_id, + ) diff --git a/app/modules/advert/endpoints_advert.py b/app/modules/advert/endpoints_advert.py index a8fc0cc744..44b3cc3338 100644 --- a/app/modules/advert/endpoints_advert.py +++ b/app/modules/advert/endpoints_advert.py @@ -18,6 +18,7 @@ is_user_in, ) from app.modules.advert import cruds_advert, models_advert, schemas_advert +from app.modules.advert.user_deleter_advert import AdvertUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -33,6 +34,7 @@ root="advert", tag="Advert", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=AdvertUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/advert/user_deleter_advert.py b/app/modules/advert/user_deleter_advert.py new file mode 100644 index 0000000000..b5e863a1ed --- /dev/null +++ b/app/modules/advert/user_deleter_advert.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class AdvertUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/amap/endpoints_amap.py b/app/modules/amap/endpoints_amap.py index 4c9e773c06..6d3fc8c1f1 100644 --- a/app/modules/amap/endpoints_amap.py +++ b/app/modules/amap/endpoints_amap.py @@ -21,6 +21,7 @@ ) from app.modules.amap import cruds_amap, models_amap, schemas_amap from app.modules.amap.types_amap import DeliveryStatusType +from app.modules.amap.user_deleter_amap import AmapUserDeleter from app.types.module import Module from app.utils.communication.notifications import NotificationTool from app.utils.redis import locker_get, locker_set @@ -30,6 +31,7 @@ root="amap", tag="AMAP", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=AmapUserDeleter(), ) hyperion_amap_logger = logging.getLogger("hyperion.amap") diff --git a/app/modules/amap/user_deleter_amap.py b/app/modules/amap/user_deleter_amap.py new file mode 100644 index 0000000000..bfb2ba9719 --- /dev/null +++ b/app/modules/amap/user_deleter_amap.py @@ -0,0 +1,40 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.amap import cruds_amap +from app.modules.amap.types_amap import DeliveryStatusType +from app.types.module_user_deleter import ModuleUserDeleter + + +class AmapUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + # Check if the user has any active orders or a negative balance + reasons = [] + user_cash = await cruds_amap.get_cash_by_id(user_id=user_id, db=db) + if user_cash is not None: + if user_cash.balance < 0: + reasons.append("User has negative balance") + orders = await cruds_amap.get_orders_of_user(user_id=user_id, db=db) + for order in orders: + delivery = await cruds_amap.get_delivery_by_id( + db=db, + delivery_id=order.delivery_id, + ) + if delivery is None: + continue + if delivery.status not in [ + DeliveryStatusType.delivered, + DeliveryStatusType.archived, + ]: + reasons.append( + f"User has order in delivery not delivered or archived: {order.delivery_id}", + ) + if reasons: + return "\n - ".join(reasons) + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/booking/endpoints_booking.py b/app/modules/booking/endpoints_booking.py index 9777a34701..4ba8d626e1 100644 --- a/app/modules/booking/endpoints_booking.py +++ b/app/modules/booking/endpoints_booking.py @@ -18,6 +18,7 @@ ) from app.modules.booking import cruds_booking, models_booking, schemas_booking from app.modules.booking.types_booking import Decision +from app.modules.booking.user_deleter_booking import BookingUserDeleter from app.types.module import Module from app.utils.communication.notifications import NotificationTool from app.utils.tools import is_group_id_valid, is_user_member_of_any_group @@ -26,6 +27,7 @@ root="booking", tag="Booking", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=BookingUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/booking/user_deleter_booking.py b/app/modules/booking/user_deleter_booking.py new file mode 100644 index 0000000000..52f13855b5 --- /dev/null +++ b/app/modules/booking/user_deleter_booking.py @@ -0,0 +1,29 @@ +from datetime import UTC, datetime + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.booking import cruds_booking +from app.types.module_user_deleter import ModuleUserDeleter + + +class BookingUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + user_bookings = await cruds_booking.get_applicant_bookings( + db=db, + applicant_id=user_id, + ) + reasons = [ + f"User has booking in future: {booking.id}" + for booking in user_bookings + if booking.end > datetime.now(tz=UTC) + ] + if reasons: + return "\n - ".join(reasons) + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/calendar/endpoints_calendar.py b/app/modules/calendar/endpoints_calendar.py index babc7c2787..0b732fc577 100644 --- a/app/modules/calendar/endpoints_calendar.py +++ b/app/modules/calendar/endpoints_calendar.py @@ -10,6 +10,7 @@ from app.dependencies import get_db, is_user_an_ecl_member, is_user_in from app.modules.calendar import cruds_calendar, models_calendar, schemas_calendar from app.modules.calendar.types_calendar import Decision +from app.modules.calendar.user_deleter_calendar import CalendarUserDeleter from app.types.module import Module from app.utils.tools import is_user_member_of_any_group @@ -17,6 +18,7 @@ root="event", tag="Calendar", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=CalendarUserDeleter(), ) ical_file_path = "data/ics/ae_calendar.ics" diff --git a/app/modules/calendar/user_deleter_calendar.py b/app/modules/calendar/user_deleter_calendar.py new file mode 100644 index 0000000000..78c560d5ce --- /dev/null +++ b/app/modules/calendar/user_deleter_calendar.py @@ -0,0 +1,29 @@ +from datetime import UTC, datetime + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.calendar import cruds_calendar +from app.types.module_user_deleter import ModuleUserDeleter + + +class CalendarUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + user_events = await cruds_calendar.get_applicant_events( + db=db, + applicant_id=user_id, + ) + reasons = [ + f"User has booking in future: {event.id}" + for event in user_events + if event.end > datetime.now(tz=UTC) + ] + if reasons: + return "\n - ".join(reasons) + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/campaign/cruds_campaign.py b/app/modules/campaign/cruds_campaign.py index 305ae05ab9..ac983c382f 100644 --- a/app/modules/campaign/cruds_campaign.py +++ b/app/modules/campaign/cruds_campaign.py @@ -364,6 +364,19 @@ async def get_has_voted( return result.scalars().all() +async def delete_user_has_voted( + db: AsyncSession, + user_id: str, +) -> None: + """Delete all votes for a given user.""" + await db.execute( + delete(models_campaign.HasVoted).where( + models_campaign.HasVoted.user_id == user_id, + ), + ) + await db.commit() + + async def get_votes(db: AsyncSession) -> Sequence[models_campaign.Votes]: result = await db.execute(select(models_campaign.Votes)) return result.scalars().all() diff --git a/app/modules/campaign/endpoints_campaign.py b/app/modules/campaign/endpoints_campaign.py index a32d8db381..740c211009 100644 --- a/app/modules/campaign/endpoints_campaign.py +++ b/app/modules/campaign/endpoints_campaign.py @@ -19,6 +19,7 @@ ) from app.modules.campaign import cruds_campaign, models_campaign, schemas_campaign from app.modules.campaign.types_campaign import ListType, StatusType +from app.modules.campaign.user_deleter_campaign import CampaignUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -32,6 +33,7 @@ root="vote", tag="Campaign", default_allowed_groups_ids=[GroupType.AE], + user_deleter=CampaignUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/campaign/user_deleter_campaign.py b/app/modules/campaign/user_deleter_campaign.py new file mode 100644 index 0000000000..168c16abb6 --- /dev/null +++ b/app/modules/campaign/user_deleter_campaign.py @@ -0,0 +1,22 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.campaign import cruds_campaign, types_campaign +from app.types.module_user_deleter import ModuleUserDeleter + + +class CampaignUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + status = await cruds_campaign.get_status(db=db) + if status != types_campaign.StatusType.published: + return " - User has voted in unpublished campaign, wait for publish" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await cruds_campaign.delete_user_has_voted( + db=db, + user_id=user_id, + ) diff --git a/app/modules/cdr/endpoints_cdr.py b/app/modules/cdr/endpoints_cdr.py index 0b0532ce97..2bd2c09bc8 100644 --- a/app/modules/cdr/endpoints_cdr.py +++ b/app/modules/cdr/endpoints_cdr.py @@ -37,6 +37,7 @@ CdrStatus, DocumentSignatureType, ) +from app.modules.cdr.user_deleter_cdr import CdrUserDeleter from app.modules.cdr.utils_cdr import ( check_request_consistency, construct_dataframe_from_users_purchases, @@ -63,6 +64,7 @@ tag="Cdr", payment_callback=validate_payment, default_allowed_groups_ids=[GroupType.admin_cdr], + user_deleter=CdrUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/cdr/user_deleter_cdr.py b/app/modules/cdr/user_deleter_cdr.py new file mode 100644 index 0000000000..ef25ee52fc --- /dev/null +++ b/app/modules/cdr/user_deleter_cdr.py @@ -0,0 +1,25 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.cdr.cruds_cdr import get_payments_by_user_id, get_purchases_by_user_id +from app.types.module_user_deleter import ModuleUserDeleter + + +class CdrUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + reasons = "" + purchases = await get_purchases_by_user_id(db, user_id) + payments = await get_payments_by_user_id(db, user_id) + if any(not purchase.validated for purchase in purchases): + reasons += "\n - User has pending purchases" + if sum(payment.total for payment in payments) != sum( + purchase.quantity * purchase.product_variant.price for purchase in purchases + ): + reasons += "\n - User has uneven wallet balance" + return reasons + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/centralisation/endpoints_centralisation.py b/app/modules/centralisation/endpoints_centralisation.py index 6dd32d93f3..d5072eb373 100644 --- a/app/modules/centralisation/endpoints_centralisation.py +++ b/app/modules/centralisation/endpoints_centralisation.py @@ -1,8 +1,12 @@ from app.core.groups.groups_type import AccountType +from app.modules.centralisation.user_deleter_centralisation import ( + CentralisationUserDeleter, +) from app.types.module import Module module = Module( root="centralisation", tag="Centralisation", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=CentralisationUserDeleter(), ) diff --git a/app/modules/centralisation/user_deleter_centralisation.py b/app/modules/centralisation/user_deleter_centralisation.py new file mode 100644 index 0000000000..947954fe9c --- /dev/null +++ b/app/modules/centralisation/user_deleter_centralisation.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CentralisationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/cinema/endpoints_cinema.py b/app/modules/cinema/endpoints_cinema.py index c700ef8c7d..dfbb5385e7 100644 --- a/app/modules/cinema/endpoints_cinema.py +++ b/app/modules/cinema/endpoints_cinema.py @@ -22,6 +22,7 @@ is_user_in, ) from app.modules.cinema import cruds_cinema, schemas_cinema +from app.modules.cinema.user_deleter_cinema import CinemaUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -38,6 +39,7 @@ root="cinema", tag="Cinema", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=CinemaUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/cinema/user_deleter_cinema.py b/app/modules/cinema/user_deleter_cinema.py new file mode 100644 index 0000000000..54b665e7a5 --- /dev/null +++ b/app/modules/cinema/user_deleter_cinema.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class CinemaUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/flappybird/cruds_flappybird.py b/app/modules/flappybird/cruds_flappybird.py index 4133013bc0..1ba966761b 100644 --- a/app/modules/flappybird/cruds_flappybird.py +++ b/app/modules/flappybird/cruds_flappybird.py @@ -89,6 +89,18 @@ async def delete_flappybird_best_score( ) +async def delete_flappybird_score( + db: AsyncSession, + user_id: str, +): + """Remove a FlappyBirdScore in database""" + await db.execute( + delete(models_flappybird.FlappyBirdScore).where( + models_flappybird.FlappyBirdScore.user_id == user_id, + ), + ) + + async def update_flappybird_best_score( db: AsyncSession, user_id: str, diff --git a/app/modules/flappybird/endpoints_flappybird.py b/app/modules/flappybird/endpoints_flappybird.py index f64f022ed2..e8af1459b9 100644 --- a/app/modules/flappybird/endpoints_flappybird.py +++ b/app/modules/flappybird/endpoints_flappybird.py @@ -12,12 +12,14 @@ models_flappybird, schemas_flappybird, ) +from app.modules.flappybird.user_deleter_flappybird import FlappybirdUserDeleter from app.types.module import Module module = Module( root="flappybird", tag="Flappy Bird", default_allowed_account_types=[AccountType.student], + user_deleter=FlappybirdUserDeleter(), ) diff --git a/app/modules/flappybird/user_deleter_flappybird.py b/app/modules/flappybird/user_deleter_flappybird.py new file mode 100644 index 0000000000..3f9dc1b03a --- /dev/null +++ b/app/modules/flappybird/user_deleter_flappybird.py @@ -0,0 +1,26 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.flappybird.cruds_flappybird import ( + delete_flappybird_best_score, + delete_flappybird_score, +) +from app.types.module_user_deleter import ModuleUserDeleter + + +class FlappybirdUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + await delete_flappybird_best_score( + db=db, + user_id=user_id, + ) + await delete_flappybird_score( + db=db, + user_id=user_id, + ) diff --git a/app/modules/home/endpoints_home.py b/app/modules/home/endpoints_home.py index 1e717783f0..cad316c14f 100644 --- a/app/modules/home/endpoints_home.py +++ b/app/modules/home/endpoints_home.py @@ -1,8 +1,10 @@ from app.core.groups.groups_type import AccountType +from app.modules.home.user_deleter_home import HomeUserDeleter from app.types.module import Module module = Module( root="home", tag="Home", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=HomeUserDeleter(), ) diff --git a/app/modules/home/user_deleter_home.py b/app/modules/home/user_deleter_home.py new file mode 100644 index 0000000000..d02419613d --- /dev/null +++ b/app/modules/home/user_deleter_home.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class HomeUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/loan/endpoints_loan.py b/app/modules/loan/endpoints_loan.py index bde67051eb..dd10868fde 100644 --- a/app/modules/loan/endpoints_loan.py +++ b/app/modules/loan/endpoints_loan.py @@ -17,6 +17,7 @@ is_user_in, ) from app.modules.loan import cruds_loan, models_loan, schemas_loan +from app.modules.loan.user_deleter_loan import LoanUserDeleter from app.types.module import Module from app.types.scheduler import Scheduler from app.utils.communication.notifications import NotificationTool @@ -34,6 +35,7 @@ root="loan", tag="Loans", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=LoanUserDeleter(), ) diff --git a/app/modules/loan/user_deleter_loan.py b/app/modules/loan/user_deleter_loan.py new file mode 100644 index 0000000000..63343c0bdd --- /dev/null +++ b/app/modules/loan/user_deleter_loan.py @@ -0,0 +1,19 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.loan.cruds_loan import get_loans_by_borrower +from app.types.module_user_deleter import ModuleUserDeleter + + +class LoanUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + loans = await get_loans_by_borrower(db, user_id) + if any(not loan.returned for loan in loans): + return "\n - User has pending loans" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/ph/endpoints_ph.py b/app/modules/ph/endpoints_ph.py index e292e39570..68798a5012 100644 --- a/app/modules/ph/endpoints_ph.py +++ b/app/modules/ph/endpoints_ph.py @@ -18,6 +18,7 @@ is_user_in, ) from app.modules.ph import cruds_ph, models_ph, schemas_ph +from app.modules.ph.user_deleter_ph import PHUserDeleter from app.types.content_type import ContentType from app.types.module import Module from app.types.scheduler import Scheduler @@ -33,6 +34,7 @@ root="ph", tag="ph", default_allowed_account_types=[AccountType.student], + user_deleter=PHUserDeleter(), ) diff --git a/app/modules/ph/user_deleter_ph.py b/app/modules/ph/user_deleter_ph.py new file mode 100644 index 0000000000..87670e7ef5 --- /dev/null +++ b/app/modules/ph/user_deleter_ph.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PHUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/phonebook/endpoints_phonebook.py b/app/modules/phonebook/endpoints_phonebook.py index 9940d44caf..8fcfa221f7 100644 --- a/app/modules/phonebook/endpoints_phonebook.py +++ b/app/modules/phonebook/endpoints_phonebook.py @@ -16,6 +16,7 @@ ) from app.modules.phonebook import cruds_phonebook, models_phonebook, schemas_phonebook from app.modules.phonebook.types_phonebook import RoleTags +from app.modules.phonebook.user_deleter_phonebook import PhonebookUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -29,6 +30,7 @@ root="phonebook", tag="Phonebook", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=PhonebookUserDeleter(), ) hyperion_error_logger = logging.getLogger("hyperion.error") diff --git a/app/modules/phonebook/user_deleter_phonebook.py b/app/modules/phonebook/user_deleter_phonebook.py new file mode 100644 index 0000000000..ca4e450ad9 --- /dev/null +++ b/app/modules/phonebook/user_deleter_phonebook.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PhonebookUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/purchases/endpoints_purchases.py b/app/modules/purchases/endpoints_purchases.py index 26b0c30bf4..8be1fb9b96 100644 --- a/app/modules/purchases/endpoints_purchases.py +++ b/app/modules/purchases/endpoints_purchases.py @@ -1,8 +1,12 @@ from app.core.groups.groups_type import AccountType +from app.modules.purchases.user_deleter_purchases import ( + PurchasesUserDeleter, +) from app.types.module import Module module = Module( root="purchases", tag="Purchases", default_allowed_account_types=[AccountType.student, AccountType.external], + user_deleter=PurchasesUserDeleter(), ) diff --git a/app/modules/purchases/user_deleter_purchases.py b/app/modules/purchases/user_deleter_purchases.py new file mode 100644 index 0000000000..dfba9cbcf1 --- /dev/null +++ b/app/modules/purchases/user_deleter_purchases.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class PurchasesUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/raffle/endpoints_raffle.py b/app/modules/raffle/endpoints_raffle.py index 2723111794..eef9fcdd92 100644 --- a/app/modules/raffle/endpoints_raffle.py +++ b/app/modules/raffle/endpoints_raffle.py @@ -19,6 +19,7 @@ ) from app.modules.raffle import cruds_raffle, models_raffle, schemas_raffle from app.modules.raffle.types_raffle import RaffleStatusType +from app.modules.raffle.user_deleter_raffle import RaffleUserDeleter from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -33,6 +34,7 @@ root="tombola", tag="Raffle", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=RaffleUserDeleter(), ) hyperion_raffle_logger = logging.getLogger("hyperion.raffle") diff --git a/app/modules/raffle/user_deleter_raffle.py b/app/modules/raffle/user_deleter_raffle.py new file mode 100644 index 0000000000..094d986c91 --- /dev/null +++ b/app/modules/raffle/user_deleter_raffle.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class RaffleUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index 48402ae456..0d02b1d21b 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -22,6 +22,7 @@ ) from app.modules.raid import coredata_raid, cruds_raid, models_raid, schemas_raid from app.modules.raid.raid_type import DocumentType, DocumentValidation, Size +from app.modules.raid.user_deleter_raid import RaidUserDeleter from app.modules.raid.utils.drive.drive_file_manager import DriveFileManager from app.modules.raid.utils.utils_raid import ( get_participant, @@ -50,6 +51,7 @@ tag="Raid", payment_callback=validate_payment, default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=RaidUserDeleter(), ) diff --git a/app/modules/raid/user_deleter_raid.py b/app/modules/raid/user_deleter_raid.py new file mode 100644 index 0000000000..a36fe12910 --- /dev/null +++ b/app/modules/raid/user_deleter_raid.py @@ -0,0 +1,18 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.raid.cruds_raid import is_user_a_participant +from app.types.module_user_deleter import ModuleUserDeleter + + +class RaidUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + if await is_user_a_participant(user_id, db) is not None: + return "\n - User is a participant in the current edition" + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index b7722c53f8..d58be483a2 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -18,6 +18,9 @@ models_recommendation, schemas_recommendation, ) +from app.modules.recommendation.user_deleter_recommendation import ( + RecommendationUserDeleter, +) from app.types import standard_responses from app.types.content_type import ContentType from app.types.module import Module @@ -30,6 +33,7 @@ root="recommendation", tag="Recommendation", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=RecommendationUserDeleter(), ) diff --git a/app/modules/recommendation/user_deleter_recommendation.py b/app/modules/recommendation/user_deleter_recommendation.py new file mode 100644 index 0000000000..05a522059a --- /dev/null +++ b/app/modules/recommendation/user_deleter_recommendation.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class RecommendationUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/modules/seed_library/endpoints_seed_library.py b/app/modules/seed_library/endpoints_seed_library.py index 3e6d8b53c4..183ffac051 100644 --- a/app/modules/seed_library/endpoints_seed_library.py +++ b/app/modules/seed_library/endpoints_seed_library.py @@ -18,6 +18,9 @@ schemas_seed_library, ) from app.modules.seed_library.types_seed_library import PlantState, SpeciesType +from app.modules.seed_library.user_deleter_seed_library import ( + SeedLibraryUserDeleter, +) from app.types.module import Module from app.utils import tools from app.utils.tools import is_user_member_of_any_group @@ -26,6 +29,7 @@ root="seed_library", tag="seed_library", default_allowed_account_types=[AccountType.student, AccountType.staff], + user_deleter=SeedLibraryUserDeleter(), ) diff --git a/app/modules/seed_library/user_deleter_seed_library.py b/app/modules/seed_library/user_deleter_seed_library.py new file mode 100644 index 0000000000..731fbb1e52 --- /dev/null +++ b/app/modules/seed_library/user_deleter_seed_library.py @@ -0,0 +1,15 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.types.module_user_deleter import ModuleUserDeleter + + +class SeedLibraryUserDeleter(ModuleUserDeleter): + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass diff --git a/app/types/module.py b/app/types/module.py index 8ba8239f29..ceeb0dbbea 100644 --- a/app/types/module.py +++ b/app/types/module.py @@ -5,6 +5,7 @@ from app.core.groups.groups_type import AccountType, GroupType from app.core.payment import schemas_payment +from app.types.module_user_deleter import ModuleUserDeleter class CoreModule: @@ -12,6 +13,7 @@ def __init__( self, root: str, tag: str, + user_deleter: ModuleUserDeleter, router: APIRouter | None = None, payment_callback: Callable[ [schemas_payment.CheckoutPayment, AsyncSession], @@ -22,10 +24,13 @@ def __init__( """ Initialize a new Module object. :param root: the root of the module, used by Titan + :param tag: the tag of the module, used by FastAPI + :param user_deleter: a ModuleUserDeleter to handle user deletion :param router: an optional custom APIRouter :param payment_callback: an optional method to call when a payment is notified by HelloAsso. A CheckoutPayment and the database will be provided during the call """ self.root = root + self.user_deleter = user_deleter self.router = router or APIRouter(tags=[tag]) self.payment_callback: ( Callable[[schemas_payment.CheckoutPayment, AsyncSession], Awaitable[None]] @@ -38,6 +43,7 @@ def __init__( self, root: str, tag: str, + user_deleter: ModuleUserDeleter, default_allowed_groups_ids: list[GroupType] | None = None, default_allowed_account_types: list[AccountType] | None = None, router: APIRouter | None = None, @@ -50,12 +56,15 @@ def __init__( """ Initialize a new Module object. :param root: the root of the module, used by Titan + :param tag: the tag of the module, used by FastAPI + :param user_deleter: a ModuleUserDeleter to handle user deletion :param default_allowed_groups_ids: list of groups that should be able to see the module by default :param default_allowed_account_types: list of account_types that should be able to see the module by default :param router: an optional custom APIRouter :param payment_callback: an optional method to call when a payment is notified by HelloAsso. A CheckoutPayment and the database will be provided during the call """ self.root = root + self.user_deleter = user_deleter self.default_allowed_groups_ids = default_allowed_groups_ids self.default_allowed_account_types = default_allowed_account_types self.router = router or APIRouter(tags=[tag]) diff --git a/app/types/module_user_deleter.py b/app/types/module_user_deleter.py new file mode 100644 index 0000000000..8d38ff5f4d --- /dev/null +++ b/app/types/module_user_deleter.py @@ -0,0 +1,48 @@ +from abc import ABC, abstractmethod + +from sqlalchemy.ext.asyncio import AsyncSession + + +class ModuleUserDeleter(ABC): + """ + Abstract base class for user deletion functionality. + This class defines the interface for deleting users from the system. + Each module should implement this interface to provide its own user deletion logic. + """ + + @abstractmethod + async def has_reason_not_to_delete_user( + self, + user_id: str, + db: AsyncSession, + ) -> str: + """ + Check if the user can be deleted. + :param user_id: The ID of the user to check. + :return: True if the user can be deleted, False otherwise. + """ + + @abstractmethod + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + """ + Delete the user from the system. + :param user_id: The ID of the user to delete. + """ + + +""" +from app.types.module_user_deleter import ModuleUserDeleter + +from sqlalchemy.ext.asyncio import AsyncSession + + +class CoreUserDeleter(ModuleUserDeleter): + async def can_delete_user(self, user_id: str, db: AsyncSession) -> Literal[True] | str: + return "" + + async def delete_user(self, user_id: str, db: AsyncSession) -> None: + pass + + +() +"""