Skip to content

Commit

Permalink
Merge pull request #62 from hotosm/task_events
Browse files Browse the repository at this point in the history
Update task states for each task.
  • Loading branch information
nrjadkry authored Jul 10, 2024
2 parents d3e88aa + d7a416a commit caaeea0
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 1 deletion.
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

0 comments on commit caaeea0

Please sign in to comment.