Skip to content

Commit

Permalink
feat: allow user deletion without deleting task references
Browse files Browse the repository at this point in the history
  • Loading branch information
Anuj-Gupta4 committed Nov 4, 2024
1 parent 2e58733 commit b361e92
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 23 deletions.
34 changes: 24 additions & 10 deletions src/backend/app/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"""SQLAlchemy database models for interacting with Postgresql."""

from datetime import datetime
from typing import cast
from typing import Optional, cast

from geoalchemy2 import Geometry, WKBElement
from sqlalchemy import (
Expand Down Expand Up @@ -222,14 +222,15 @@ class DbTaskHistory(Base):
action_text = cast(str, Column(String))
action_date = cast(datetime, Column(DateTime, nullable=False, default=timestamp))
user_id = cast(
int,
Optional[int],
Column(
BigInteger,
ForeignKey("users.id", name="fk_users"),
index=True,
nullable=False,
nullable=True,
),
)
username = cast(str, Column(String))

# Define relationships
user = relationship(DbUser, uselist=False, backref="task_history_user")
Expand Down Expand Up @@ -288,17 +289,30 @@ class DbTask(Base):
feature_count = cast(int, Column(Integer))
task_status = cast(TaskStatus, Column(Enum(TaskStatus), default=TaskStatus.READY))
locked_by = cast(
int,
Column(BigInteger, ForeignKey("users.id", name="fk_users_locked"), index=True),
Optional[int],
Column(
BigInteger,
ForeignKey("users.id", name="fk_users_locked"),
index=True,
nullable=True,
),
)
mapped_by = cast(
int,
Column(BigInteger, ForeignKey("users.id", name="fk_users_mapper"), index=True),
Optional[int],
Column(
BigInteger,
ForeignKey("users.id", name="fk_users_mapper"),
index=True,
nullable=True,
),
)
validated_by = cast(
int,
Optional[int],
Column(
BigInteger, ForeignKey("users.id", name="fk_users_validator"), index=True
BigInteger,
ForeignKey("users.id", name="fk_users_validator"),
index=True,
nullable=True,
),
)

Expand Down Expand Up @@ -344,7 +358,7 @@ class DbProject(Base):
Column(
BigInteger,
ForeignKey("users.id", name="fk_users"),
nullable=False,
nullable=True,
server_default="20386219",
),
)
Expand Down
51 changes: 40 additions & 11 deletions src/backend/app/tasks/tasks_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,8 @@ async def update_task_status(

# update history prior to updating task
update_history = await create_task_history_for_status_change(
db_task, new_status, db_user
db_task, new_status, db_user, db
)
db.add(update_history)

db_task.task_status = new_status

Expand Down Expand Up @@ -176,7 +175,7 @@ async def update_task_status(


async def create_task_history_for_status_change(
db_task: db_models.DbTask, new_status: TaskStatus, db_user: db_models.DbUser
db_task: db_models.DbTask, new_status: TaskStatus, db_user: db_models.DbUser, db
):
"""Append task status change to task history."""
msg = (
Expand All @@ -185,15 +184,34 @@ async def create_task_history_for_status_change(
)
log.info(msg)

new_task_history = db_models.DbTaskHistory(
project_id=db_task.project_id,
task_id=db_task.id,
action=get_action_for_status_change(new_status),
action_text=msg,
actioned_by=db_user,
user_id=db_user.id,
query = text(
"""
INSERT INTO task_history (
project_id, task_id, action, action_text,
action_date, user_id
)
VALUES (
:project_id, :task_id, :action, :action_text,
:action_date, :user_id
)
RETURNING *
"""
)

params = {
"project_id": db_task.project_id,
"task_id": db_task.id,
"action": get_action_for_status_change(new_status).name,
"action_text": msg,
"action_date": datetime.now(),
"user_id": db_user.id,
}

result = db.execute(query, params)
db.commit()

row = result.fetchone()

# TODO add invalidation history
# if new_status == TaskStatus.INVALIDATED:
# new_invalidation_history = db_models.DbTaskInvalidationHistory(
Expand All @@ -204,6 +222,17 @@ async def create_task_history_for_status_change(
# TODO add mapping issue
# if new_status == TaskStatus.BAD:

new_task_history = db_models.DbTaskHistory(
id=row[0],
project_id=row[1],
task_id=row[2],
action=row[3],
action_text=row[4],
action_date=row[5],
user_id=row[6],
username=row[7],
)

return new_task_history


Expand Down Expand Up @@ -348,7 +377,7 @@ async def get_project_task_history(
"""
query = """
SELECT task_history.id, task_history.task_id, task_history.action_text,
task_history.action_date, users.username,
task_history.action_date, task_history.username,
users.profile_img
FROM task_history
LEFT JOIN users on users.id = task_history.user_id
Expand Down
4 changes: 2 additions & 2 deletions src/backend/app/tasks/tasks_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ class TaskHistoryBase(BaseModel):
class TaskHistoryOut(TaskHistoryBase):
"""Task mapping history display."""

username: str
profile_img: Optional[str]
username: Optional[str] = ""
profile_img: Optional[str] = ""
status: Optional[str] = None


Expand Down
71 changes: 71 additions & 0 deletions src/backend/app/users/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from typing import List

from fastapi import APIRouter, Depends, HTTPException
from loguru import logger as log
from sqlalchemy import text
from sqlalchemy.orm import Session

from app.db import database
Expand Down Expand Up @@ -83,3 +85,72 @@ async def get_user_roles():
for role in UserRoleEnum:
user_roles[role.name] = role.value
return user_roles


@router.delete("/{user_id}", response_model=dict)
async def delete_user_account(user_id: int, db: Session = Depends(database.get_db)):
"""Delete a user account while preserving task history references."""
try:
transaction_sql = text(
"""
BEGIN;
-- Update task history to remove user references
UPDATE task_history
SET user_id = NULL
WHERE user_id = :user_id;
-- Update projects authored by the user
UPDATE projects
SET author_id = NULL
WHERE author_id = :user_id;
-- Update tasks associated with the user
UPDATE tasks
SET
mapped_by = CASE
WHEN mapped_by = :user_id THEN NULL
ELSE mapped_by
END,
locked_by = CASE
WHEN locked_by = :user_id THEN NULL
ELSE locked_by
END,
validated_by = CASE
WHEN validated_by = :user_id THEN NULL
ELSE validated_by
END
WHERE
mapped_by = :user_id
OR locked_by = :user_id
OR validated_by = :user_id;
-- Delete user roles
DELETE FROM user_roles
WHERE user_id = :user_id;
-- Delete organisation managers
DELETE FROM organisation_managers
WHERE user_id = :user_id;
-- Delete user
DELETE FROM users
WHERE id = :user_id;
COMMIT;
"""
)

result = db.execute(transaction_sql, {"user_id": user_id})

if result.rowcount == 0:
raise HTTPException(status_code=404, detail=f"User ID {user_id} not found")

db.commit()

except Exception as e:
db.rollback()
log.error(f"Error occurred while deleting user {user_id}: {e}")
raise HTTPException(status_code=400, detail="Failed to delete user") from e

return {"message": "User deleted successfully"}
25 changes: 25 additions & 0 deletions src/backend/migrations/009-delete-user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- ## Migration to:
-- * Enable user data deletion

-- Start a transaction
BEGIN;

-- Allow user_id in task_history to be NULL
ALTER TABLE task_history
ALTER COLUMN user_id DROP NOT NULL;

-- Add a new column username to task_history
ALTER TABLE task_history
ADD COLUMN username VARCHAR;

-- Allow author_id in projects to be NULL
ALTER TABLE projects ALTER COLUMN author_id DROP NOT NULL;

-- Allow locked_by, mapped_by, and validated_by in tasks to be NULL
ALTER TABLE tasks
ALTER COLUMN locked_by DROP NOT NULL,
ALTER COLUMN mapped_by DROP NOT NULL,
ALTER COLUMN validated_by DROP NOT NULL;

-- Commit the transaction
COMMIT;
25 changes: 25 additions & 0 deletions src/backend/migrations/revert/009-delete-user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- ## Migration to:
-- * Revert user data deletion changes

-- Start a transaction
BEGIN;

-- Disallow user_id in task_history to be NULL
ALTER TABLE task_history
ALTER COLUMN user_id SET NOT NULL;

-- Remove the column username from task_history
ALTER TABLE task_history
DROP COLUMN username;

-- Disallow author_id in projects to be NULL
ALTER TABLE projects ALTER COLUMN author_id SET NOT NULL;

-- Disallow locked_by, mapped_by, and validated_by in tasks to be NULL
ALTER TABLE tasks
ALTER COLUMN locked_by SET NOT NULL,
ALTER COLUMN mapped_by SET NOT NULL,
ALTER COLUMN validated_by SET NOT NULL;

-- Commit the transaction
COMMIT;

0 comments on commit b361e92

Please sign in to comment.