diff --git a/mephisto/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx b/mephisto/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx index f8832acb6..58a130b6b 100644 --- a/mephisto/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx +++ b/mephisto/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx @@ -10,9 +10,12 @@ import { useEffect } from "react"; import { Button, Col, Form, Row } from "react-bootstrap"; import { getQualifications, postQualification } from "requests/qualifications"; import "./ModalForm.css"; +import { getWorkerGrantedQualifications } from "requests/workers"; const BONUS_FOR_WORKER_ENABLED = true; const FEEDBACK_FOR_WORKER_ENABLED = true; +const QUALIFICATION_VALUE_MIN = 1; +const QUALIFICATION_VALUE_MAX = 10; const range = (start, end) => Array.from(Array(end + 1).keys()).slice(start); @@ -24,13 +27,14 @@ type ModalFormProps = { }; function ModalForm(props: ModalFormProps) { + const [ + workerGrantedQualifications, + setWorkerGrantedQualifications, + ] = React.useState({}); const [qualifications, setQualifications] = React.useState< - Array + QualificationType[] >(null); - const [ - getQualificationsloading, - setGetQualificationsloading, - ] = React.useState(false); + const [loading, setLoading] = React.useState(false); const [_, setCreateQualificationLoading] = React.useState(false); const onChangeAssign = (value: boolean) => { @@ -53,6 +57,17 @@ function ModalForm(props: ModalFormProps) { prevFormData.newQualificationValue = ""; } else { prevFormData.qualification = Number(value); + + const prevGrantedQualification = + workerGrantedQualifications[Number(value)]; + const prevGrantedQualificationValue = prevGrantedQualification?.value; + if (prevGrantedQualificationValue !== undefined) { + // Set to previous granted value for selected qualification + prevFormData.qualificationValue = prevGrantedQualificationValue; + } else { + // Set to default value + prevFormData.qualificationValue = QUALIFICATION_VALUE_MIN; + } } props.setData({ ...props.data, form: prevFormData }); @@ -131,17 +146,32 @@ function ModalForm(props: ModalFormProps) { requestQualifications(); }; + const onGetWorkerGrantedQualificationsSuccess = ( + grantedQualifications: GrantedQualificationType[] + ) => { + const _workerGrantedQualifications = {}; + + grantedQualifications.forEach((gq: GrantedQualificationType) => { + _workerGrantedQualifications[gq.qualification_id] = gq; + }); + setWorkerGrantedQualifications(_workerGrantedQualifications); + }; + const requestQualifications = () => { let params; if (props.data.type === ReviewType.REJECT) { params = { worker_id: props.workerId }; } - getQualifications( - setQualifications, - setGetQualificationsloading, - onError, - params + getQualifications(setQualifications, setLoading, onError, params); + }; + + const requestWorkerGrantedQualifications = () => { + getWorkerGrantedQualifications( + props.workerId, + onGetWorkerGrantedQualificationsSuccess, + setLoading, + onError ); }; @@ -156,12 +186,14 @@ function ModalForm(props: ModalFormProps) { // Effiects useEffect(() => { + requestWorkerGrantedQualifications(); + if (qualifications === null) { requestQualifications(); } }, []); - if (getQualificationsloading) { + if (loading) { return; } @@ -201,9 +233,20 @@ function ModalForm(props: ModalFormProps) { {qualifications && qualifications.map((q: QualificationType) => { + const prevGrantedQualification = + workerGrantedQualifications[q.id]; + const prevGrantedQualificationValue = + prevGrantedQualification?.value; + + let nameSuffix = ""; + if (prevGrantedQualificationValue !== undefined) { + nameSuffix = ` (granted value: ${prevGrantedQualificationValue})`; + } + const qualificationName = `${q.name}${nameSuffix}`; + return ( ); })} @@ -218,7 +261,10 @@ function ModalForm(props: ModalFormProps) { onChangeAssignQualificationValue(e.target.value) } > - {range(1, 10).map((i) => { + {range( + QUALIFICATION_VALUE_MIN, + QUALIFICATION_VALUE_MAX + ).map((i) => { return ; })} diff --git a/mephisto/review_app/client/src/requests/workers.ts b/mephisto/review_app/client/src/requests/workers.ts index 25b5b06b7..3d95989d5 100644 --- a/mephisto/review_app/client/src/requests/workers.ts +++ b/mephisto/review_app/client/src/requests/workers.ts @@ -16,7 +16,7 @@ export function postWorkerBlock( data: { [key: string]: string | number | number[] }, abortController?: AbortController ) { - const url = generateURL(urls.server.workersBlock(id), null, null); + const url = generateURL(urls.server.workersBlock, [id], null); makeRequest( "POST", @@ -29,3 +29,24 @@ export function postWorkerBlock( abortController ); } + +export function getWorkerGrantedQualifications( + id: number, + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + abortController?: AbortController +) { + const url = generateURL(urls.server.workerGrantedQualifications, [id]); + + makeRequest( + "GET", + url, + null, + (data) => setDataAction(data.granted_qualifications), + setLoadingAction, + setErrorsAction, + "getWorkerGrantedQualifications error:", + abortController + ); +} diff --git a/mephisto/review_app/client/src/types/qualifications.d.ts b/mephisto/review_app/client/src/types/qualifications.d.ts index 994f5cec1..713a3f30d 100644 --- a/mephisto/review_app/client/src/types/qualifications.d.ts +++ b/mephisto/review_app/client/src/types/qualifications.d.ts @@ -8,3 +8,14 @@ declare type QualificationType = { id: number; name: string; }; + +declare type GrantedQualificationType = { + worker_id: number; + qualification_id: number; + value: number; + granted_at: string; +}; + +declare type WorkerGrantedQualificationsType = { + [key: number]: GrantedQualificationType; +}; diff --git a/mephisto/review_app/client/src/urls.ts b/mephisto/review_app/client/src/urls.ts index efd89b14b..2e1e2d384 100644 --- a/mephisto/review_app/client/src/urls.ts +++ b/mephisto/review_app/client/src/urls.ts @@ -36,6 +36,8 @@ const urls = { API_URL + `/api/units/${id}/static/${filename}`, unitsOutputsFileByFieldname: (id, fieldname) => API_URL + `/api/units/${id}/static/fieldname/${fieldname}`, + workerGrantedQualifications: (id) => + API_URL + `/api/workers/${id}/qualifications`, workersBlock: (id) => API_URL + `/api/workers/${id}/block`, }, }; diff --git a/mephisto/review_app/server/README.md b/mephisto/review_app/server/README.md index 98ac1f326..f674a0d78 100644 --- a/mephisto/review_app/server/README.md +++ b/mephisto/review_app/server/README.md @@ -256,6 +256,26 @@ Permanently block a worker --- +`GET /api/workers/{id}/qualifications` + +Get list of all granted qualifications for a worker + +``` +{ + "granted_qualifications": [ + { + "worker_id": , + "qualification_id": , + "value": , + "granted_at": , // maps to `unit_review.created_at` column + } + ], + ... // more granted qualifications +} +``` + +--- + `GET /api/stats?{task_id=}{worker_id=}{since=}{limit=}` Get stats of (recent) approvals. Either `task_id` or `worker_id` (or both) must be present. diff --git a/mephisto/review_app/server/api/views/__init__.py b/mephisto/review_app/server/api/views/__init__.py index 21bcfe590..6e90ade3c 100644 --- a/mephisto/review_app/server/api/views/__init__.py +++ b/mephisto/review_app/server/api/views/__init__.py @@ -24,3 +24,4 @@ from .units_soft_reject_view import UnitsSoftRejectView from .units_view import UnitsView from .worker_block_view import WorkerBlockView +from .worker_granted_qualifications_view import WorkerGrantedQualificationsView diff --git a/mephisto/review_app/server/api/views/qualification_workers_view.py b/mephisto/review_app/server/api/views/qualification_workers_view.py index e1f5e9e9a..7cb1a6cd4 100644 --- a/mephisto/review_app/server/api/views/qualification_workers_view.py +++ b/mephisto/review_app/server/api/views/qualification_workers_view.py @@ -66,7 +66,7 @@ def _find_unit_reviews( class QualificationWorkersView(MethodView): - def get(self, qualification_id) -> dict: + def get(self, qualification_id: int) -> dict: """Get list of all bearers of a qualification.""" task_id = request.args.get("task_id") diff --git a/mephisto/review_app/server/api/views/unit_data_static_by_field_name_view.py b/mephisto/review_app/server/api/views/unit_data_static_by_field_name_view.py index 97841c592..7eb79c017 100644 --- a/mephisto/review_app/server/api/views/unit_data_static_by_field_name_view.py +++ b/mephisto/review_app/server/api/views/unit_data_static_by_field_name_view.py @@ -20,7 +20,7 @@ class UnitDataStaticByFieldNameView(MethodView): @staticmethod def _get_filename_by_fieldname(agent: Agent, fieldname: str) -> Union[str, None]: unit_parsed_data = agent.state.get_parsed_data() - outputs = unit_parsed_data.get("outputs", {}) + outputs = unit_parsed_data.get("outputs") or {} # In case if there is outdated code that returns `final_submission` # under `outputs` key, we should use the value in side `final_submission` if "final_submission" in outputs: diff --git a/mephisto/review_app/server/api/views/unit_data_static_view.py b/mephisto/review_app/server/api/views/unit_data_static_view.py index 01cf4eaf2..474b6d8c2 100644 --- a/mephisto/review_app/server/api/views/unit_data_static_view.py +++ b/mephisto/review_app/server/api/views/unit_data_static_view.py @@ -22,7 +22,7 @@ def _get_filename_by_original_name(unit: Unit, filename: str) -> Union[str, None agent: Agent = unit.get_assigned_agent() if agent: unit_parsed_data = agent.state.get_parsed_data() - outputs = unit_parsed_data.get("outputs", {}) + outputs = unit_parsed_data.get("outputs") or {} # In case if there is outdated code that returns `final_submission` # under `outputs` key, we should use the value in side `final_submission` if "final_submission" in outputs: diff --git a/mephisto/review_app/server/api/views/units_details_view.py b/mephisto/review_app/server/api/views/units_details_view.py index fc0227ea0..92ecee643 100644 --- a/mephisto/review_app/server/api/views/units_details_view.py +++ b/mephisto/review_app/server/api/views/units_details_view.py @@ -55,8 +55,8 @@ def get(self) -> dict: task_run: TaskRun = unit.get_task_run() has_task_source_review = bool(task_run.args.get("blueprint").get("task_source_review")) - inputs = unit_data.get("data", {}).get("inputs", {}) - outputs = unit_data.get("data", {}).get("outputs", {}) + inputs = unit_data.get("data", {}).get("inputs") or {} + outputs = unit_data.get("data", {}).get("outputs") or {} # In case if there is outdated code that returns `final_submission` # under `inputs` and `outputs` keys, we should use the value in side `final_submission` diff --git a/mephisto/review_app/server/api/views/worker_granted_qualifications_view.py b/mephisto/review_app/server/api/views/worker_granted_qualifications_view.py new file mode 100644 index 000000000..a0f259b8a --- /dev/null +++ b/mephisto/review_app/server/api/views/worker_granted_qualifications_view.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +# Copyright (c) Meta Platforms and its affiliates. +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +from typing import List + +from flask import current_app as app +from flask.views import MethodView + +from mephisto.abstractions.databases.local_database import LocalMephistoDB +from mephisto.abstractions.databases.local_database import StringIDRow +from mephisto.data_model.worker import Worker + + +def _find_granted_qualifications(db: LocalMephistoDB, worker_id: str) -> List[StringIDRow]: + """Return the granted qualifications in the database by the given worker id""" + + with db.table_access_condition: + conn = db._get_connection() + c = conn.cursor() + c.execute( + """ + SELECT + gq.value, + gq.worker_id, + gq.qualification_id, + gq.granted_qualification_id, + ur.created_at AS granted_at + FROM granted_qualifications AS gq + LEFT JOIN ( + SELECT + updated_qualification_id, + created_at + FROM unit_review + ORDER BY created_at DESC + /* + We’re retrieving unit_review data only + for the latest update of the worker-qualification pair. + */ + LIMIT 1 + ) AS ur ON ur.updated_qualification_id = gq.qualification_id + WHERE gq.worker_id = ?1 + """, + (worker_id,), + ) + rows = c.fetchall() + return rows + + +class WorkerGrantedQualificationsView(MethodView): + def get(self, worker_id: int) -> dict: + """Get list of all granted queslifications for a worker.""" + + worker: Worker = Worker.get(app.db, str(worker_id)) + app.logger.debug(f"Found Worker in DB: {worker}") + + db_granted_qualifications = _find_granted_qualifications(app.db, worker.db_id) + + app.logger.debug( + f"Found granted qualifications for worker {worker_id} in DB: " + f"{list(db_granted_qualifications)}" + ) + + granted_qualifications = [] + for gq in db_granted_qualifications: + granted_qualifications.append( + { + "worker_id": gq["worker_id"], + "qualification_id": gq["qualification_id"], + "value": int(gq["value"]), + "granted_at": gq["worker_id"], # maps to `unit_review.created_at` column + }, + ) + + return { + "granted_qualifications": granted_qualifications, + } diff --git a/mephisto/review_app/server/urls.py b/mephisto/review_app/server/urls.py index 55cce13e6..1658f3d53 100644 --- a/mephisto/review_app/server/urls.py +++ b/mephisto/review_app/server/urls.py @@ -86,6 +86,12 @@ def init_urls(app: Flask): "/api/workers//block", view_func=api_views.WorkerBlockView.as_view("worker_block"), ) + app.add_url_rule( + "/api/workers//qualifications", + view_func=api_views.WorkerGrantedQualificationsView.as_view( + "worker_granted_qualifications", + ), + ) app.add_url_rule( "/api/stats", view_func=api_views.StatsView.as_view("stats"),