Skip to content

Commit

Permalink
Add clip table
Browse files Browse the repository at this point in the history
  • Loading branch information
vogelbam committed Jun 10, 2024
1 parent 515ba06 commit 1455389
Show file tree
Hide file tree
Showing 12 changed files with 482 additions and 2 deletions.
52 changes: 51 additions & 1 deletion back/src/whombat/filters/annotation_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from uuid import UUID

from soundevent import data
from sqlalchemy import Select
from sqlalchemy import Select, or_

from whombat import models
from whombat.filters import base
Expand All @@ -12,6 +12,7 @@
"AnnotationProjectFilter",
"DatasetFilter",
"AnnotationTaskFilter",
"SearchRecordingsFilter",
]


Expand Down Expand Up @@ -118,6 +119,25 @@ def filter(self, query: Select) -> Select:
)


class IsCompletedFilter(base.Filter):
"""Filter for tasks if rejected."""

eq: bool | None = None

def filter(self, query: Select) -> Select:
"""Filter the query."""
if self.eq is None:
return query

Check warning on line 130 in back/src/whombat/filters/annotation_tasks.py

View check run for this annotation

Codecov / codecov/patch

back/src/whombat/filters/annotation_tasks.py#L129-L130

Added lines #L129 - L130 were not covered by tests

return query.where(

Check warning on line 132 in back/src/whombat/filters/annotation_tasks.py

View check run for this annotation

Codecov / codecov/patch

back/src/whombat/filters/annotation_tasks.py#L132

Added line #L132 was not covered by tests
models.AnnotationTask.status_badges.any(
models.AnnotationStatusBadge.state
== data.AnnotationState.completed,
)
== self.eq,
)


class IsAssignedFilter(base.Filter):
"""Filter for tasks if assigned."""

Expand Down Expand Up @@ -206,11 +226,41 @@ def filter(self, query: Select) -> Select:
)


class SearchRecordingsFilter(base.Filter):
"""Filter recordings by the dataset they are in."""

search_recordings: str | None = None

def filter(self, query: Select) -> Select:
"""Filter the query."""

query = (

Check warning on line 237 in back/src/whombat/filters/annotation_tasks.py

View check run for this annotation

Codecov / codecov/patch

back/src/whombat/filters/annotation_tasks.py#L237

Added line #L237 was not covered by tests
query.join(
models.ClipAnnotation,
models.AnnotationTask.clip_annotation_id == models.ClipAnnotation.id,
)
.join(
models.Clip,
models.ClipAnnotation.clip_id == models.Clip.id,
)
.join(
models.Recording,
models.Recording.id == models.Clip.recording_id,
)
)
fields = [models.Recording.path]

Check warning on line 251 in back/src/whombat/filters/annotation_tasks.py

View check run for this annotation

Codecov / codecov/patch

back/src/whombat/filters/annotation_tasks.py#L251

Added line #L251 was not covered by tests

term = f"%{self.search_recordings}%"
return query.where(or_(*[field.ilike(term) for field in fields]))

Check warning on line 254 in back/src/whombat/filters/annotation_tasks.py

View check run for this annotation

Codecov / codecov/patch

back/src/whombat/filters/annotation_tasks.py#L253-L254

Added lines #L253 - L254 were not covered by tests


AnnotationTaskFilter = base.combine(
SearchRecordingsFilter,
assigned_to=AssignedToFilter,
pending=PendingFilter,
verified=IsVerifiedFilter,
rejected=IsRejectedFilter,
completed=IsCompletedFilter,
assigned=IsAssignedFilter,
annotation_project=AnnotationProjectFilter,
dataset=DatasetFilter,
Expand Down
2 changes: 2 additions & 0 deletions back/src/whombat/models/annotation_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ class AnnotationTask(Base):
clip: orm.Mapped[Clip] = orm.relationship(
init=False,
repr=False,
lazy="joined",
)
clip_annotation: orm.Mapped[ClipAnnotation] = orm.relationship(
back_populates="annotation_task",
cascade="all, delete-orphan",
init=False,
single_parent=True,
lazy="joined",
)
status_badges: orm.Mapped[list["AnnotationStatusBadge"]] = (
orm.relationship(
Expand Down
8 changes: 8 additions & 0 deletions back/src/whombat/schemas/annotation_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from soundevent.data import AnnotationState

from whombat.schemas.base import BaseSchema
from whombat.schemas.clip_annotations import ClipAnnotation
from whombat.schemas.clips import Clip
from whombat.schemas.users import SimpleUser

__all__ = [
Expand Down Expand Up @@ -50,6 +52,12 @@ class AnnotationTask(BaseSchema):
status_badges: list[AnnotationStatusBadge]
"""Status badges for the task."""

clip: Clip
"""Clip of the task."""

clip_annotation: ClipAnnotation
"""Clip annotation for the task."""


class AnnotationTaskUpdate(BaseModel):
"""Schema for updating a task."""
Expand Down
4 changes: 4 additions & 0 deletions front/src/api/annotation_tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export const AnnotationTaskFilterSchema = z.object({
assigned: z.boolean().optional(),
verified: z.boolean().optional(),
rejected: z.boolean().optional(),
completed: z.boolean().optional(),
assigned_to: UserSchema.optional(),
search_recordings: z.string().optional(),
});

export type AnnotationTaskFilter = z.input<typeof AnnotationTaskFilterSchema>;
Expand Down Expand Up @@ -92,7 +94,9 @@ export function registerAnnotationTasksAPI(
assigned__eq: params.assigned,
verified__eq: params.verified,
rejected__eq: params.rejected,
completed__eq: params.completed,
assigned_to__eq: params.assigned_to?.id,
search_recordings: params.search_recordings,
},
});
return AnnotationTaskPageSchema.parse(response.data);
Expand Down
24 changes: 24 additions & 0 deletions front/src/app/(base)/annotation_projects/detail/clips/page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.resizer {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 5px;
cursor: col-resize;
user-select: none;
touch-action: none;
}

.resizer.isResizing {
opacity: 1;
}

@media (hover: hover) {
.resizer {
opacity: 0;
}

*:hover > .resizer {
opacity: 1;
}
}
31 changes: 31 additions & 0 deletions front/src/app/(base)/annotation_projects/detail/clips/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";
import { notFound } from "next/navigation";
import { useContext } from "react";

import AnnotationProjectTaskClips from "@/components/annotation_projects/AnnotationProjectTaskClips";
import AnnotationProject from "../context";

import type { AnnotationTask } from "@/types";

import "./page.css";

function getAnnotationTaskLink(annotationTask: AnnotationTask): string {
return `detail/annotation/?annotation_task_uuid=${annotationTask.uuid}`;
}

export default function Page() {
const annotationProject = useContext(AnnotationProject);

if (annotationProject == null) {
return notFound();
}

return (
<div className="w-full">
<AnnotationProjectTaskClips
annotationProject={annotationProject}
getAnnotationTaskLink={getAnnotationTaskLink}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {

import Header from "@/components/Header";
import { H1 } from "@/components/Headings";
import { DatasetIcon, EditIcon, TagsIcon, TasksIcon } from "@/components/icons";
import { DatasetIcon, EditIcon, ClipsIcon, TagsIcon, TasksIcon } from "@/components/icons";
import Tabs from "@/components/Tabs";

import type { AnnotationProject } from "@/types";
Expand Down Expand Up @@ -39,6 +39,17 @@ export default function AnnotationProjectHeader({
);
},
},
{
id: "clips",
title: "Clips",
isActive: selectedLayoutSegment === "clips",
icon: <ClipsIcon className="w-5 h-5 align-middle"/>,
onClick: () => {
router.push(
`/annotation_projects/detail/clips/?${params.toString()}`,
);
},
},
{
id: "annotate",
title: "Annotate",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useMemo } from "react";

import AnnotationTaskTable from "@/components/annotation_tasks/AnnotationTaskTable";

import type {AnnotationProject, AnnotationTask} from "@/types";

export default function AnnotationProjectTaskClips({
annotationProject,
getAnnotationTaskLink: getAnnotationTaskLinkFn,
}: {
annotationProject: AnnotationProject;
getAnnotationTaskLink?: (annotationTask: AnnotationTask) => string;
}) {

const getAnnotationTaskLink = useMemo(() => {
if (getAnnotationTaskLinkFn == null) return undefined;

return (annotationTask: AnnotationTask) => {
const url = getAnnotationTaskLinkFn(annotationTask);
return `${url}&annotation_project_uuid=${annotationProject.uuid}`;
};
}, [getAnnotationTaskLinkFn, annotationProject.uuid]);
const filter = useMemo(() => ({ annotation_project: annotationProject }), [annotationProject]);

return (
<AnnotationTaskTable
filter={filter}
fixed={["annotation_project"]}
getAnnotationTaskLink={getAnnotationTaskLink}
// pathFormatter={pathFormatter} TODO: if there was a dataset column, the path could be formatted as in the recordings table
/>
);
}
69 changes: 69 additions & 0 deletions front/src/components/annotation_tasks/AnnotationTaskTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { AnnotationTaskFilter } from "@/api/annotation_tasks";
import type { AnnotationTask } from "@/types";
import useAnnotationTasks from "@/hooks/api/useAnnotationTasks";
import useAnnotationTaskTable from "@/hooks/useAnnotationTaskTable";
import Loading from "@/app/loading";
import Search from "@/components/inputs/Search";
import FilterPopover from "@/components/filters/FilterMenu";
import annotationTaskFilterDefs from "@/components/filters/annotation_tasks";
import FilterBar from "@/components/filters/FilterBar";
import Table from "@/components/tables/Table";
import Pagination from "@/components/lists/Pagination";

export default function AnnotationTaskTable({
filter,
fixed,
getAnnotationTaskLink,
pathFormatter,
}: {
filter: AnnotationTaskFilter;
fixed?: (keyof AnnotationTaskFilter)[];
getAnnotationTaskLink?: (annotationTask: AnnotationTask) => string;
pathFormatter?: (path: string) => string;
}) {
const annotationTasks = useAnnotationTasks({ filter, fixed });

const table = useAnnotationTaskTable({
data: annotationTasks.items,
getAnnotationTaskLink: getAnnotationTaskLink,
pathFormatter
});

if (annotationTasks.isLoading || annotationTasks.data == null) {
return <Loading />;
}

return (
<div className="flex flex-col gap-y-4">
<div className="flex flex-row justify-between space-x-4">
<div className="flex flex-row space-x-3 basis-1/2">
<div className="grow">
<Search
label="Search"
placeholder="Search recordings..."
value={annotationTasks.filter.get("search_recordings") ?? ""}
onChange={(value) =>
annotationTasks.filter.set("search_recordings", value as string)
}
/>
</div>
<FilterPopover
filter={annotationTasks.filter}
filterDef={annotationTaskFilterDefs}
/>
</div>
</div>
<FilterBar
filter={annotationTasks.filter}
total={annotationTasks.total}
filterDef={annotationTaskFilterDefs}
/>
<div className="w-full">
<div className="overflow-x-auto overflow-y-auto w-full max-h-screen rounded-md outline outline-1 outline-stone-200 dark:outline-stone-800">
<Table table={table}/>
</div>
</div>
<Pagination {...annotationTasks.pagination} />
</div>
);
}
Loading

0 comments on commit 1455389

Please sign in to comment.