From 6cbce4e31e34dde57029688789a9aa1cf668bb07 Mon Sep 17 00:00:00 2001 From: Paul Abumov Date: Thu, 16 Nov 2023 15:17:43 -0500 Subject: [PATCH] Added results export in JSON format for TaskReview app --- .../providers/prolific/prolific_agent.py | 6 +- .../pages/TaskPage/ModalForm/ModalForm.tsx | 125 ++++++++++-------- .../client/src/pages/TasksPage/TasksPage.css | 11 ++ .../client/src/pages/TasksPage/TasksPage.tsx | 45 ++++++- .../review_app/client/src/requests/tasks.ts | 21 +++ mephisto/client/review_app/client/src/urls.ts | 3 + .../review_app/server/api/views/__init__.py | 2 + .../views/task_export_results_json_view.py | 30 +++++ .../api/views/task_export_results_view.py | 76 +++++++++++ .../api/views/unit_review_bundle_view.py | 16 ++- mephisto/client/review_app/server/urls.py | 8 ++ mephisto/utils/testing.py | 8 +- test/core/test_operator.py | 4 +- .../server/api/base_test_api_view_case.py | 8 +- test/review_app/server/api/test_home_view.py | 2 +- .../server/api/test_qualifications_view.py | 3 +- .../api/test_task_export_results_json_view.py | 51 +++++++ .../api/test_task_export_results_view.py | 85 ++++++++++++ test/review_app/server/api/test_task_view.py | 2 +- .../server/api/test_unit_html_view.py | 2 +- .../server/api/test_units_approve_view.py | 6 +- 21 files changed, 430 insertions(+), 84 deletions(-) create mode 100644 mephisto/client/review_app/server/api/views/task_export_results_json_view.py create mode 100644 mephisto/client/review_app/server/api/views/task_export_results_view.py create mode 100644 test/review_app/server/api/test_task_export_results_json_view.py create mode 100644 test/review_app/server/api/test_task_export_results_view.py diff --git a/mephisto/abstractions/providers/prolific/prolific_agent.py b/mephisto/abstractions/providers/prolific/prolific_agent.py index 63bb1a61a..a97a8d78c 100644 --- a/mephisto/abstractions/providers/prolific/prolific_agent.py +++ b/mephisto/abstractions/providers/prolific/prolific_agent.py @@ -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( @@ -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( @@ -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( 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 index 2b18e9db4..1a37a4349 100644 --- a/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx +++ b/mephisto/client/review_app/client/src/pages/TaskPage/ModalForm/ModalForm.tsx @@ -24,8 +24,11 @@ function ModalForm(props: ModalFormProps) { const [qualifications, setQualifications] = React.useState< Array >(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); @@ -129,7 +132,7 @@ function ModalForm(props: ModalFormProps) { setQualifications, setGetQualificationsloading, onError, - params, + params ); }; @@ -138,7 +141,7 @@ function ModalForm(props: ModalFormProps) { onCreateNewQualificationSuccess, setCreateQualificationLoading, onError, - {name: name}, + { name: name } ); }; @@ -173,68 +176,76 @@ function ModalForm(props: ModalFormProps) { } /> - {props.data.form.checkboxAssignQualification && (<> - - - onChangeAssignQualification(e.target.value)} - > - - - {qualifications && - qualifications.map((q: QualificationType) => { - return ( - - ); - })} - - - - - onChangeAssignQualificationValue(e.target.value) - } - > - {range(1, 10).map((i) => { - return ; - })} - - - - {props.data.form.showNewQualification && ( - + {props.data.form.checkboxAssignQualification && ( + <> + - onChangeNewQualificationValue(e.target.value)} - /> + value={props.data.form.qualification || ""} + onChange={(e) => + onChangeAssignQualification(e.target.value) + } + > + + + {qualifications && + qualifications.map((q: QualificationType) => { + return ( + + ); + })} + - + {range(1, 10).map((i) => { + return ; + })} + - )} - )} + {props.data.form.showNewQualification && ( + + + + onChangeNewQualificationValue(e.target.value) + } + /> + + + + + + )} + + )} )} diff --git a/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.css b/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.css index 133106419..31cd46dfc 100644 --- a/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.css +++ b/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.css @@ -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 { 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 bcf3065be..d3622712c 100644 --- a/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.tsx +++ b/mephisto/client/review_app/client/src/pages/TasksPage/TasksPage.tsx @@ -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"; @@ -24,6 +24,7 @@ function TasksPage(props: PropsType) { const [tasks, setTasks] = React.useState>(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)); @@ -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"; @@ -70,6 +89,9 @@ function TasksPage(props: PropsType) { Date + + Export task results + @@ -97,6 +119,27 @@ function TasksPage(props: PropsType) { {task.unit_count} {date} + + {task.is_reviewed && !loadingExportResults && ( + requestTaskResults(task.id)} + > + Download + + )} + {loadingExportResults && ( +
+ + Loading... + +
+ )} + ); diff --git a/mephisto/client/review_app/client/src/requests/tasks.ts b/mephisto/client/review_app/client/src/requests/tasks.ts index 603ada566..539f76aa4 100644 --- a/mephisto/client/review_app/client/src/requests/tasks.ts +++ b/mephisto/client/review_app/client/src/requests/tasks.ts @@ -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 + ); +} diff --git a/mephisto/client/review_app/client/src/urls.ts b/mephisto/client/review_app/client/src/urls.ts index e6da291df..1745b6acf 100644 --- a/mephisto/client/review_app/client/src/urls.ts +++ b/mephisto/client/review_app/client/src/urls.ts @@ -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`, diff --git a/mephisto/client/review_app/server/api/views/__init__.py b/mephisto/client/review_app/server/api/views/__init__.py index 2fc7df8fc..d8fee9508 100644 --- a/mephisto/client/review_app/server/api/views/__init__.py +++ b/mephisto/client/review_app/server/api/views/__init__.py @@ -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 diff --git a/mephisto/client/review_app/server/api/views/task_export_results_json_view.py b/mephisto/client/review_app/server/api/views/task_export_results_json_view.py new file mode 100644 index 000000000..486c970c5 --- /dev/null +++ b/mephisto/client/review_app/server/api/views/task_export_results_json_view.py @@ -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", + ) diff --git a/mephisto/client/review_app/server/api/views/task_export_results_view.py b/mephisto/client/review_app/server/api/views/task_export_results_view.py new file mode 100644 index 000000000..292a8f262 --- /dev/null +++ b/mephisto/client/review_app/server/api/views/task_export_results_view.py @@ -0,0 +1,76 @@ +#!/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 json +import os +from pathlib import Path +from typing import List + +from flask import current_app as app +from flask.views import MethodView +from werkzeug.exceptions import BadRequest + +from mephisto.abstractions.databases.local_database import StringIDRow +from mephisto.client.review_app.server.db_queries import find_units +from mephisto.data_model.constants.assignment_state import AssignmentState +from mephisto.data_model.unit import Unit + + +def get_results_dir() -> str: + project_root_dir = Path(__file__).resolve().parent.parent.parent.parent.parent.parent.parent + results_dir = os.path.join(project_root_dir, "tmp", "results") + return results_dir + + +def get_result_file_path(results_dir: str, task_id: str) -> str: + return os.path.join(results_dir, f"task_{task_id}_results.json") + + +class TaskExportResultsView(MethodView): + def get(self, task_id: str = None) -> dict: + """Assemble results for all Units related to the Task into a single file""" + + db_task: StringIDRow = app.db.get_task(task_id) + app.logger.debug(f"Found Task in DB: {db_task}") + + db_units: List[StringIDRow] = find_units( + app.db, + int(db_task["task_id"]), + statuses=AssignmentState.completed(), + debug=app.debug, + ) + + is_reviewed = all([u["status"] != AssignmentState.COMPLETED for u in db_units]) + + if not is_reviewed: + raise BadRequest( + "This task has not been fully reviewed yet. " + "Please review it completely before requesting the results." + ) + + task_units_data = {} + for db_unit in db_units: + unit_id = db_unit["unit_id"] + unit: Unit = Unit.get(app.db, unit_id) + + try: + unit_data = app.data_browser.get_data_from_unit(unit) + except AssertionError: + # In case if unit does not have agent somehow + unit_data = {} + + task_units_data[unit_id] = unit_data + + # Save file with results, so it can be copied later from the repo if needed + results_dir = get_results_dir() + results_file_path = get_result_file_path(results_dir, task_id) + + if not os.path.exists(results_file_path): + os.makedirs(results_dir, exist_ok=True) + with open(results_file_path, "w") as f: + f.write(json.dumps(task_units_data, indent=4)) + + return {"file_created": True} diff --git a/mephisto/client/review_app/server/api/views/unit_review_bundle_view.py b/mephisto/client/review_app/server/api/views/unit_review_bundle_view.py index 8aea639ac..73bcccb3f 100644 --- a/mephisto/client/review_app/server/api/views/unit_review_bundle_view.py +++ b/mephisto/client/review_app/server/api/views/unit_review_bundle_view.py @@ -35,13 +35,15 @@ def get(self, unit_id: str = None) -> Union[dict, Response]: except (ConfigKeyError, FileNotFoundError) as e: return Response( - json.dumps({ - "error": ( - "`blueprint.task_source_review` was not found or not specified " - "or database is corrupted or just old. " - "You need this settings to make review app work." - ), - }), + json.dumps( + { + "error": ( + "`blueprint.task_source_review` was not found or not specified " + "or database is corrupted or just old. " + "You need this settings to make review app work." + ), + } + ), status=status.HTTP_404_NOT_FOUND, mimetype="application/json", ) diff --git a/mephisto/client/review_app/server/urls.py b/mephisto/client/review_app/server/urls.py index e7b969db9..f33e7e712 100644 --- a/mephisto/client/review_app/server/urls.py +++ b/mephisto/client/review_app/server/urls.py @@ -34,6 +34,14 @@ def init_urls(app: Flask): "/api/tasks/", view_func=api_views.TaskView.as_view("task"), ) + app.add_url_rule( + "/api/tasks//export-results", + view_func=api_views.TaskExportResultsView.as_view("task_export_results"), + ) + app.add_url_rule( + "/api/tasks//export-results.json", + view_func=api_views.TaskExportResultsJsonView.as_view("task_export_results_json"), + ) app.add_url_rule( "/api/units", view_func=api_views.UnitsView.as_view("units"), diff --git a/mephisto/utils/testing.py b/mephisto/utils/testing.py index 03d7ba33d..d8015b434 100644 --- a/mephisto/utils/testing.py +++ b/mephisto/utils/testing.py @@ -219,10 +219,10 @@ def find_unit_reviews( c.execute( f""" SELECT * FROM unit_review - WHERE - (updated_qualification_id = ?1) OR - (revoked_qualification_id = ?1) - AND (worker_id = ?2) + WHERE + (updated_qualification_id = ?1) OR + (revoked_qualification_id = ?1) + AND (worker_id = ?2) {task_query} ORDER BY created_at ASC; """, diff --git a/test/core/test_operator.py b/test/core/test_operator.py index cf425ceb1..5a7078279 100644 --- a/test/core/test_operator.py +++ b/test/core/test_operator.py @@ -79,8 +79,8 @@ def tearDown(self): threads = threading.enumerate() target_threads = [ t - for t in threads if - not isinstance(t, TMonitor) + for t in threads + if not isinstance(t, TMonitor) and not t.daemon and not t.name.startswith("asyncio_") ] diff --git a/test/review_app/server/api/base_test_api_view_case.py b/test/review_app/server/api/base_test_api_view_case.py index 96f2a018e..e1cbb65da 100644 --- a/test/review_app/server/api/base_test_api_view_case.py +++ b/test/review_app/server/api/base_test_api_view_case.py @@ -30,9 +30,11 @@ def setUp(self): # Configure test Flask client app = create_app(debug=True, database_path=database_path) - app.config.update({ - "TESTING": True, - }) + app.config.update( + { + "TESTING": True, + } + ) self.app_context = app.test_request_context() self.app_context.push() self.client = app.test_client() diff --git a/test/review_app/server/api/test_home_view.py b/test/review_app/server/api/test_home_view.py index 779dbe32d..b8ab34b67 100644 --- a/test/review_app/server/api/test_home_view.py +++ b/test/review_app/server/api/test_home_view.py @@ -42,7 +42,7 @@ def test_home_success(self, mock_join, *args, **kwargs): @patch("os.path.join") def test_home__no_html_file_error(self, mock_join, *args, **kwargs): - mock_join.return_value = '/nonexistent/path' + mock_join.return_value = "/nonexistent/path" with self.app_context: url = url_for("client-tasks", path="tasks") diff --git a/test/review_app/server/api/test_qualifications_view.py b/test/review_app/server/api/test_qualifications_view.py index a457d132e..9b8e37040 100644 --- a/test/review_app/server/api/test_qualifications_view.py +++ b/test/review_app/server/api/test_qualifications_view.py @@ -67,7 +67,8 @@ def test_qualification_create_already_exists_error(self, *args, **kwargs): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - result["error"], f'Qualification with name "{qualification_name}" already exists.', + result["error"], + f'Qualification with name "{qualification_name}" already exists.', ) diff --git a/test/review_app/server/api/test_task_export_results_json_view.py b/test/review_app/server/api/test_task_export_results_json_view.py new file mode 100644 index 000000000..296b03992 --- /dev/null +++ b/test/review_app/server/api/test_task_export_results_json_view.py @@ -0,0 +1,51 @@ +#!/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. + +import unittest +from unittest.mock import patch + +from flask import url_for + +from mephisto.abstractions.providers.prolific.api import status +from mephisto.client.review_app.server.api.views.task_export_results_view import ( + get_result_file_path, +) +from test.review_app.server.api.base_test_api_view_case import BaseTestApiViewCase + + +class TestTaskExportResultsJsonView(BaseTestApiViewCase): + @patch( + "mephisto.client.review_app.server.api.views.task_export_results_json_view.get_results_dir" + ) + def test_task_export_result_json_success(self, mock_get_results_dir, *args, **kwargs): + mock_get_results_dir.return_value = self.data_dir + task_id = 1 + + results_file_data = "Test JS" + + results_file_path = get_result_file_path(self.data_dir, task_id) + f = open(results_file_path, "w") + f.write(results_file_data) + f.close() + + with self.app_context: + url = url_for("task_export_results_json", task_id=task_id) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, results_file_data.encode()) + self.assertEqual(response.mimetype, "application/json") + + def test_task_export_result_json_not_found_error(self, *args, **kwargs): + with self.app_context: + url = url_for("task_export_results_json", task_id=999) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/review_app/server/api/test_task_export_results_view.py b/test/review_app/server/api/test_task_export_results_view.py new file mode 100644 index 000000000..578d7eecc --- /dev/null +++ b/test/review_app/server/api/test_task_export_results_view.py @@ -0,0 +1,85 @@ +#!/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. + +import unittest +from unittest.mock import patch + +from flask import url_for + +from mephisto.abstractions.providers.prolific.api import status +from mephisto.client.review_app.server.api.views.task_export_results_view import ( + get_result_file_path, +) +from mephisto.data_model.constants.assignment_state import AssignmentState +from mephisto.data_model.unit import Unit +from mephisto.utils.testing import get_test_qualification +from mephisto.utils.testing import get_test_unit +from mephisto.utils.testing import get_test_worker +from test.review_app.server.api.base_test_api_view_case import BaseTestApiViewCase + + +class TestTaskExportResultsView(BaseTestApiViewCase): + @patch("mephisto.client.review_app.server.api.views.task_export_results_view.get_results_dir") + def test_task_export_result_success(self, mock_get_results_dir, *args, **kwargs): + mock_get_results_dir.return_value = self.data_dir + + # Unit + unit_id = get_test_unit(self.db) + unit: Unit = Unit.get(self.db, unit_id) + unit.set_db_status(AssignmentState.ACCEPTED) + + # Qualification + qualification_id = get_test_qualification(self.db) + + # Worker + _, worker_id = get_test_worker(self.db) + + # Unit Review + self.db.new_unit_review(unit_id, unit.task_id, worker_id, unit.db_status) + self.db.update_unit_review(unit_id, qualification_id, worker_id) + + results_file_data = "Test JS" + + results_file_path = get_result_file_path(self.data_dir, unit.task_id) + f = open(results_file_path, "w") + f.write(results_file_data) + f.close() + + with self.app_context: + url = url_for("task_export_results", task_id=unit.task_id) + response = self.client.get(url) + result = response.json + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(result, {"file_created": True}) + self.assertEqual(response.mimetype, "application/json") + + def test_task_export_result_not_found_error(self, *args, **kwargs): + with self.app_context: + url = url_for("task_export_results", task_id=999) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_task_export_result_not_reviews_error(self, *args, **kwargs): + unit_id = get_test_unit(self.db) + unit: Unit = Unit.get(self.db, unit_id) + unit.set_db_status(AssignmentState.COMPLETED) + + with self.app_context: + url = url_for("task_export_results", task_id=unit.task_id) + response = self.client.get(url) + result = response.json + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + result["error"], + "Task is still on review. Finish review before requesting for the results.", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/review_app/server/api/test_task_view.py b/test/review_app/server/api/test_task_view.py index 4bcb618b1..c580c4b71 100644 --- a/test/review_app/server/api/test_task_view.py +++ b/test/review_app/server/api/test_task_view.py @@ -21,7 +21,7 @@ def test_no_task_error(self, *args, **kwargs): result = response.json self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(result, {"error" : "Not found"}) + self.assertEqual(result, {"error": "Not found"}) def test_one_task_success(self, *args, **kwargs): task_name, task_id = get_test_task(self.db) diff --git a/test/review_app/server/api/test_unit_html_view.py b/test/review_app/server/api/test_unit_html_view.py index 2bae7233c..98f92f195 100644 --- a/test/review_app/server/api/test_unit_html_view.py +++ b/test/review_app/server/api/test_unit_html_view.py @@ -23,7 +23,7 @@ def test_html_success(self, *args, **kwargs): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue( - f"" in response.data.decode() + f'' in response.data.decode() ) diff --git a/test/review_app/server/api/test_units_approve_view.py b/test/review_app/server/api/test_units_approve_view.py index f7a77220b..d9955eab3 100644 --- a/test/review_app/server/api/test_units_approve_view.py +++ b/test/review_app/server/api/test_units_approve_view.py @@ -65,7 +65,7 @@ def test_units_approve_bonus_param_format_error(self, *args, **kwargs): with self.app_context: url = url_for("units_approve") - with self.assertLogs(level='ERROR') as cm: + with self.assertLogs(level="ERROR") as cm: response = self.client.post(url, json={"unit_ids": [unit_id], "bonus": bonus}) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -91,7 +91,7 @@ def test_units_approve_paying_bonus_error(self, mock_bonus_worker, *args, **kwar with self.app_context: url = url_for("units_approve") - with self.assertLogs(level='ERROR') as cm: + with self.assertLogs(level="ERROR") as cm: response = self.client.post(url, json={"unit_ids": [unit_id], "bonus": bonus}) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -116,7 +116,7 @@ def test_units_approve_paying_bonus_exception_error(self, mock_bonus_worker, *ar with self.app_context: url = url_for("units_approve") - with self.assertLogs(level='ERROR') as cm: + with self.assertLogs(level="ERROR") as cm: response = self.client.post(url, json={"unit_ids": [unit_id], "bonus": bonus}) self.assertEqual(response.status_code, status.HTTP_200_OK)