Régis Behmo ece1ab9983 Make xqueue actually do something
Xqueue was mostly broken in the previous releases. In this version, we
address the following issues:

- Proper initialisation by creating the right users
- Shift from 8040 to the more standard 8000 port
- Expose xqueue service on the internet for accessing its API
- Properly define the xqueue name ("openedx")
- Make sure that all logs go to the console
- Add convenient CLI for using the awkward Xqueue API
- Properly document how to use Xqueue
2020-05-05 16:49:38 +02:00

190 lines
5.7 KiB
Python

from glob import glob
import json
import os
import click
import pkg_resources
import requests
from tutor import config as tutor_config
from tutor.exceptions import TutorError
from .__about__ import __version__
config = {
"add": {
"AUTH_PASSWORD": "{{ 8|random_string }}",
"MYSQL_PASSWORD": "{{ 8|random_string }}",
"SECRET_KEY": "{{ 24|random_string }}",
},
"defaults": {
"VERSION": __version__,
"AUTH_USERNAME": "lms",
"DOCKER_IMAGE": "overhangio/openedx-xqueue:{{ XQUEUE_VERSION }}",
"HOST": "xqueue.{{ LMS_HOST }}",
"MYSQL_DATABASE": "xqueue",
"MYSQL_USERNAME": "xqueue",
},
}
templates = pkg_resources.resource_filename("tutorxqueue", "templates")
hooks = {
"init": ["mysql", "xqueue"],
"build-image": {"xqueue": "{{ DOCKER_REGISTRY }}{{ XQUEUE_DOCKER_IMAGE }}"},
"remote-image": {"xqueue": "{{ DOCKER_REGISTRY }}{{ XQUEUE_DOCKER_IMAGE }}"},
}
def patches():
all_patches = {}
for path in glob(
os.path.join(pkg_resources.resource_filename("tutorxqueue", "patches"), "*")
):
with open(path) as patch_file:
name = os.path.basename(path)
content = patch_file.read()
all_patches[name] = content
return all_patches
@click.group(help="Interact with the Xqueue server")
def command():
pass
@click.group(help="List and grade submissions")
@click.pass_obj
@click.option("-q", "--queue", default="openedx", show_default=True, help="Queue name")
@click.option(
"-u",
"--url",
help="Xqueue server base url. By default, this value will be defined from the plugin configuration.",
)
def submissions(context, queue, url):
context.queue = queue
context.url = url
@click.command(name="count", help="Count submissions in queue")
@click.pass_obj
def count_submissions(context):
print_result(context, "count_submissions", context.queue)
@click.command(name="show", help="Show last submission")
@click.pass_obj
def show_submission(context):
print_result(context, "show_submission", context.queue)
@click.command(name="grade", help="Grade a specific submission")
@click.argument("submission_id")
@click.argument("submission_key")
@click.argument("grade", type=click.FLOAT)
@click.argument("correct", type=click.BOOL)
@click.argument("message")
@click.pass_obj
def grade_submission(context, submission_id, submission_key, grade, correct, message):
print_result(
context,
"grade_submission",
submission_id,
submission_key,
grade,
correct,
message,
)
def print_result(context, client_func_name, *args, **kwargs):
user_config = tutor_config.load(context.root)
client = Client(user_config, url=context.url)
func = getattr(client, client_func_name)
result = func(*args, **kwargs)
print(json.dumps(result, indent=2))
class Client:
def __init__(self, user_config, url=None):
self._session = None
self.username = user_config["XQUEUE_AUTH_USERNAME"]
self.password = user_config["XQUEUE_AUTH_PASSWORD"]
self.base_url = url
if not self.base_url:
scheme = "https" if user_config["ACTIVATE_HTTPS"] else "http"
host = host or user_config["XQUEUE_HOST"]
self.base_url = "{}://{}".format(scheme, host)
self.login()
@property
def session(self):
if self._session is None:
self._session = requests.Session()
return self._session
def url(self, endpoint):
# Don't forget to add a trailing slash to all endpoints: this is how xqueue
# works...
return self.base_url + endpoint
def login(self):
response = self.request(
"/xqueue/login/",
method="POST",
data={"username": self.username, "password": self.password},
)
message = response.get("content")
if message != "Logged in":
raise TutorError(
"Could not login to xqueue server at {}. Response: '{}'".format(
self.base_url, message
)
)
def show_submission(self, queue):
response = self.request("/xqueue/get_submission/", params={"queue_name": queue})
if response["return_code"] != 0:
return response
data = json.loads(response["content"])
header = json.loads(data["xqueue_header"])
submission_body = json.loads(data["xqueue_body"])
submission_id = header["submission_id"]
submission_key = header["submission_key"]
return {
"id": submission_id,
"key": submission_key,
"body": submission_body,
"return_code": response["return_code"],
}
def count_submissions(self, queue):
return self.request("/xqueue/get_queuelen/", params={"queue_name": queue})
def grade_submission(self, submission_id, submission_key, grade, correct, msg):
return self.request(
"/xqueue/put_result/",
method="POST",
data={
"xqueue_header": json.dumps(
{"submission_id": submission_id, "submission_key": submission_key}
),
"xqueue_body": json.dumps(
{"correct": correct, "score": grade, "msg": msg}
),
},
)
def request(self, endpoint, method="GET", data=None, params=None):
func = getattr(self.session, method.lower())
response = func(self.url(endpoint), data=data, params=params)
# TODO handle errors >= 400 and non-parsable json responses
return response.json()
submissions.add_command(count_submissions)
submissions.add_command(show_submission)
submissions.add_command(grade_submission)
command.add_command(submissions)