Skip to content

Commit

Permalink
Merge pull request #25 from it-goats/feature/ITG-58-ITG-62
Browse files Browse the repository at this point in the history
[ITG-24] Create, view and delete subtasks.
  • Loading branch information
gregori0o committed Apr 27, 2022
2 parents 55fbd3a + cb17da2 commit 5cab778
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 3 deletions.
8 changes: 8 additions & 0 deletions bode/bode/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from bode.app import db
from bode.models.tag import Tag
from bode.models.task_relation import TaskRelation
from bode.models.task_tag import task_tag
from bode.models.utc_datetime import UTCDateTime

Expand Down Expand Up @@ -44,6 +45,13 @@ def edit(task_id, **task_data):
return task

def delete(task_id):
subtasks = TaskRelation.get_subtasks_id_by_task_id(task_id)

for relation in TaskRelation.get_all_relations_by_task_id(task_id):
TaskRelation.delete(relation.id)
for subtask in subtasks:
Task.delete(subtask[0])

task = Task.get(task_id)

db.session.delete(task)
Expand Down
26 changes: 25 additions & 1 deletion bode/bode/models/task_relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ def list(cls):


class TaskRelation(db.Model):
"""
Type meaning:
T1 := first_task_id
T2 := second_task_id
type = SUBTASKS -> T2 is subtask of T1
type = DEPENDET -> T2 is dependent on T1
type = INTERCHANGABLE -> T1 is interchangable with T2 and (T2, T1, INTERCHANGABLE) record is in the database
"""

__tablename__ = "tasks_relations"

id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
Expand Down Expand Up @@ -71,7 +81,21 @@ def delete(relation_id):

return relation

def get_all_relations_by_task_id(task_id):
return (
TaskRelation.query.filter(TaskRelation.first_task_id == task_id).all()
+ TaskRelation.query.filter(TaskRelation.second_task_id == task_id).all()
)

def get_subtasks_id_by_task_id(task_id):
return (
TaskRelation.query.with_entities(TaskRelation.second_task_id)
.filter(TaskRelation.first_task_id == task_id)
.filter(TaskRelation.type == RelationType.Subtask.value)
.all()
)

def __repr__(self):
return f"""<TaskRelation
{self.first_task_id} <{self.relationship}> {self.second_task_id}
{self.first_task_id} <{self.type}> {self.second_task_id}
>"""
25 changes: 25 additions & 0 deletions bode/bode/resources/task_relations/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from psycopg2 import DataError
from sqlalchemy.exc import IntegrityError

from bode.app import db
from bode.models.task import Task
from bode.models.task_relation import TaskRelation
from bode.resources.task_relations.schemas import (
SimpleTaskRelationSchema,
TaskRelationInputSchema,
TasksRelatedSchema,
)

blueprint = Blueprint("task-relations", "task-relations", url_prefix="/task-relations")
Expand All @@ -21,9 +25,30 @@ def post(self, relation_data):
except IntegrityError:
abort(422, message="Relation already exists")

@blueprint.response(200, SimpleTaskRelationSchema(many=True))
def get(self):
return TaskRelation.query.all()


@blueprint.route("/<relation_id>")
class TasksRelationsById(MethodView):
@blueprint.response(200, SimpleTaskRelationSchema)
def delete(self, relation_id):
return TaskRelation.delete(relation_id)


@blueprint.route("/<task_id>/<relation_type>")
class TasksRelatedByIdAndRelationType(MethodView):
@blueprint.response(200, TasksRelatedSchema(many=True))
def get(self, task_id, relation_type):
try:
result = (
db.session.query(TaskRelation.id, Task)
.join(Task, Task.id == TaskRelation.second_task_id)
.filter(TaskRelation.first_task_id == task_id)
.filter(TaskRelation.type == relation_type)
.all()
)
return [{"id": rel_id, "task": task} for rel_id, task in result]
except DataError:
abort(404)
6 changes: 6 additions & 0 deletions bode/bode/resources/task_relations/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from bode.models.task_relation import RelationType
from bode.resources.base_schema import BaseSchema
from bode.resources.tasks.schemas import TaskSchema


class TaskRelationInputSchema(BaseSchema):
Expand All @@ -20,3 +21,8 @@ class SimpleTaskRelationSchema(BaseSchema):
first_task_id = fields.UUID()
second_task_id = fields.UUID()
type = fields.String()


class TasksRelatedSchema(BaseSchema):
id = fields.UUID(dump_only=True)
task = fields.Nested(TaskSchema)
15 changes: 15 additions & 0 deletions cabra/src/api/taskRelations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ITaskRelation, ITasksRelated } from "../types/taskRelation";

import axios from "axios";

export const getSubtasks = {
cacheKey: (id: string) => ["task-relations", id, "SUBTASK"],
run: (id: string) =>
axios.get<ITasksRelated[]>(`/task-relations/${id}/SUBTASK`),
};

export const createRelation = (data: Omit<ITaskRelation, "id">) =>
axios.post<ITaskRelation>("/task-relations", data);

export const deleteRelation = (id: string) =>
axios.delete<ITaskRelation>(`/task-relations/${id}`);
2 changes: 2 additions & 0 deletions cabra/src/pages/TaskDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import "twin.macro";

import { useNavigate, useParams } from "react-router-dom";

import { ArrowLeftIcon } from "@heroicons/react/solid";
import Layout from "./components/Layout";
import NavigationButton from "./components/NavigationButton";
Expand Down
11 changes: 10 additions & 1 deletion cabra/src/pages/TaskEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import "twin.macro";

import TaskForm, { TaskFormInputs } from "./components/TaskForm";
import { getTask, getTasks, updateTask } from "../api/tasks";
import tw, { styled } from "twin.macro";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate, useParams } from "react-router-dom";

import { ArrowLeftIcon } from "@heroicons/react/solid";
import Layout from "./components/Layout";
import NavigationButton from "./components/NavigationButton";
import SubtasksListEdit from "./components/SubtasksListEdit";
import { routeHelpers } from "../routes";

const Label = styled.label(tw`text-gray-50 font-bold`);

export default function TaskEditPage() {
const navigate = useNavigate();
const { id } = useParams() as { id: string };
Expand Down Expand Up @@ -45,8 +49,13 @@ export default function TaskEditPage() {
Go Back
</NavigationButton>
</div>

<TaskForm task={data.data} onSubmit={editTask.mutateAsync} />
<div tw="grid gap-4">
<div tw="w-full">
<Label>subtasks:</Label>
<SubtasksListEdit parentId={id} />
</div>
</div>
</div>
</Layout>
);
Expand Down
7 changes: 7 additions & 0 deletions cabra/src/pages/components/AddDependenceButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import tw, { styled } from "twin.macro";

const AddDependenceButton = styled.button(
tw`rounded-lg flex flex-row items-start h-5 py-1 px-4 w-16 bg-tertiary font-bold text-lg color[#787DAB]`
);

export default AddDependenceButton;
30 changes: 30 additions & 0 deletions cabra/src/pages/components/MiniTaskDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import "twin.macro";

import NavigationButton from "./NavigationButton";
import { TrashIcon } from "@heroicons/react/solid";

interface Props {
title: string;
taskId: string;
onClickDelete: (taskId: string) => void;
}

export default function MiniTaskDelete({
title,
onClickDelete,
taskId,
}: Props) {
return (
<div tw="rounded-xl w-full bg-white shadow-2xl text-blue-800 p-1.5">
<p tw="flex items-center text-sm">
{title}
<NavigationButton
tw="text-stone-50 bg-red-500 flex ml-auto"
onClick={() => onClickDelete(taskId)}
>
<TrashIcon height={10} width={10} />
</NavigationButton>
</p>
</div>
);
}
85 changes: 85 additions & 0 deletions cabra/src/pages/components/SubtasksList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { getTask, getTasks, updateTask } from "../../api/tasks";
import tw, { styled } from "twin.macro";
import { useMutation, useQuery, useQueryClient } from "react-query";

import Checkbox from "./CheckBox";
import { ITask } from "../../types/task";
import { getSubtasks } from "../../api/taskRelations";
import { useState } from "react";

interface Props {
subtask: ITask;
parentId: string;
}

interface PropsList {
parentId: string;
}

const Container = styled.div(tw`grid grid-cols-4 gap-3`);

const Subtask = ({ subtask, parentId }: Props) => {
const [errorMessage, setErrorMessage] = useState("");
const client = useQueryClient();
const editTask = useMutation((task: ITask) => updateTask(task.id, task), {
onSuccess: () => {
client.invalidateQueries(getTasks.cacheKey);
client.invalidateQueries(getTask.cacheKey(parentId));
client.invalidateQueries(getSubtasks.cacheKey(parentId));
},
});
const handleIsDoneChange = async () => {
try {
const updatedTask = {
...subtask,
isDone: !subtask.isDone,
};
await editTask.mutateAsync(updatedTask);
} catch (error) {
setErrorMessage(
"Something went wrong :C, It's not possible to uncheck the task."
);
}
};

return (
<div tw="rounded-xl bg-tertiary text-secondary p-1.5 grid">
<p tw="font-medium text-xs">{subtask.title}</p>
<p tw="place-self-end">
<Checkbox checked={subtask.isDone} onChange={handleIsDoneChange} />
</p>
{errorMessage && (
<p tw="flex items-center text-orange-500 pt-1">&nbsp;{errorMessage}</p>
)}
</div>
);
};

export default function SubtasksList({ parentId }: PropsList) {
const { data, isLoading, error } = useQuery(
getSubtasks.cacheKey(parentId),
() => getSubtasks.run(parentId)
);

if (isLoading) return <Container>Loading</Container>;
if (error) return <Container>Oops</Container>;
if (!data?.data) return <Container />;

const subtasks = data.data.slice().reverse();

if (subtasks.length == 0) {
return <Container>{"<No tasks>"}</Container>;
}

return (
<Container>
{subtasks.map((relation) => (
<Subtask
key={relation.id}
subtask={relation.task}
parentId={parentId}
/>
))}
</Container>
);
}
Loading

0 comments on commit 5cab778

Please sign in to comment.