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 }}
+
+
+
+
+
+
+ {{ column }}
+
+
+
+
+ {{ column.label }}
+
+
+
+