From 16e6e5dbdb85065b0449b495004b22b51772c1fe Mon Sep 17 00:00:00 2001 From: Paul Abumov Date: Fri, 1 Sep 2023 21:34:00 -0400 Subject: [PATCH] [WIP] Task review app - added modal components, updated /stats endpoint --- .../review_app/client/package-lock.json | 1 + .../client/review_app/client/package.json | 1 + .../client/review_app/client/src/App/App.tsx | 2 - .../review.ts} | 8 +- .../QualificationsPage/QualificationsPage.tsx | 87 -------- .../src/pages/TaskPage/Header/Header.css | 46 +++++ .../src/pages/TaskPage/Header/Header.tsx | 53 +++++ .../pages/TaskPage/ModalForm/ModalForm.css | 19 ++ .../pages/TaskPage/ModalForm/ModalForm.tsx | 189 ++++++++++++++++++ .../TaskPage/ReviewModal/ReviewModal.css | 44 ++++ .../TaskPage/ReviewModal/ReviewModal.tsx | 77 +++++++ .../client/src/pages/TaskPage/TaskPage.tsx | 121 ++++++----- .../client/src/pages/TaskPage/modalData.tsx | 61 ++++++ .../client/src/pages/TasksPage/TasksPage.tsx | 2 +- .../client/src/requests/makeRequest.ts | 25 +-- .../client/src/requests/mockResponses.ts | 114 +++++++++++ .../client/src/requests/qualifications.ts | 93 +++++++++ .../review_app/client/src/requests/stats.ts | 32 +++ .../review_app/client/src/requests/tasks.ts | 23 +++ .../review_app/client/src/requests/units.ts | 120 +++++++++++ .../review_app/client/src/requests/workers.ts | 33 +++ .../client/src/types/reviewModal.d.ts | 26 +++ .../review_app/client/src/types/units.d.ts | 25 +++ mephisto/client/review_app/client/src/urls.ts | 20 +- .../review_app/server/api/views/__init__.py | 2 +- .../{worker_stats_view.py => stats_view.py} | 56 ++++-- .../review_app/server/api/views/units_view.py | 2 +- mephisto/client/review_app/server/urls.py | 14 +- 28 files changed, 1101 insertions(+), 195 deletions(-) rename mephisto/client/review_app/client/src/{pages/QualificationsPage/QualificationsPage.css => consts/review.ts} (64%) delete mode 100644 mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.tsx create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.css create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.tsx create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.css create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.css create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.tsx create mode 100644 mephisto/client/review_app/client/src/pages/TaskPage/modalData.tsx create mode 100644 mephisto/client/review_app/client/src/requests/mockResponses.ts create mode 100644 mephisto/client/review_app/client/src/requests/stats.ts create mode 100644 mephisto/client/review_app/client/src/requests/units.ts create mode 100644 mephisto/client/review_app/client/src/requests/workers.ts create mode 100644 mephisto/client/review_app/client/src/types/reviewModal.d.ts create mode 100644 mephisto/client/review_app/client/src/types/units.d.ts rename mephisto/client/review_app/server/api/views/{worker_stats_view.py => stats_view.py} (68%) diff --git a/mephisto/client/review_app/client/package-lock.json b/mephisto/client/review_app/client/package-lock.json index 39d0d5229..2315afc83 100644 --- a/mephisto/client/review_app/client/package-lock.json +++ b/mephisto/client/review_app/client/package-lock.json @@ -10,6 +10,7 @@ "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "bootstrap": "^5.3.1", + "lodash": "^4.17.21", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.2.0", diff --git a/mephisto/client/review_app/client/package.json b/mephisto/client/review_app/client/package.json index d78be64b9..4eea33686 100644 --- a/mephisto/client/review_app/client/package.json +++ b/mephisto/client/review_app/client/package.json @@ -6,6 +6,7 @@ "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "bootstrap": "^5.3.1", + "lodash": "^4.17.21", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.2.0", diff --git a/mephisto/client/review_app/client/src/App/App.tsx b/mephisto/client/review_app/client/src/App/App.tsx index 8f9e99f83..8a783e885 100644 --- a/mephisto/client/review_app/client/src/App/App.tsx +++ b/mephisto/client/review_app/client/src/App/App.tsx @@ -7,7 +7,6 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/js/bootstrap.bundle.min'; import HomePage from 'pages/HomePage/HomePage'; -import QualificationsPage from 'pages/QualificationsPage/QualificationsPage'; import TaskPage from 'pages/TaskPage/TaskPage'; import TasksPage from 'pages/TasksPage/TasksPage'; import * as React from 'react'; @@ -23,7 +22,6 @@ function App() { } /> } /> } /> - } /> ); diff --git a/mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.css b/mephisto/client/review_app/client/src/consts/review.ts similarity index 64% rename from mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.css rename to mephisto/client/review_app/client/src/consts/review.ts index 2c29efa04..285d11b91 100644 --- a/mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.css +++ b/mephisto/client/review_app/client/src/consts/review.ts @@ -5,6 +5,8 @@ */ -.qualifications { - -} +export const ReviewType = { + APPROVE: "approve", + REJECT: "reject", + SOFT_REJECT: "soft-reject", +}; diff --git a/mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.tsx b/mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.tsx deleted file mode 100644 index 68b375b9d..000000000 --- a/mephisto/client/review_app/client/src/pages/QualificationsPage/QualificationsPage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) Facebook, Inc. 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. - */ - -import * as React from 'react'; -import { useEffect } from 'react'; -import { getQualifications } from 'requests/qualifications'; -import './QualificationsPage.css'; - - -const STORAGE_APPROVE_QIALIFICATION_ID_KEY: string = 'approveQualificationID'; -const STORAGE_REJECT_QIALIFICATION_ID_KEY: string = 'rejectQualificationID'; - - -function QualificationsPage() { - const { localStorage } = window; - - const [qualifications, setQualifications] = React.useState>(null); - const [approveQualificationID, setApproveQualificationID] = React.useState(null); - const [rejectQualificationID, setRejectQualificationID] = React.useState(null); - const [loading, setLoading] = React.useState(false); - const [errors, setErrors] = React.useState(null); - - const localStorageApproveQualificationID = localStorage.getItem( - STORAGE_APPROVE_QIALIFICATION_ID_KEY - ); - const localStorageRejectQualificationID = localStorage.getItem( - STORAGE_REJECT_QIALIFICATION_ID_KEY - ); - - if (!approveQualificationID && localStorageApproveQualificationID) { - setApproveQualificationID(Number(localStorageApproveQualificationID)); - } - - if (!rejectQualificationID && localStorageRejectQualificationID) { - setRejectQualificationID(Number(localStorageRejectQualificationID)); - } - - const getQualificationsIDs = (_qualifications: Array): Array => { - let ids = []; - _qualifications.map((q: Qualification) => ids.push(q.id)); - return ids; - } - - const getQualificationByID = ( - _qualifications: Array, id: number | string, - ): Qualification => { - for (let q of _qualifications) { - if (String(q.id) === String(id)) { - return q; - } - } - return null - } - - useEffect(() => { - if (qualifications === null) { - getQualifications(setQualifications, setLoading, setErrors, null); - } - }, []); - - return
- Qualifications: - - {errors && ( -
{errors.error}
- )} - - {qualifications && approveQualificationID in getQualificationsIDs(qualifications) && approveQualificationID ? ( -
- Approve qualification: {getQualificationByID(qualifications, approveQualificationID).name} -
- ) : null} - - {qualifications && rejectQualificationID in getQualificationsIDs(qualifications) && rejectQualificationID ? ( -
- Reject qualification: {getQualificationByID(qualifications, rejectQualificationID).name} -
- ) : null} - -
; -} - - -export default QualificationsPage; diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.css b/mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.css new file mode 100644 index 000000000..bef9d8f6a --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.css @@ -0,0 +1,46 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +.task-header { + max-width: 100%; + margin: 0; + background-color: rgba(236, 218, 223, 0.3); + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.task-header .logo { + display: flex; + align-items: center; + padding-bottom: 10px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.task-header .logo img { + max-width: 100%; +} + +.task-header .table tr { + border: transparent; +} + +.task-header .table tr.total td { + color: #a6a6a6; +} + +.task-header .table th, .task-header .table td { + background: transparent; + line-height: 0.8; +} + +.task-header .table .title b { + display: inline-block; + line-height: 2; + border-bottom: 1px solid grey; +} diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.tsx b/mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.tsx new file mode 100644 index 000000000..63d101254 --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/Header/Header.tsx @@ -0,0 +1,53 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +import * as React from 'react'; +import { Col, Container, Row, Table } from 'react-bootstrap'; +import logo from 'static/images/logo.svg'; +import './Header.css'; + + +function Header() { + return + + + logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ReviewedApprovedSoft-RejectedRejected
worker15/2516 (80%)1 (5%)3 (15%)
Total64/256186 (78%)23 (7%)56 (17%)
+ +
+
; +} + + +export default Header; diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.css b/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.css new file mode 100644 index 000000000..044e098ac --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.css @@ -0,0 +1,19 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +.review-form >* input, +.review-form >* select, +.review-form >* textarea { + border: 1px solid black; +} + +.review-form .second-line { + margin-top: 10px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + padding-left: 25px; +} diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx b/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx new file mode 100644 index 000000000..3b3bb1712 --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx @@ -0,0 +1,189 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +import * as React from 'react'; +import { useEffect } from 'react'; +import { Col, Form, Row } from 'react-bootstrap'; +import { getQualifications } from 'requests/qualifications'; +import './ModalForm.css'; + + +const range = (start, end) => Array.from(Array(end + 1).keys()).slice(start); + + +type ModalFormProps = { + data: ModalDataType; + setData: React.Dispatch>; +}; + + +function ModalForm(props: ModalFormProps) { + const [qualifications, setQualifications] = React.useState>(null); + const [loading, setLoading] = React.useState(false); + const [errors, setErrors] = React.useState(null); + + const onChangeAssign = (value: boolean) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.checkboxAssign = value; + props.setData({...props.data, form: prevFormData}) + }; + + const onChangeAssignQualification = (id: string) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.qualification = Number(id); + props.setData({...props.data, form: prevFormData}) + }; + + const onChangeAssignQualificationValue = (value: string) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.qualificationValue = Number(value); + props.setData({...props.data, form: prevFormData}) + }; + + const onChangeGiveTips = (value: boolean) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.checkboxGiveTips = value; + props.setData({...props.data, form: prevFormData}); + }; + + const onChangeTips = (value: string) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.tips = Number(value); + props.setData({...props.data, form: prevFormData}) + }; + + const onChangeBanWorker = (value: boolean) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.checkboxBanWorker = value; + props.setData({...props.data, form: prevFormData}); + }; + + const onChangeWriteComment = (value: boolean) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.checkboxComment = value; + props.setData({...props.data, form: prevFormData}); + }; + + const onChangeComment = (value: string) => { + let prevFormData: FormType = Object(props.data.form); + prevFormData.comment = value; + props.setData({...props.data, form: prevFormData}); + }; + + // Effiects + useEffect(() => { + if (qualifications === null) { + getQualifications(setQualifications, setLoading, setErrors, null); + } + }, []); + + return
{e.preventDefault();}}> + onChangeAssign(!props.data.form.checkboxAssign)} + /> + + {props.data.form.checkboxAssign && ( + + + onChangeAssignQualification(e.target.value)} + > + + {qualifications && qualifications.map((q: Qualification) => { + return ; + })} + + + + onChangeAssignQualificationValue(e.target.value)} + > + {range(1, 20).map((i) => { + return ; + })} + + + + )} + +
+ + {props.data.form.checkboxGiveTips !== undefined && (<> + onChangeGiveTips(!props.data.form.checkboxGiveTips)} + /> + + {props.data.form.checkboxGiveTips && ( + + + onChangeTips(e.target.value)} + /> + + + Amount (cents) + + + )} + +
+ )} + + {props.data.form.checkboxBanWorker !== undefined && (<> + onChangeBanWorker(!props.data.form.checkboxBanWorker)} + /> + +
+ )} + + onChangeWriteComment(!props.data.form.checkboxComment)} + /> + + {props.data.form.checkboxComment && ( + + + onChangeComment(e.target.value)} + /> + + + )} + ; +} + + +export default ModalForm; diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.css b/mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.css new file mode 100644 index 000000000..f426a8b4e --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.css @@ -0,0 +1,44 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +.review-modal .modal-dialog .modal-header { + background-color: #ecdadf; + display: flex; + justify-content: center; + padding: 5px; +} + +.review-modal .modal-dialog .modal-header .modal-title { + font-size: 26px; +} + +/* Body */ +.review-modal .modal-dialog .modal-content { + border-radius: initial; +} + +/* Footer */ +.review-modal .modal-dialog .modal-content .modal-footer .review-buttons { + width: 100%; + display: flex; + justify-content: space-between; +} + +.review-modal .modal-dialog .modal-content .modal-footer .review-buttons .btn-cancel-button { + text-decoration: none; + color: grey; + border: none; +} + +.review-modal .modal-dialog .modal-content .modal-footer .apply-all-checkbox > label { + font-style: italic; + color: grey; + font-size: 14px; +} + +.review-modal .modal-dialog .modal-content .modal-footer .apply-all-checkbox > input { + border: 1px solid black; +} diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.tsx b/mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.tsx new file mode 100644 index 000000000..befdf874e --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/ReviewModal/ReviewModal.tsx @@ -0,0 +1,77 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +import { ReviewType } from 'consts/review'; +import * as React from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import ModalForm from '../ModalForm/ModalForm'; +import './ReviewModal.css'; + + +const ReviewTypeButtonClassMapping = { + [ReviewType.APPROVE]: "success", + [ReviewType.SOFT_REJECT]: "warning", + [ReviewType.REJECT]: "danger", +}; + + +type ReviewModalProps = { + data: ModalDataType; + setData: React.Dispatch>; + show: boolean; + setShow: React.Dispatch>; + onSubmit: Function; +}; + + +function ReviewModal(props: ReviewModalProps) { + const onModalClose = () => { + props.setShow(!props.show); + }; + + const onChangeApplyToNext = (value: boolean) => { + props.setData({...props.data, applyToNext: value}); + }; + + return props.show && + + {props.data.title} + + + + + + + +
+ + +
+
+ onChangeApplyToNext(!props.data.applyToNext)} + /> + +
+
; +} + + +export default ReviewModal; diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/TaskPage.tsx b/mephisto/client/review_app/client/src/pages/TaskPage/TaskPage.tsx index 01ce1d2f7..7dd4162e3 100644 --- a/mephisto/client/review_app/client/src/pages/TaskPage/TaskPage.tsx +++ b/mephisto/client/review_app/client/src/pages/TaskPage/TaskPage.tsx @@ -4,63 +4,86 @@ * LICENSE file in the root directory of this source tree. */ - +import cloneDeep from 'lodash/cloneDeep'; import * as React from 'react'; -import { Button, Col, Container, Form, Row, Table } from 'react-bootstrap'; +import { useEffect } from 'react'; +import { Button } from 'react-bootstrap'; +import { useParams } from 'react-router-dom'; +import { getUnits } from 'requests/units'; +import Header from './Header/Header'; +import { + APPROVE_MODAL_DATA_STATE, + REJECT_MODAL_DATA_STATE, + SOFT_REJECT_MODAL_DATA_STATE, +} from './modalData'; +import ReviewModal from './ReviewModal/ReviewModal'; import './TaskPage.css'; -import logo from 'static/images/logo.svg'; + + +type ParamsType = { + id: string; +}; function TaskPage() { + const params = useParams(); + + const [units, setUnits] = React.useState>(null); + const [loading, setLoading] = React.useState(false); + const [errors, setErrors] = React.useState(null); + + const [modalShow, setModalShow] = React.useState(false); + const [modalData, setModalData] = React.useState( + cloneDeep(APPROVE_MODAL_DATA_STATE) + ); + + const onApproveClick = () => { + setModalShow(true); + setModalData(cloneDeep(APPROVE_MODAL_DATA_STATE)); + }; + + const onSoftRejectClick = () => { + setModalShow(true); + setModalData(cloneDeep(SOFT_REJECT_MODAL_DATA_STATE)); + }; + + const onRejectClick = () => { + setModalShow(true); + setModalData(cloneDeep(REJECT_MODAL_DATA_STATE)); + }; + + const onModalSubmit = () => { + setModalShow(false); + console.log('Data:', modalData); + }; + + // Effects + useEffect(() => { + if (units === null) { + getUnits(setUnits, setLoading, setErrors, {task_id: params.id}); + } + }, []); + + if (units === null) { + return null; + } + return
- - - - logo - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ReviewedApprovedSoft-RejectedRejected
worker15/2516 (80%)1 (5%)3 (15%)
Total64/256186 (78%)23 (7%)56 (17%)
- -
-
+
+
- - - -
- - + + +
+ +
; } diff --git a/mephisto/client/review_app/client/src/pages/TaskPage/modalData.tsx b/mephisto/client/review_app/client/src/pages/TaskPage/modalData.tsx new file mode 100644 index 000000000..dbfcccecb --- /dev/null +++ b/mephisto/client/review_app/client/src/pages/TaskPage/modalData.tsx @@ -0,0 +1,61 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +import { ReviewType } from 'consts/review'; + + +export const APPROVE_MODAL_DATA_STATE: ModalDataType = { + applyToNext: false, + buttonCancel: "Cancel", + buttonSubmit: "Approve", + form: { + checkboxAssign: false, + checkboxComment: false, + checkboxGiveTips: false, + comment: '', + qualification: null, + qualificationValue: 1, + tips: null, + }, + title: "Approve Item", + type: ReviewType.APPROVE, +}; + + +export const SOFT_REJECT_MODAL_DATA_STATE: ModalDataType = { + applyToNext: false, + buttonCancel: "Cancel", + buttonSubmit: "Soft-Reject", + form: { + checkboxAssign: false, + checkboxComment: false, + checkboxGiveTips: false, + comment: '', + qualification: null, + qualificationValue: 1, + tips: null, + }, + title: "Soft-Reject Item", + type: ReviewType.SOFT_REJECT, +}; + + +export const REJECT_MODAL_DATA_STATE: ModalDataType = { + applyToNext: false, + buttonCancel: "Cancel", + buttonSubmit: "Reject", + title: "Reject Item", + type: ReviewType.REJECT, + form: { + checkboxAssign: false, + checkboxComment: false, + checkboxBanWorker: false, + comment: '', + qualification: null, + qualificationValue: 1, + tips: null, + } +}; diff --git a/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.tsx b/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.tsx index 17222842c..18a3a9be9 100644 --- a/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.tsx +++ b/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.tsx @@ -26,7 +26,7 @@ function TasksPage() { const onTaskClick = (id: number) => { localStorage.setItem(STORAGE_TASK_ID_KEY, String(id)); - navigate(urls.client.qualifications); + navigate(urls.client.task(id)); } useEffect(() => { diff --git a/mephisto/client/review_app/client/src/requests/makeRequest.ts b/mephisto/client/review_app/client/src/requests/makeRequest.ts index 66ba4d38d..20e665adf 100644 --- a/mephisto/client/review_app/client/src/requests/makeRequest.ts +++ b/mephisto/client/review_app/client/src/requests/makeRequest.ts @@ -4,32 +4,11 @@ * LICENSE file in the root directory of this source tree. */ - import { Status } from 'consts/http'; -import urls from 'urls'; +import { MOCK_RESPONSES_DATA } from './mockResponses'; const MOCK_RESPONSES = process.env.REACT_APP__MOCK_RESPONSES; -const MOCK_RESPONSES_DATA = { - [urls.server.tasks]: { - "tasks": [ - { - "id": 1, - "name": 'task1', - "is_reviewed": false, - "unit_count": 3, - "created_at": '2023-08-28T12:00:56', - }, - { - "id": 2, - "name": 'task2', - "is_reviewed": true, - "unit_count": 10, - "created_at": '2023-08-28T12:00:56', - } - ], - } -} function makeRequest( @@ -44,7 +23,7 @@ function makeRequest( setNotFoundErrorsAction?: SetRequestErrorsActionType, ) { if (MOCK_RESPONSES === 'true') { - const mockData = MOCK_RESPONSES_DATA[url] + const mockData = MOCK_RESPONSES_DATA[url]; if (mockData !== undefined) { setDataAction(mockData); return diff --git a/mephisto/client/review_app/client/src/requests/mockResponses.ts b/mephisto/client/review_app/client/src/requests/mockResponses.ts new file mode 100644 index 000000000..c02662062 --- /dev/null +++ b/mephisto/client/review_app/client/src/requests/mockResponses.ts @@ -0,0 +1,114 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +import urls from 'urls'; + + +export const MOCK_RESPONSES_DATA = { + [urls.server.tasks]: { + "tasks": [ + { + "id": 1, + "name": 'task1', + "is_reviewed": false, + "unit_count": 3, + "created_at": '2023-08-28T12:00:56', + }, + { + "id": 2, + "name": 'task2', + "is_reviewed": true, + "unit_count": 10, + "created_at": "2023-08-28T12:00:56", + }, + ], + }, + + [urls.server.qualifications]: { + "qualifications": [ + { + "id": 1, + "name": 'Great workers!', + }, + { + "id": 2, + "name": 'Rejected workers', + }, + { + "id": 3, + "name": 'Some other qualification', + }, + ], + }, + + [`${urls.server.units}?task_id=1`]: { + "units": [ + { + "id": 1, + "worker_id": 1, + "task_id": 1, + "pay_amount": 10, + "status": "completed", + "creation_date": "2023-08-28T12:00:56", + "results": { + "start": 1693239305.1141467, + "end": 1693239999.1141467, + "input_preview": null, + "output_preview": null, + }, + "review": { + "tips": 5, + "feedback": null, + }, + }, + { + "id": 2, + "worker_id": 2, + "task_id": 1, + "pay_amount": 11, + "status": "completed", + "creation_date": "2023-08-28T12:00:56", + "results": { + "start": 1693239305.1141467, + "end": 1693239999.1141467, + "input_preview": null, + "output_preview": null, + }, + "review": { + "tips": 6, + "feedback": null, + }, + }, + ], + }, + + [`${urls.server.stats}?task_id=1`]: { + "stats": { + "total_count": 3, + "reviewed_count": 3, + "approved_count": 1, + "rejected_count": 1, + "soft_rejected_count": 1, + }, + }, + + [urls.server.tasksWorkerUnitsIds(1)]: { + "worker_units_ids": [ + { + "worker_id": 1, + "unit_id": 1, + }, + { + "worker_id": 1, + "unit_id": 2, + }, + { + "worker_id": 1, + "unit_id": 3, + }, + ], + }, +}; diff --git a/mephisto/client/review_app/client/src/requests/qualifications.ts b/mephisto/client/review_app/client/src/requests/qualifications.ts index 59c840011..811021823 100644 --- a/mephisto/client/review_app/client/src/requests/qualifications.ts +++ b/mephisto/client/review_app/client/src/requests/qualifications.ts @@ -30,3 +30,96 @@ export function getQualifications( abortController, ); } + + +export function getQualificationWorkers( + id: number, + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + getParams: { [key: string]: string | number } = null, + abortController?: AbortController, +) { + const url = generateURL(urls.server.qualificationWorkers(id), null, getParams); + + makeRequest( + 'GET', + url, + null, + (data) => setDataAction(data.workers), + setLoadingAction, + setErrorsAction, + 'getQualificationWorkers error:', + abortController, + ); +} + + +export function postQualification( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.qualifications, null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postQualification error:', + abortController, + ); +} + + +export function postQualificationGrantWorker( + id: number, + workerId: number, + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.qualificationGrantWorker(id, workerId), null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postQualificationGrantWorker error:', + abortController, + ); +} + + +export function postQualificationRevokeWorker( + id: number, + workerId: number, + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.qualificationRevokeWorker(id, workerId), null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postQualificationRevokeWorker error:', + abortController, + ); +} diff --git a/mephisto/client/review_app/client/src/requests/stats.ts b/mephisto/client/review_app/client/src/requests/stats.ts new file mode 100644 index 000000000..af926ee60 --- /dev/null +++ b/mephisto/client/review_app/client/src/requests/stats.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + + +import urls from 'urls'; +import generateURL from './generateURL'; +import makeRequest from './makeRequest'; + + +export function getStats( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + getParams: { [key: string]: string | number } = null, + abortController?: AbortController, +) { + const url = generateURL(urls.server.stats, null, getParams); + + makeRequest( + 'GET', + url, + null, + (data) => setDataAction(data.stats), + setLoadingAction, + setErrorsAction, + 'getStats error:', + abortController, + ); +} diff --git a/mephisto/client/review_app/client/src/requests/tasks.ts b/mephisto/client/review_app/client/src/requests/tasks.ts index 1e23efaed..f723a7c24 100644 --- a/mephisto/client/review_app/client/src/requests/tasks.ts +++ b/mephisto/client/review_app/client/src/requests/tasks.ts @@ -30,3 +30,26 @@ export function getTasks( abortController, ); } + + +export function getTaskWorkerUnitsIds( + id: number, + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + getParams: { [key: string]: string | number } = null, + abortController?: AbortController, +) { + const url = generateURL(urls.server.tasksWorkerUnitsIds(id), null, getParams); + + makeRequest( + 'GET', + url, + null, + (data) => setDataAction(data.worker_units_ids), + setLoadingAction, + setErrorsAction, + 'getTaskWorkerUnitsIds error:', + abortController, + ); +} diff --git a/mephisto/client/review_app/client/src/requests/units.ts b/mephisto/client/review_app/client/src/requests/units.ts new file mode 100644 index 000000000..3fee5636b --- /dev/null +++ b/mephisto/client/review_app/client/src/requests/units.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + + +import urls from 'urls'; +import generateURL from './generateURL'; +import makeRequest from './makeRequest'; + + +export function getUnits( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + getParams: { [key: string]: string | number } = null, + abortController?: AbortController, +) { + const url = generateURL(urls.server.units, null, getParams); + + makeRequest( + 'GET', + url, + null, + (data) => setDataAction(data.units), + setLoadingAction, + setErrorsAction, + 'getUnits error:', + abortController, + ); +} + + +export function getUnitsDetails( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + getParams: { [key: string]: string | number } = null, + abortController?: AbortController, +) { + const url = generateURL(urls.server.unitsDetails, null, getParams); + + makeRequest( + 'GET', + url, + null, + (data) => setDataAction(data.units), + setLoadingAction, + setErrorsAction, + 'getUnitsDetails error:', + abortController, + ); +} + + +export function postUnitsApprove( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.unitsApprove, null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postUnitsApprove error:', + abortController, + ); +} + + +export function postUnitsReject( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.unitsReject, null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postUnitsReject error:', + abortController, + ); +} + + +export function postUnitsSoftReject( + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.unitsSoftReject, null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postUnitsSoftReject error:', + abortController, + ); +} diff --git a/mephisto/client/review_app/client/src/requests/workers.ts b/mephisto/client/review_app/client/src/requests/workers.ts new file mode 100644 index 000000000..1adaef8ad --- /dev/null +++ b/mephisto/client/review_app/client/src/requests/workers.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + + +import urls from 'urls'; +import generateURL from './generateURL'; +import makeRequest from './makeRequest'; + + +export function postWorkerBlock( + id: number, + setDataAction: SetRequestDataActionType, + setLoadingAction: SetRequestLoadingActionType, + setErrorsAction: SetRequestErrorsActionType, + data: { [key: string]: string | number }, + abortController?: AbortController, +) { + const url = generateURL(urls.server.workersBlock(id), null, null); + + makeRequest( + 'POST', + url, + JSON.stringify(data), + (data) => setDataAction(data), + setLoadingAction, + setErrorsAction, + 'postWorkerBlock error:', + abortController, + ); +} diff --git a/mephisto/client/review_app/client/src/types/reviewModal.d.ts b/mephisto/client/review_app/client/src/types/reviewModal.d.ts new file mode 100644 index 000000000..99196b027 --- /dev/null +++ b/mephisto/client/review_app/client/src/types/reviewModal.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + +type FormType = { + checkboxAssign: boolean; + checkboxBanWorker?: boolean; + checkboxComment: boolean; + checkboxGiveTips?: boolean; + comment: string; + qualification: number | null; + qualificationValue: number; + tips: number | null; +}; + + +type ModalDataType = { + applyToNext: boolean; + buttonCancel: string; + buttonSubmit: string; + form: FormType; + title: string; + type: string; +}; diff --git a/mephisto/client/review_app/client/src/types/units.d.ts b/mephisto/client/review_app/client/src/types/units.d.ts new file mode 100644 index 000000000..83758a18c --- /dev/null +++ b/mephisto/client/review_app/client/src/types/units.d.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) Facebook, Inc. 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. + */ + + +declare type Unit = { + id: number; + worker_id: number | null; + task_id: number | null; + pay_amount: number; + status: string; + creation_date: string; + results: { + start: number; + end: number; + input_preview: null; + output_preview: null; + }; + review: { + tips: number | null; + feedback: number | null; + }; +}; diff --git a/mephisto/client/review_app/client/src/urls.ts b/mephisto/client/review_app/client/src/urls.ts index a3a34e80c..3d366abd6 100644 --- a/mephisto/client/review_app/client/src/urls.ts +++ b/mephisto/client/review_app/client/src/urls.ts @@ -11,13 +11,27 @@ const API_URL = 'http://localhost:5001'; const urls = { client: { home: '/', - qualifications: '/qualifications', task: (id) => `/tasks/${id}`, tasks: '/tasks', }, server: { - qualifications: API_URL + '/qualifications/', - tasks: API_URL + '/tasks/', + qualifications: API_URL + '/qualifications', + qualificationWorkers: (id) => API_URL + `/qualifications/${id}/workers`, + qualificationGrantWorker: (id, workerId) => ( + API_URL + `/qualifications/${id}/workers/${workerId}/grant` + ), + qualificationRevokeWorker: (id, workerId) => ( + API_URL + `/qualifications/${id}/workers/${workerId}/revoke` + ), + stats: API_URL + '/stats', + tasks: API_URL + '/tasks', + tasksWorkerUnitsIds: (id) => API_URL + `/tasks/${id}/worker-units-ids`, + units: API_URL + '/units', + unitsApprove: API_URL + '/units/approve', + unitsDetails: API_URL + '/units/details', + unitsReject: API_URL + '/units/reject', + unitsSoftReject: API_URL + '/units/soft-reject', + workersBlock: (id) => API_URL + `/workers/${id}/block`, }, }; diff --git a/mephisto/client/review_app/server/api/views/__init__.py b/mephisto/client/review_app/server/api/views/__init__.py index 7e3fbab01..5524ea0ba 100644 --- a/mephisto/client/review_app/server/api/views/__init__.py +++ b/mephisto/client/review_app/server/api/views/__init__.py @@ -7,6 +7,7 @@ from .qualification_workers_view import QualificationWorkersView from .qualifications_view import QualificationsView from .qualify_worker_view import QualifyWorkerView +from .stats_view import StatsView from .tasks_view import TasksView from .tasks_worker_units_view import TasksWorkerUnitsView from .units_approve_view import UnitsApproveView @@ -15,4 +16,3 @@ from .units_soft_reject_view import UnitsSoftRejectView from .units_view import UnitsView from .worker_block_view import WorkerBlockView -from .worker_stats_view import WorkerStatsView diff --git a/mephisto/client/review_app/server/api/views/worker_stats_view.py b/mephisto/client/review_app/server/api/views/stats_view.py similarity index 68% rename from mephisto/client/review_app/server/api/views/worker_stats_view.py rename to mephisto/client/review_app/server/api/views/stats_view.py index 9f0793cff..525d7b409 100644 --- a/mephisto/client/review_app/server/api/views/worker_stats_view.py +++ b/mephisto/client/review_app/server/api/views/stats_view.py @@ -17,31 +17,40 @@ from mephisto.abstractions.databases.local_database import nonesafe_int from mephisto.abstractions.databases.local_database import StringIDRow from mephisto.data_model.constants.assignment_state import AssignmentState -from mephisto.data_model.worker import Worker def _find_unit_reviews( db, - worker_id: str, + worker_id: Optional[str] = None, task_id: Optional[str] = None, status: Optional[str] = None, since: Optional[str] = None, limit: Optional[int] = None, ) -> List[StringIDRow]: - params = [nonesafe_int(worker_id)] + params = [] - task_query = "AND (task_id = ?)" if task_id else "" + worker_query = "worker_id = ?" if worker_id else "" + if worker_id: + params.append(nonesafe_int(worker_id)) + + task_query = "task_id = ?" if task_id else "" if task_id: params.append(nonesafe_int(task_id)) - status_query = "AND (status = ?)" if status else "" + status_query = "status = ?" if status else "" if status: params.append(status) - since_query = "AND (created_at >= ?)" if since else "" + since_query = "created_at >= ?" if since else "" if since: params.append(since) + joined_queries = ' AND '.join(list(filter(bool, [ + worker_query, task_query, status_query, since_query, + ]))) + + where_query = f"WHERE {joined_queries}" if joined_queries else "" + limit_query = "LIMIT ?" if limit else "" if limit: params.append(nonesafe_int(limit)) @@ -53,7 +62,7 @@ def _find_unit_reviews( c.execute( f""" SELECT * FROM unit_review - WHERE (worker_id = ?) {task_query} {status_query} {since_query} + {where_query} ORDER BY created_at ASC {limit_query}; """, params, @@ -63,13 +72,11 @@ def _find_unit_reviews( return results -class WorkerStatsView(MethodView): - def get(self, worker_id: int) -> dict: - """ Get stats of recent approvals for the worker """ - - # Check if exists. Raises exceptions in case if not - worker: Worker = Worker.get(app.db, str(worker_id)) +class StatsView(MethodView): + def get(self) -> dict: + """ Get stats of recent approvals for the worker or task """ + worker_id = request.args.get("worker_id") task_id = request.args.get("task_id") limit = request.args.get("limit") since = request.args.get("since") @@ -83,7 +90,7 @@ def get(self, worker_id: int) -> dict: approved_unit_reviews = _find_unit_reviews( db=app.db, - worker_id=worker.db_id, + worker_id=worker_id, task_id=task_id, status=AssignmentState.ACCEPTED, since=since, @@ -91,7 +98,7 @@ def get(self, worker_id: int) -> dict: ) rejected_unit_reviews = _find_unit_reviews( db=app.db, - worker_id=worker.db_id, + worker_id=worker_id, task_id=task_id, status=AssignmentState.REJECTED, since=since, @@ -99,18 +106,31 @@ def get(self, worker_id: int) -> dict: ) soft_rejected_unit_reviews = _find_unit_reviews( db=app.db, - worker_id=worker.db_id, + worker_id=worker_id, task_id=task_id, status=AssignmentState.SOFT_REJECTED, since=since, limit=limit, ) - all_unit_reviews = _find_unit_reviews(db=app.db, worker_id=worker.db_id) + all_unit_reviews = _find_unit_reviews( + db=app.db, + worker_id=worker_id, + task_id=task_id, + since=since, + limit=limit, + ) + + rewied_statuses = [ + AssignmentState.ACCEPTED, + AssignmentState.REJECTED, + AssignmentState.SOFT_REJECTED, + ] + reviewed_reviews = [ur for ur in all_unit_reviews if ur["status"] in rewied_statuses] return { - "worker_id": worker.db_id, "stats": { "total_count": len(all_unit_reviews), # within the scope of the filters + "reviewed_count": len(reviewed_reviews), "approved_count": len(approved_unit_reviews), "rejected_count": len(rejected_unit_reviews), "soft_rejected_count": len(soft_rejected_unit_reviews), diff --git a/mephisto/client/review_app/server/api/views/units_view.py b/mephisto/client/review_app/server/api/views/units_view.py index 6f5e4c97d..4731ece1a 100644 --- a/mephisto/client/review_app/server/api/views/units_view.py +++ b/mephisto/client/review_app/server/api/views/units_view.py @@ -82,7 +82,7 @@ def get(self) -> dict: }, "review": { "tips": int(tips) if tips else None, - "feedback": int(feedback) if feedback else None, + "feedback": feedback if feedback else None, } } ) diff --git a/mephisto/client/review_app/server/urls.py b/mephisto/client/review_app/server/urls.py index d091536de..6c8953141 100644 --- a/mephisto/client/review_app/server/urls.py +++ b/mephisto/client/review_app/server/urls.py @@ -19,17 +19,17 @@ def init_urls(app: Flask): defaults={"action": "revoke"}, ) app.add_url_rule( - "/qualifications/", + "/qualifications", view_func=api_views.QualificationsView.as_view("qualifications"), ) - app.add_url_rule( - "/tasks/", - view_func=api_views.TasksView.as_view("tasks"), - ) app.add_url_rule( "/tasks//worker-units-ids", view_func=api_views.TasksWorkerUnitsView.as_view("worker-units-ids"), ) + app.add_url_rule( + "/tasks", + view_func=api_views.TasksView.as_view("tasks"), + ) app.add_url_rule( "/units", view_func=api_views.UnitsView.as_view("units"), @@ -55,6 +55,6 @@ def init_urls(app: Flask): view_func=api_views.WorkerBlockView.as_view("worker_block"), ) app.add_url_rule( - "/workers//stats", - view_func=api_views.WorkerStatsView.as_view("worker_stats"), + "/stats", + view_func=api_views.StatsView.as_view("stats"), )