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

[ITG-24] Create, view and delete subtasks. #25

Merged
merged 16 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
gregori0o marked this conversation as resolved.
Show resolved Hide resolved
)
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`space-x-2 space-y-3 grid grid-cols-4`);

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