Skip to content

Commit

Permalink
Added results export in JSON format for TaskReview app
Browse files Browse the repository at this point in the history
  • Loading branch information
meta-paul committed Nov 16, 2023
1 parent cc1c2e0 commit 6cbce4e
Show file tree
Hide file tree
Showing 21 changed files with 430 additions and 84 deletions.
6 changes: 3 additions & 3 deletions mephisto/abstractions/providers/prolific/prolific_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def approve_work(
datastore_unit = self.datastore.get_unit(self.unit_id)
prolific_utils.approve_work(
client,
submission_id=datastore_unit['prolific_submission_id'],
submission_id=datastore_unit["prolific_submission_id"],
)

logger.debug(
Expand Down Expand Up @@ -153,7 +153,7 @@ def soft_reject_work(self, review_note: Optional[str] = None) -> None:
datastore_unit = self.datastore.get_unit(self.unit_id)
prolific_utils.approve_work(
client,
submission_id=datastore_unit['prolific_submission_id'],
submission_id=datastore_unit["prolific_submission_id"],
)

logger.debug(
Expand Down Expand Up @@ -181,7 +181,7 @@ def reject_work(self, review_note: Optional[str] = None) -> None:
try:
prolific_utils.reject_work(
client,
submission_id=datastore_unit['prolific_submission_id'],
submission_id=datastore_unit["prolific_submission_id"],
)
except ProlificException:
logger.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ function ModalForm(props: ModalFormProps) {
const [qualifications, setQualifications] = React.useState<
Array<QualificationType>
>(null);
const [getQualificationsloading, setGetQualificationsloading] = React.useState(false);
const [, setCreateQualificationLoading] = React.useState(false);
const [
getQualificationsloading,
setGetQualificationsloading,
] = React.useState(false);
const [_, setCreateQualificationLoading] = React.useState(false);

const onChangeAssign = (value: boolean) => {
let prevFormData: FormType = Object(props.data.form);
Expand Down Expand Up @@ -129,7 +132,7 @@ function ModalForm(props: ModalFormProps) {
setQualifications,
setGetQualificationsloading,
onError,
params,
params
);
};

Expand All @@ -138,7 +141,7 @@ function ModalForm(props: ModalFormProps) {
onCreateNewQualificationSuccess,
setCreateQualificationLoading,
onError,
{name: name},
{ name: name }
);
};

Expand Down Expand Up @@ -173,68 +176,76 @@ function ModalForm(props: ModalFormProps) {
}
/>

{props.data.form.checkboxAssignQualification && (<>
<Row className={"second-line"}>
<Col xs={9}>
<Form.Select
id={"assignQualification"}
size={"sm"}
value={props.data.form.qualification || ""}
onChange={(e) => onChangeAssignQualification(e.target.value)}
>
<option value={""}>---</option>
<option value={"+"}>+ Add new qualification</option>
{qualifications &&
qualifications.map((q: QualificationType) => {
return (
<option key={"qual" + q.id} value={q.id}>
{q.name}
</option>
);
})}
</Form.Select>
</Col>
<Col>
<Form.Select
id={"assignQualificationValue"}
size={"sm"}
value={props.data.form.qualificationValue}
onChange={(e) =>
onChangeAssignQualificationValue(e.target.value)
}
>
{range(1, 10).map((i) => {
return <option key={"qualVal" + i}>{i}</option>;
})}
</Form.Select>
</Col>
</Row>
{props.data.form.showNewQualification && (
<Row className={"third-line"}>
{props.data.form.checkboxAssignQualification && (
<>
<Row className={"second-line"}>
<Col xs={9}>
<Form.Control
<Form.Select
id={"assignQualification"}
size={"sm"}
type={"input"}
placeholder={"New qualification name"}
value={props.data.form.newQualificationValue || ""}
onChange={(e) => onChangeNewQualificationValue(e.target.value)}
/>
value={props.data.form.qualification || ""}
onChange={(e) =>
onChangeAssignQualification(e.target.value)
}
>
<option value={""}>---</option>
<option value={"+"}>+ Add new qualification</option>
{qualifications &&
qualifications.map((q: QualificationType) => {
return (
<option key={"qual" + q.id} value={q.id}>
{q.name}
</option>
);
})}
</Form.Select>
</Col>
<Col>
<Button
className={"new-qualification-name-button"}
variant={"secondary"}
<Form.Select
id={"assignQualificationValue"}
size={"sm"}
onClick={
() => onClickAddNewQualification(props.data.form.newQualificationValue)
value={props.data.form.qualificationValue}
onChange={(e) =>
onChangeAssignQualificationValue(e.target.value)
}
>
Add
</Button>
{range(1, 10).map((i) => {
return <option key={"qualVal" + i}>{i}</option>;
})}
</Form.Select>
</Col>
</Row>
)}
</>)}
{props.data.form.showNewQualification && (
<Row className={"third-line"}>
<Col xs={9}>
<Form.Control
size={"sm"}
type={"input"}
placeholder={"New qualification name"}
value={props.data.form.newQualificationValue || ""}
onChange={(e) =>
onChangeNewQualificationValue(e.target.value)
}
/>
</Col>
<Col>
<Button
className={"new-qualification-name-button"}
variant={"secondary"}
size={"sm"}
onClick={() =>
onClickAddNewQualification(
props.data.form.newQualificationValue
)
}
>
Add
</Button>
</Col>
</Row>
)}
</>
)}
</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@
text-decoration: underline;
}

.tasks .tasks-table .task-row .export-loading {
padding-left: 20px;
}

.tasks .tasks-table .task-row .download-button {
cursor: pointer;
}
.tasks .tasks-table .task-row .download-button:hover {
text-decoration: underline;
}

.tasks .tasks-table .task-row .reviewed,
.tasks .tasks-table .task-row .units,
.tasks .tasks-table .task-row .date {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as moment from "moment/moment";
import * as React from "react";
import { useEffect } from "react";
import { Spinner, Table } from "react-bootstrap";
import { getTasks } from "requests/tasks";
import { exportTaskResults, getTasks } from "requests/tasks";
import urls from "urls";
import TasksHeader from "./TasksHeader/TasksHeader";
import "./TasksPage.css";
Expand All @@ -24,6 +24,7 @@ function TasksPage(props: PropsType) {

const [tasks, setTasks] = React.useState<Array<TaskType>>(null);
const [loading, setLoading] = React.useState(false);
const [loadingExportResults, setLoadingExportResults] = React.useState(false);

const onTaskRowClick = (id: number) => {
localStorage.setItem(STORAGE_TASK_ID_KEY, String(id));
Expand All @@ -41,6 +42,24 @@ function TasksPage(props: PropsType) {
}
};

const requestTaskResults = (taskId: number) => {
const onSuccessExportResults = (data) => {
if (data.file_created) {
// Create pseudo link and click it
const linkId = "result-json";
const link = document.createElement("a");
link.setAttribute("style", "display: none;");
link.id = linkId;
link.href = urls.server.taskExportResultsJson(taskId);
link.target = "_blank";
link.click();
link.remove();
}
};

exportTaskResults(taskId, onSuccessExportResults, setLoadingExportResults);
};

useEffect(() => {
document.title = "Mephisto - Task Review - All Tasks";

Expand Down Expand Up @@ -70,6 +89,9 @@ function TasksPage(props: PropsType) {
<th className={"title date"}>
<b>Date</b>
</th>
<th className={"title export"}>
<b>Export task results</b>
</th>
<th></th>
</tr>
</thead>
Expand Down Expand Up @@ -97,6 +119,27 @@ function TasksPage(props: PropsType) {
</td>
<td className={"units"}>{task.unit_count}</td>
<td className={"date"}>{date}</td>
<td className={"export"}>
{task.is_reviewed && !loadingExportResults && (
<span
className={"text-primary download-button"}
onClick={() => requestTaskResults(task.id)}
>
Download
</span>
)}
{loadingExportResults && (
<div className={"export-loading"}>
<Spinner
animation="border"
role="status"
style={{ width: "1.2rem", height: "1.2rem" }}
>
<span className="visually-hidden">Loading...</span>
</Spinner>
</div>
)}
</td>
<td></td>
</tr>
);
Expand Down
21 changes: 21 additions & 0 deletions mephisto/client/review_app/client/src/requests/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,24 @@ export function getTaskWorkerUnitsIds(
abortController
);
}

export function exportTaskResults(
id: number,
setDataAction: SetRequestDataActionType,
setLoadingAction: SetRequestLoadingActionType,
setErrorsAction: SetRequestErrorsActionType,
abortController?: AbortController
) {
const url = generateURL(urls.server.taskExportResults, [id]);

makeRequest(
"GET",
url,
null,
setDataAction,
setLoadingAction,
setErrorsAction,
"exportTaskResults error:",
abortController
);
}
3 changes: 3 additions & 0 deletions mephisto/client/review_app/client/src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const urls = {
API_URL + `/api/qualifications/${id}/workers/${workerId}/revoke`,
stats: API_URL + "/api/stats",
task: (id) => API_URL + `/api/tasks/${id}`,
taskExportResults: (id) => API_URL + `/api/tasks/${id}/export-results`,
taskExportResultsJson: (id) =>
API_URL + `/api/tasks/${id}/export-results.json`,
tasks: API_URL + "/api/tasks",
tasksWorkerUnitsIds: (id) => API_URL + `/api/tasks/${id}/worker-units-ids`,
unitReviewHtml: (id) => API_URL + `/api/units/${id}/review.html`,
Expand Down
2 changes: 2 additions & 0 deletions mephisto/client/review_app/server/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from .qualifications_view import QualificationsView
from .qualify_worker_view import QualifyWorkerView
from .stats_view import StatsView
from .task_export_results_json_view import TaskExportResultsJsonView
from .task_export_results_view import TaskExportResultsView
from .task_view import TaskView
from .tasks_view import TasksView
from .tasks_worker_units_view import TaskUnitIdsView
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3

# 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 os

from flask import send_file
from flask.views import MethodView
from werkzeug.exceptions import NotFound

from .task_export_results_view import get_result_file_path
from .task_export_results_view import get_results_dir


class TaskExportResultsJsonView(MethodView):
def get(self, task_id: str = None) -> dict:
"""Get result data file in JSON format"""
results_dir = get_results_dir()
results_file_path = get_result_file_path(results_dir, task_id)

if not os.path.exists(results_file_path):
raise NotFound("File not found")

return send_file(
results_file_path,
as_attachment=True,
attachment_filename=f"task-{task_id}-results.json",
)
Loading

0 comments on commit 6cbce4e

Please sign in to comment.