Skip to content

Commit

Permalink
[WIP] Task review app - initial API commit
Browse files Browse the repository at this point in the history
  • Loading branch information
meta-paul committed Aug 25, 2023
1 parent 9bcd52b commit c8faeb9
Show file tree
Hide file tree
Showing 26 changed files with 965 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ COPY . /mephisto
RUN mkdir ~/.mephisto

# Create the main Mephisto data directory
RUN mkdir /mephisto/data
RUN mkdir -p /mephisto/data

# Write the mephisto config file manually for now to avoid prompt.
# For bash-style string $ expansion for newlines,
Expand Down
4 changes: 4 additions & 0 deletions mephisto/abstractions/providers/prolific/api/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@
# 4xx
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_404_NOT_FOUND = 404

# 5xx
HTTP_500_INTERNAL_SERVER_ERROR = 500
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def init_tables(self) -> None:
c.execute(tables.CREATE_RUN_MAP_TABLE)
c.execute(tables.CREATE_PARTICIPANT_GROUPS_TABLE)
c.execute(tables.CREATE_PARTICIPANT_GROUP_QUALIFICATIONS_MAPPING_TABLE)
c.execute(tables.CREATE_UNIT_REVIEW_TABLE)
conn.commit()

def is_study_mapping_in_sync(self, unit_id: str, compare_time: float):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,22 @@
creation_date DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""

CREATE_UNIT_REVIEW_TABLE = """
CREATE TABLE IF NOT EXISTS unit_review (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unit_id TEXT NOT NULL,
worker_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
status TEXT NOT NULL,
feedback TEXT,
tips INTEGER,
blocked_worker BOOLEAN DEFAULT false,
/* ID of `db.qualifications` (not `db.granted_qualifications`) */
updated_qualification_id INTEGER,
updated_qualification_value INTEGER,
/* ID of `db.qualifications` (not `db.granted_qualifications`) */
revoked_qualification_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
53 changes: 53 additions & 0 deletions mephisto/client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@


from typing import List

from flask.cli import pass_script_info
from rich import print

from mephisto.abstractions.providers.prolific.provider_type import (
PROVIDER_TYPE as PROLIFIC_PROVIDER_TYPE,
)
from mephisto.client.cli_commands import get_wut_arguments
from mephisto.operations.registry import get_valid_provider_types
from mephisto.utils.rich import console, create_table
Expand Down Expand Up @@ -397,5 +403,52 @@ def metrics_cli(args):
shutdown_grafana_server()


@cli.command(
"review_app",
cls=RichCommand,
)
@click.option("-h", "--host", type=(str), default="127.0.0.1")
@click.option("-p", "--port", type=(int), default=5000)
@click.option("-d", "--debug", type=(bool), default=None)
@click.option("-P", "--provider", type=(str), default=PROLIFIC_PROVIDER_TYPE)
@pass_script_info
def review_app(
info,
host,
port,
debug,
provider,
):
"""
Launch a local review server.
Custom implementation of `flask run <app_name>` command (`flask.cli.run_command`)
"""
from flask.cli import show_server_banner
from flask.helpers import get_debug_flag
from flask.helpers import get_env
from werkzeug.serving import run_simple
from mephisto.client.review_app.server import create_app

debug = debug if debug is not None else get_debug_flag()
reload = debug
debugger = debug
eager_loading = not reload

# Flask banner
show_server_banner(get_env(), debug, info.app_import_path, eager_loading)

# Init App
app = create_app(provider=provider)

# Run Flask server
run_simple(
host,
port,
app,
use_reloader=reload,
use_debugger=debugger,
)


if __name__ == "__main__":
cli()
14 changes: 14 additions & 0 deletions mephisto/client/review_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## Preview App Docs

#### Run application

```shell
mephisto review_app --host 0.0.0.0 --port 5000 --debug True --provider prolific
```

where

- `-h`/`--host` - host address (optional, default: `"127.0.0.1"`)
- `-p`/`--port` - port (optional, default: `5000`)
- `-d`/`--debug` - debug (optional, default: `None`)
- `-P`/`--provider` - provider (optional, default: `"prolific"`)
5 changes: 5 additions & 0 deletions mephisto/client/review_app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/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.
92 changes: 92 additions & 0 deletions mephisto/client/review_app/server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/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
import traceback
from logging.config import dictConfig
from typing import Tuple

from flask import Flask
from werkzeug import Response
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
from werkzeug.utils import import_string

from mephisto.abstractions.database import EntryDoesNotExistException
from mephisto.abstractions.databases.local_database import LocalMephistoDB
from mephisto.abstractions.providers.prolific.api import status
from mephisto.abstractions.providers.prolific.api.exceptions import ProlificException
from mephisto.tools.data_browser import DataBrowser
from mephisto.utils.logger_core import get_logger
from .urls import init_urls

FLASK_SETTINGS_MODULE = os.environ.get(
'FLASK_SETTINGS_MODULE',
'mephisto.client.review_app.server.settings.base',
)


def create_app(provider: str) -> Flask:
# Logging
# TODO [Review APP]: Fix logging (it works in views only with `app.logger` somehow)
flask_logger = get_logger('')
settings = import_string(FLASK_SETTINGS_MODULE)
dictConfig(settings.LOGGING)

# Create and configure the app
app = Flask(__name__)

# Logger
app.logger = flask_logger

# Settings
app.config.from_object(FLASK_SETTINGS_MODULE)

# Databases
app.db = LocalMephistoDB()
app.data_browser = DataBrowser(db=app.db)
app.datastore = app.db.get_datastore_for_provider(provider)

# API URLS
init_urls(app)

# Logger for this module
logger = get_logger(name=__name__)

# Exceptions handlers
@app.errorhandler(WerkzeugHTTPException)
def handle_flask_exception(e: WerkzeugHTTPException) -> Response:
logger.error(''.join(traceback.format_tb(e.__traceback__)))
response = e.get_response()
response.data = json.dumps({
'error': e.description,
})
response.content_type = 'application/json'
return response

@app.errorhandler(Exception)
def handle_not_flask_exception(e: Exception) -> Tuple[dict, int]:
# Not to handle Flask exceptions here, pass it further to catch in `handle_flask_exception`
if isinstance(e, WerkzeugHTTPException):
return e

elif isinstance(e, ProlificException):
return {
'error': e.message,
}, status.HTTP_400_BAD_REQUEST

elif isinstance(e, EntryDoesNotExistException):
return {
'error': 'Not found',
}, status.HTTP_404_NOT_FOUND

# Other uncaught exceptions
logger.error(''.join(traceback.format_tb(e.__traceback__)))
return {
'error': str(e),
}, status.HTTP_500_INTERNAL_SERVER_ERROR

return app
5 changes: 5 additions & 0 deletions mephisto/client/review_app/server/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/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.
17 changes: 17 additions & 0 deletions mephisto/client/review_app/server/api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/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.

from .qualification_workers_view import QualificationWorkersView
from .qualifications_view import QualificationsView
from .qualify_worker_view import QualifyWorkerView
from .tasks_view import TasksView
from .tasks_worker_units_view import TasksWorkerUnitsView
from .units_approve_view import UnitsApproveView
from .units_details_view import UnitsDetailsView
from .units_reject_view import UnitsRejectView
from .units_soft_reject_view import UnitsSoftRejectView
from .units_view import UnitsView
from .worker_block_view import WorkerBlockView
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/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.

from typing import List
from typing import Optional

from flask import current_app as app
from flask import request
from flask.views import MethodView

from mephisto.abstractions.databases.local_database import LocalMephistoDB
from mephisto.abstractions.databases.local_database import nonesafe_int
from mephisto.abstractions.databases.local_database import StringIDRow


def _find_granted_qualifications(db: LocalMephistoDB, qualification_id: str) -> List[StringIDRow]:
""" Return the granted qualifications in the database by the given qualification id """

with db.table_access_condition:
conn = db._get_connection()
c = conn.cursor()
c.execute(
f"""
SELECT * FROM granted_qualifications
WHERE (qualification_id = ?1);
""",
(nonesafe_int(qualification_id),),
)

results = c.fetchall()
return results


def _find_unit_reviews(
datastore,
qualification_id: str,
worker_id: str,
task_id: Optional[str] = None,
) -> List[StringIDRow]:
"""
Return unit reviews in the datastore by the given Qualification ID, Worker ID and Task ID
"""

params = [nonesafe_int(qualification_id), nonesafe_int(worker_id)]
task_query = "AND (task_id = ?3)" if task_id else ""
if task_id:
params.append(nonesafe_int(task_id))

with datastore.table_access_condition:
conn = datastore._get_connection()
conn.set_trace_callback(print)
c = conn.cursor()
c.execute(
f"""
SELECT * FROM unit_review
WHERE (updated_qualification_id = ?1) AND (worker_id = ?2) {task_query}
ORDER BY created_at ASC;
""",
params,
)

results = c.fetchall()
return results


class QualificationWorkersView(MethodView):
def get(self, qualification_id) -> dict:
""" Get list of all bearers of a qualification. """

task_id = request.args.get('task_id')

db_qualification: StringIDRow = app.db.get_qualification(qualification_id)
app.logger.debug(f"Found qualification in DB: {dict(db_qualification)}")

db_granted_qualifications = _find_granted_qualifications(app.db, qualification_id)

app.logger.debug(
f"Found granted qualifications for this qualification in DB: "
f"{db_granted_qualifications}"
)

workers = []

for gq in db_granted_qualifications:
unit_reviews = _find_unit_reviews(
app.datastore, qualification_id, gq["worker_id"], task_id,
)

if unit_reviews:
latest_unit_review = unit_reviews[-1]
unit_review_id = latest_unit_review["id"]
granted_at = latest_unit_review["created_at"]
else:
continue

workers.append(
{
"worker_id": gq["worker_id"],
"value": gq["value"],
"unit_review_id": unit_review_id, # latest grant of this qualification
"granted_at": granted_at, # maps to `unit_review.created_at` column
}
)

return {
"workers": workers,
}
Loading

0 comments on commit c8faeb9

Please sign in to comment.