Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update task states for each task. #62

Merged
merged 11 commits into from
Jul 10, 2024
2 changes: 1 addition & 1 deletion src/backend/app/db/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ class TaskEvent(Base):
user_id = cast(str, Column(String(100), ForeignKey("users.id"), nullable=False))
comment = cast(str, Column(String))

state = cast(State, Column(Enum(TaskStatus), nullable=False))
state = cast(State, Column(Enum(State), nullable=False))
created_at = cast(datetime, Column(DateTime, default=timestamp))

__table_args__ = (
Expand Down
2 changes: 2 additions & 0 deletions src/backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from fastapi.responses import RedirectResponse, JSONResponse
from app.users import oauth_routes
from app.users import user_routes
from app.tasks import task_routes
from loguru import logger as log
from fastapi.templating import Jinja2Templates

Expand Down Expand Up @@ -95,6 +96,7 @@ def get_application() -> FastAPI:
_app.include_router(waypoint_routes.router)
_app.include_router(user_routes.router)
_app.include_router(oauth_routes.router)
_app.include_router(task_routes.router)

return _app

Expand Down
56 changes: 56 additions & 0 deletions src/backend/app/migrations/versions/06668eb5d14a_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""

Revision ID: 06668eb5d14a
Revises: fa5c74996273
Create Date: 2024-07-09 04:17:49.816148

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "06668eb5d14a"
down_revision: Union[str, None] = "fa5c74996273"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


# Define the new enum type
new_state_enum = sa.Enum(
"UNLOCKED_TO_MAP",
"LOCKED_FOR_MAPPING",
"UNLOCKED_TO_VALIDATE",
"LOCKED_FOR_VALIDATION",
"UNLOCKED_DONE",
"REQUEST_FOR_MAPPING",
name="state",
)

old_state_enum = sa.Enum(
"UNLOCKED_TO_MAP",
"LOCKED_FOR_MAPPING",
"UNLOCKED_TO_VALIDATE",
"LOCKED_FOR_VALIDATION",
"UNLOCKED_DONE",
name="state",
)


def upgrade():
op.execute("ALTER TYPE state ADD VALUE 'REQUEST_FOR_MAPPING'")


def downgrade():
# Downgrade the enum type by recreating it without the new value
op.execute("ALTER TYPE state RENAME TO state_old")
old_state_enum.create(op.get_bind(), checkfirst=False)
op.execute(
(
"ALTER TABLE task_events "
"ALTER COLUMN state TYPE state USING state::text::state"
)
)
op.execute("DROP TYPE state_old")
66 changes: 66 additions & 0 deletions src/backend/app/migrations/versions/fa5c74996273_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""

Revision ID: fa5c74996273
Revises: ac09917990dc
Create Date: 2024-07-05 11:51:02.146671

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision: str = "fa5c74996273"
down_revision: Union[str, None] = "ac09917990dc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

# Define the existing enum type
existing_taskstatus_enum = sa.Enum(
"READY",
"LOCKED_FOR_MAPPING",
"MAPPED",
"LOCKED_FOR_VALIDATION",
"VALIDATED",
"INVALIDATED",
"BAD",
"SPLIT",
name="taskstatus",
)

# Define the new enum type
new_state_enum = sa.Enum(
"UNLOCKED_TO_MAP",
"LOCKED_FOR_MAPPING",
"UNLOCKED_TO_VALIDATE",
"LOCKED_FOR_VALIDATION",
"UNLOCKED_DONE",
name="state",
)


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###

# Create the new enum type in the database
new_state_enum.create(op.get_bind())

# Use the USING clause to convert existing column values to the new enum type
op.execute(
"ALTER TABLE task_events ALTER COLUMN state TYPE state USING state::text::state"
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###

# Use the USING clause to convert back to the original enum type
op.execute(
"ALTER TABLE task_events ALTER COLUMN state TYPE taskstatus USING state::text::taskstatus"
)

# Drop the new enum type from the database
new_state_enum.drop(op.get_bind())
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions src/backend/app/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,15 @@ class State(int, Enum):
"""The state of a task.

The state can be:
- ``request for mapping``
- ``unlocked to map``
- ``locked for mapping``
- ``unlocked to validate``
- ``locked for validation``
- ``unlocked done``
"""

REQUEST_FOR_MAPPING = -1
UNLOCKED_TO_MAP = 0
LOCKED_FOR_MAPPING = 1
UNLOCKED_TO_VALIDATE = 2
Expand All @@ -142,6 +144,7 @@ class EventType(str, Enum):

Possible values are:

- ``request`` -- Request a task to be mapped.
- ``map`` -- Set to *locked for mapping*, i.e. mapping in progress.
- ``finish`` -- Set to *unlocked to validate*, i.e. is mapped.
- ``validate`` -- Request recent task ready to be validate.
Expand All @@ -154,6 +157,8 @@ class EventType(str, Enum):
Note that ``task_id`` must be specified in the endpoint too.
"""

REQUESTS = "request"
REJECTED = "reject"
MAP = "map"
FINISH = "finish"
VALIDATE = "validate"
Expand Down
125 changes: 125 additions & 0 deletions src/backend/app/tasks/task_crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import uuid
from databases import Database
from app.models.enums import State


async def all_tasks_states(db: Database, project_id: uuid.UUID):
query = """
SELECT DISTINCT ON (task_id) project_id, task_id, state
FROM task_events
WHERE project_id=:project_id
ORDER BY task_id, created_at DESC
"""
r = await db.fetch_all(query, {"project_id": str(project_id)})

return [dict(r) for r in r]


async def request_mapping(
db: Database, project_id: uuid.UUID, task_id: uuid.UUID, user_id: str, comment: str
):
query = """
WITH last AS (
SELECT *
FROM task_events
WHERE project_id= :project_id AND task_id= :task_id
ORDER BY created_at DESC
LIMIT 1
),
released AS (
SELECT COUNT(*) = 0 AS no_record
FROM task_events
WHERE project_id= :project_id AND task_id= :task_id AND state = :unlocked_to_map_state
)
INSERT INTO task_events (event_id, project_id, task_id, user_id, comment, state, created_at)

SELECT
gen_random_uuid(),
:project_id,
:task_id,
:user_id,
:comment,
:request_for_map_state,
now()
FROM last
RIGHT JOIN released ON true
WHERE (last.state = :unlocked_to_map_state OR released.no_record = true);
"""

values = {
"project_id": str(project_id),
"task_id": str(task_id),
"user_id": str(user_id),
"comment": comment,
"unlocked_to_map_state": State.UNLOCKED_TO_MAP.name,
"request_for_map_state": State.REQUEST_FOR_MAPPING.name,
}

await db.fetch_one(query, values)

return {"project_id": project_id, "task_id": task_id, "comment": comment}


async def update_task_state(
db: Database,
project_id: uuid.UUID,
task_id: uuid.UUID,
user_id: str,
comment: str,
initial_state: State,
final_state: State,
):
query = """
WITH last AS (
SELECT *
FROM task_events
WHERE project_id = :project_id AND task_id = :task_id
ORDER BY created_at DESC
LIMIT 1
),
locked AS (
SELECT *
FROM last
WHERE user_id = :user_id AND state = :initial_state
)
INSERT INTO task_events(event_id, project_id, task_id, user_id, state, comment, created_at)
SELECT gen_random_uuid(), project_id, task_id, user_id, :final_state, :comment, now()
FROM last
WHERE user_id = :user_id
RETURNING project_id, task_id, user_id, state;
"""

values = {
"project_id": str(project_id),
"task_id": str(task_id),
"user_id": str(user_id),
"comment": comment,
"initial_state": initial_state.name,
"final_state": final_state.name,
}

await db.fetch_one(query, values)

return {"project_id": project_id, "task_id": task_id, "comment": comment}


async def get_requested_user_id(
db: Database, project_id: uuid.UUID, task_id: uuid.UUID
):
query = """
SELECT user_id
FROM task_events
WHERE project_id = :project_id AND task_id = :task_id and state = :request_for_map_state
ORDER BY created_at DESC
LIMIT 1
"""
values = {
"project_id": str(project_id),
"task_id": str(task_id),
"request_for_map_state": State.REQUEST_FOR_MAPPING.name,
}

result = await db.fetch_one(query, values)
if result is None:
raise ValueError("No user requested for mapping")
return result["user_id"]
Loading
Loading