From 1032b0e96841b6f6dad108d37acc33e7f631a2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1ria?= Date: Fri, 7 Jun 2024 13:37:55 +0200 Subject: [PATCH] Statistics table for each user and task (total time, count of annotation results). Missing count of lines (to be discussed). --- backend/scripts/stats.py | 90 ++--- backend/semant_annotation/db/crud_task.py | 20 +- .../semant_annotation/routes/task_routes.py | 24 +- .../semant_annotation/schemas/base_objects.py | 9 + frontend/src/css/app.scss | 43 +++ frontend/src/layouts/MainLayout.vue | 2 +- frontend/src/models.ts | 8 + frontend/src/pages/StatisticsPage.vue | 341 ++++++++++++------ 8 files changed, 373 insertions(+), 164 deletions(-) diff --git a/backend/scripts/stats.py b/backend/scripts/stats.py index 45d9c06..51f1f67 100644 --- a/backend/scripts/stats.py +++ b/backend/scripts/stats.py @@ -1,6 +1,6 @@ import logging -import requests +#import requests import argparse import json import sys @@ -182,47 +182,47 @@ def get_final_list(users, tasks, start_date, end_date, keypress_time, task_resul return list - -def main(): - args = parse_args() - keypress_time = int(args.keypress_time) - - login_url = urljoin(args.api_url, 'token') - user_url = urljoin(args.api_url, 'user/') - task_url = urljoin(args.api_url, 'task/task') - task_result_url = urljoin(args.api_url, 'task/results') - time_tracking_url = urljoin(args.api_url, 'time_tracking/time_tracking') - - session = requests.Session() - - session = get_session(login_url, session, args.login, args.password) - - users = query_api(user_url, session, None) - - tasks = query_api(task_url, session, None) - tasks = [item for item in tasks if item['active']] - - final_list = get_final_list(users, tasks, args.start_date, args.end_date, keypress_time, task_result_url, time_tracking_url, session) - field_names = [] - field_names_dict = max(final_list, key=len) - for key in field_names_dict: - field_names.append(key) - - with open(args.output + '.jsonl', 'w') as csvfile: - for item in final_list: - csvfile.write(json.dumps(item) + '\n') - - with open(args.output + '.csv', 'w') as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=field_names) - writer.writeheader() - writer.writerows(final_list) - - if args.export_blocks: - time_blocks_list = get_time_blocks_list(session, users, time_tracking_url, args.start_date, args.end_date) - with open(args.output + '_time_blocks.jsonl', 'w') as jsonfile: - for item in time_blocks_list: - jsonfile.write(json.dumps(item) + '\n') - - -if __name__ == '__main__': - main() +#def main(): +# args = parse_args() +# keypress_time = int(args.keypress_time) +# +# login_url = urljoin(args.api_url, 'token') +# user_url = urljoin(args.api_url, 'user/') +# task_url = urljoin(args.api_url, 'task/task') +# task_result_url = urljoin(args.api_url, 'task/results') +# time_tracking_url = urljoin(args.api_url, 'time_tracking/time_tracking') +# +# session = requests.Session() +# +# session = get_session(login_url, session, args.login, args.password) +# +# users = query_api(user_url, session, None) +# +# tasks = query_api(task_url, session, None) +# tasks = [item for item in tasks if item['active']] +# +# final_list = get_final_list(users, tasks, args.start_date, args.end_date, keypress_time, task_result_url, time_tracking_url, session) +# field_names = [] +# field_names_dict = max(final_list, key=len) +# for key in field_names_dict: +# field_names.append(key) +# +# with open(args.output + '.jsonl', 'w') as csvfile: +# for item in final_list: +# csvfile.write(json.dumps(item) + '\n') +# +# with open(args.output + '.csv', 'w') as csvfile: +# writer = csv.DictWriter(csvfile, fieldnames=field_names) +# writer.writeheader() +# writer.writerows(final_list) +# +# if args.export_blocks: +# time_blocks_list = get_time_blocks_list(session, users, time_tracking_url, args.start_date, args.end_date) +# with open(args.output + '_time_blocks.jsonl', 'w') as jsonfile: +# for item in time_blocks_list: +# jsonfile.write(json.dumps(item) + '\n') +# +# +#if __name__ == '__main__': +# main() +# \ No newline at end of file diff --git a/backend/semant_annotation/db/crud_task.py b/backend/semant_annotation/db/crud_task.py index a2350f1..2e25769 100644 --- a/backend/semant_annotation/db/crud_task.py +++ b/backend/semant_annotation/db/crud_task.py @@ -1,7 +1,7 @@ import logging from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import exc, select, or_, update +from sqlalchemy import exc, select, or_, update, func from .database import DBError from . import model from semant_annotation.schemas import base_objects @@ -71,7 +71,7 @@ async def get_task_instance_random(db: AsyncSession, task_id: UUID, result_count raise DBError(f'Failed fetching task instance from database.') -async def get_task_instance_results(db: AsyncSession, task_id: UUID, user_id: UUID=None, from_date: datetime=None, +async def get_task_instance_results(db: AsyncSession, task_id: UUID, page_size: int, user_id: UUID=None, from_date: datetime=None, to_date: datetime=None) -> base_objects.AnnotationTaskResult: try: async with db.begin(): @@ -83,30 +83,40 @@ async def get_task_instance_results(db: AsyncSession, task_id: UUID, user_id: UU stmt = stmt.where(model.AnnotationTaskResult.created_date >= from_date) if to_date: stmt = stmt.where(model.AnnotationTaskResult.created_date <= to_date) + result = await db.execute(stmt) db_task_instance_result = result.scalars().all() + return [base_objects.AnnotationTaskResult.model_validate(db_task_instance_result) for db_task_instance_result in db_task_instance_result] except exc.SQLAlchemyError as e: logging.error(str(e)) raise DBError(f'Failed fetching task instance result from database.') - async def get_task_instance_result_times(db: AsyncSession, task_id: UUID = None, user_id: UUID=None, from_date: datetime=None, - to_date: datetime=None) -> base_objects.SimplifiedAnnotationTaskResult: + to_date: datetime=None, result_type: base_objects.AnnotationResultType=None) -> base_objects.SimplifiedAnnotationTaskResult: try: async with db.begin(): - stmt = select(model.AnnotationTaskResult.start_time, + stmt = select(model.AnnotationTaskInstance.annotation_task_id, # Include annotation_task_id in the select clause + model.AnnotationTaskResult.start_time, model.AnnotationTaskResult.end_time, model.AnnotationTaskResult.result_type, model.AnnotationTaskResult.user_id) + + if task_id: stmt = stmt.join(model.AnnotationTaskInstance).where(model.AnnotationTaskInstance.annotation_task_id == task_id) + else: + stmt = stmt.join(model.AnnotationTaskInstance) + if user_id: stmt = stmt.where(model.AnnotationTaskResult.user_id == user_id) + if result_type: + stmt = stmt.where(model.AnnotationTaskResult.result_type == result_type) if from_date: stmt = stmt.where(model.AnnotationTaskResult.created_date >= from_date) if to_date: stmt = stmt.where(model.AnnotationTaskResult.created_date <= to_date) + result = await db.execute(stmt.distinct()) db_task_instance_result = result.all() return [base_objects.SimplifiedAnnotationTaskResult.model_validate(db_task_instance_result) for db_task_instance_result in db_task_instance_result] diff --git a/backend/semant_annotation/routes/task_routes.py b/backend/semant_annotation/routes/task_routes.py index 6cd79b9..689a20a 100644 --- a/backend/semant_annotation/routes/task_routes.py +++ b/backend/semant_annotation/routes/task_routes.py @@ -4,6 +4,7 @@ from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession +from scripts.stats import get_final_list from . import task_route @@ -53,6 +54,12 @@ async def delete_task(task_id: UUID, await crud_general.delete_obj(db, task_id, model.AnnotationTask) +@task_route.get("/types/", response_model=List[base_objects.AnnotationResultType], tags=["Task"]) +async def get_types( + user_token: TokenData = Depends(get_current_user), db: AsyncSession = Depends(get_async_session)): + return [type_.value for type_ in base_objects.AnnotationResultType] + + @task_route.post("/subtask", tags=["Task"]) async def new_subtask(subtask: base_objects.AnnotationSubtaskUpdate, user_token: TokenData = Depends(get_current_admin), db: AsyncSession = Depends(get_async_session)): @@ -109,17 +116,22 @@ async def update_task_instance_result(task_instance_result: base_objects.Annotat @task_route.post("/results", response_model=List[base_objects.AnnotationTaskResult], tags=["Task"]) async def get_task_instance_result(query: base_objects.AnnotationTaskResultQuery, user_token: TokenData = Depends(get_current_admin), db: AsyncSession = Depends(get_async_session)): - return await crud_task.get_task_instance_results(db, query.annotation_task_id, query.user_id, + + return await crud_task.get_task_instance_results(db, query.annotation_task_id, query.page_size, query.user_id, query.from_date, query.to_date) - @task_route.post("/result_times", response_model=List[base_objects.SimplifiedAnnotationTaskResult], tags=["Task"]) -async def get_task_instance_result_times(query: base_objects.AnnotationTaskResultQuery, +async def get_task_instance_result_times(query: base_objects.AnnotationTaskResultQueryStats, user_token: TokenData = Depends(get_current_user), db: AsyncSession = Depends(get_async_session)): + if not user_token.trusted_user and user_token.user_id != query.user_id: raise HTTPException(status_code=403, detail="You can only access your own statistics.") - return await crud_task.get_task_instance_result_times(db, query.annotation_task_id, query.user_id, - query.from_date, query.to_date) + + result = await crud_task.get_task_instance_result_times(db, query.annotation_task_id, query.user_id, + query.from_date, query.to_date, query.result_type) + return result + + async def get_image_path(image_id: UUID, task_id: UUID, make_dir: bool = True): path = os.path.join(config.UPLOADED_IMAGES_FOLDER, str(task_id)) @@ -129,7 +141,7 @@ async def get_image_path(image_id: UUID, task_id: UUID, make_dir: bool = True): @task_route.get("/image/{task_id}/{image_id}", tags=["Task"]) -async def get_image(task_id: UUID, image_id: UUID, +async def get_image(task_id: UUID, image_id: UUID, user_token: TokenData = Depends(get_current_user)): path = await get_image_path(image_id=image_id, task_id=task_id, make_dir=False) if not os.path.exists(path): diff --git a/backend/semant_annotation/schemas/base_objects.py b/backend/semant_annotation/schemas/base_objects.py index a7e0ac8..6590ec5 100644 --- a/backend/semant_annotation/schemas/base_objects.py +++ b/backend/semant_annotation/schemas/base_objects.py @@ -108,6 +108,7 @@ class SimplifiedAnnotationTaskResult(BaseModel): end_time: datetime result_type: AnnotationResultType user_id: UUID + annotation_task_id: UUID class Config: from_attributes = True @@ -138,6 +139,14 @@ class AnnotationTaskResultQuery(BaseModel): user_id: UUID = None +class AnnotationTaskResultQueryStats(BaseModel): + annotation_task_id: UUID = None + from_date: date = None + to_date: date = None + user_id: UUID = None + result_type: AnnotationResultType = None + + class TimeTrackingItemNew(BaseModel): id: UUID user_id: UUID diff --git a/frontend/src/css/app.scss b/frontend/src/css/app.scss index ecac98f..569655c 100644 --- a/frontend/src/css/app.scss +++ b/frontend/src/css/app.scss @@ -1 +1,44 @@ // app global css in SCSS form + +.my-sticky-header-column-table { + height: 500px; + overflow: auto; + + td:first-child { + background-color: $primary; + color: white; + z-index: 1; + position: sticky; + left: 0; + } + + tr th { + position: sticky; + z-index: 2; + background: $primary; + color: white; + top: 0; + } + + tr:first-child th:first-child { + z-index: 3; + } + + th:first-child { + position: sticky; + left: 0; + z-index: 3; + } + + tr:nth-child(2) th { + top: 48px; + } + + tr:first-child { + background-color: #d3d3d3; + } +} + +.q-page { + overflow: auto; +} \ No newline at end of file diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 1839b61..b668ca5 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -76,7 +76,7 @@ :class="{ 'drawer-item-selected': currentRoute.startsWith('/annotation_statistics') }" v-ripple clickable> - + Annotation statistics diff --git a/frontend/src/models.ts b/frontend/src/models.ts index 58b068a..9bf2080 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -8,6 +8,13 @@ export interface User { disabled: boolean } + +export enum AnnotationResultType { + NEW = 'new', + CORRECTION = 'correction', + REJECTED = 'rejected' +} + export interface UserWithPassword extends User { password: string } @@ -83,6 +90,7 @@ export class SimplifiedAnnotationTaskResult{ result_type = '' start_time = '' end_time = '' + annotation_task_id = '' } diff --git a/frontend/src/pages/StatisticsPage.vue b/frontend/src/pages/StatisticsPage.vue index ce3ebb7..4dc3acb 100644 --- a/frontend/src/pages/StatisticsPage.vue +++ b/frontend/src/pages/StatisticsPage.vue @@ -3,161 +3,272 @@ + v-model="selected_task" + :options="tasks" + label="Task" + clearable + option-label="name" + :display-value="`${selected_task ? selected_task.name : 'All'}`" + /> + v-model="selected_user" + :options="users" + label="User" + option-label="username" + :display-value="`${selected_user ? selected_user.username : 'All'}`" + clearable + /> + - - - - - - {{ rows }} + + + +