From 7bfcca05ad6ed30da5ec96ef05fa6441690cdbe3 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Fri, 14 Apr 2017 22:24:11 +0430 Subject: [PATCH 01/21] Basic features of API --- cms/server/admin/handlers/__init__.py | 16 +- cms/server/admin/handlers/api.py | 245 ++++++++++++++++++ cms/server/contest/handlers/__init__.py | 7 + cms/server/contest/handlers/api.py | 316 ++++++++++++++++++++++++ 4 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 cms/server/admin/handlers/api.py create mode 100644 cms/server/contest/handlers/api.py diff --git a/cms/server/admin/handlers/__init__.py b/cms/server/admin/handlers/__init__.py index 6ae8f1a4c0..f108983040 100644 --- a/cms/server/admin/handlers/__init__.py +++ b/cms/server/admin/handlers/__init__.py @@ -103,7 +103,13 @@ from .usertest import \ UserTestHandler, \ UserTestFileHandler - +from .api import \ + APITaskTypesHandler, \ + APIScoreTypesHandler, \ + APILanguages, \ + APIAddTask, \ + APIModifyTask, \ + APIAddTestcase HANDLERS = [ (r"/", OverviewHandler), @@ -216,6 +222,14 @@ (r"/user_test/([0-9]+)(?:/([0-9]+))?", UserTestHandler), (r"/user_test_file/([0-9]+)", UserTestFileHandler), + + # API + (r"/api/tasktypes", APITaskTypesHandler), + (r"/api/scoretypes", APIScoreTypesHandler), + (r"/api/languages", APILanguages), + (r"/api/tasks/add", APIAddTask), + (r"/api/task/([0-9]+)/modify", APIModifyTask), + (r"/api/task/([0-9]+)/testcases/add", APIAddTestcase), ] diff --git a/cms/server/admin/handlers/api.py b/cms/server/admin/handlers/api.py new file mode 100644 index 0000000000..7195e47650 --- /dev/null +++ b/cms/server/admin/handlers/api.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2010-2013 Giovanni Mascellani +# Copyright © 2010-2017 Stefano Maggiolo +# Copyright © 2010-2012 Matteo Boscariol +# Copyright © 2012-2014 Luca Wehrstedt +# Copyright © 2014 Artem Iglikov +# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> +# Copyright © 2016 Myungwoo Chun +# TODO: add your name +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Handlers for the API related to AWS + +""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import json +import logging +import traceback + +import tornado.web + +from cms.db import Attachment, Dataset, Session, Statement, Submission, \ + SubmissionFormatElement, Task, Testcase +from cmscommon.datetime import make_datetime +from cms import plugin_list +from cms.grading.languagemanager import LANGUAGES + +from .base import BaseHandler, SimpleHandler, require_permission + + +logger = logging.getLogger(__name__) + + +class APITaskTypesHandler(BaseHandler): + """Writes a list of task types names + + """ + + @require_permission(BaseHandler.PERMISSION_ALL) + def get(self): + task_type_list = plugin_list("cms.grading.tasktypes", "tasktypes") + task_types_name = [task_type.__name__ for task_type in task_type_list] + return self.write(json.dumps(task_types_name)) + + +class APIScoreTypesHandler(BaseHandler): + """Writes a list of task types names + + """ + + @require_permission(BaseHandler.PERMISSION_ALL) + def get(self): + score_type_list = plugin_list("cms.grading.scoretypes", "scoretypes") + score_types_name = [score_type.__name__ for score_type in score_type_list] + return self.write(json.dumps(score_types_name)) + + +class APILanguages(BaseHandler): + """Writes a list of language names + + """ + + @require_permission(BaseHandler.PERMISSION_ALL) + def get(self): + language_names = [lang.name for lang in LANGUAGES] + return self.write(json.dumps(language_names)) + + +class APIAddTask(BaseHandler): + """Creates a new task. + + Based on AddTaskHandler + """ + + @require_permission(BaseHandler.PERMISSION_ALL) + def post(self): + # + # TODO: helpers and managers + # TODO: add to a contest + # + try: + attrs = dict() + + self.get_string(attrs, "name", empty=None) + assert attrs.get("name") is not None, "No task name specified." + attrs["title"] = attrs["name"] + + # Set default submission format as ["taskname.%l"] + attrs["submission_format"] = \ + [SubmissionFormatElement("%s.%%l" % attrs["name"])] + + # Create the task. + task = Task(**attrs) + self.sql_session.add(task) + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + try: + attrs = dict() + + # Create its first dataset. + attrs["description"] = "Default" + attrs["autojudge"] = True + self.get_time_limit(attrs, "time_limit") + self.get_memory_limit(attrs, "memory_limit") + self.get_task_type(attrs, "task_type", "task_type_parameters_") + self.get_score_type(attrs, "score_type", "score_type_parameters") + attrs["task"] = task + dataset = Dataset(**attrs) + self.sql_session.add(dataset) + + # Make the dataset active. Life works better that way. + task.active_dataset = dataset + + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + if self.try_commit(): + # Create the task on RWS. + self.application.service.proxy_service.reinitialize() + self.write('%d' % task.id) + else: + raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + + +class APIModifyTask(BaseHandler): + """Updates an existing task. + + Based on TaskHandler + """ + + @require_permission(BaseHandler.PERMISSION_ALL) + def post(self, task_id): + # + # TODO: helpers and managers + # + task = self.safe_get_item(Task, task_id) + + try: + attrs = task.get_attrs() + + self.get_string(attrs, "name", empty=None) + assert attrs.get("name") is not None, "No task name specified." + attrs["title"] = attrs["name"] + + # Set default submission format as ["taskname.%l"] + attrs["submission_format"] = \ + [SubmissionFormatElement("%s.%%l" % attrs["name"])] + + # Update the task. + task.set_attrs(attrs) + + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + try: + dataset = task.active_dataset + attrs = dataset.get_attrs() + + # Create its first dataset. + self.get_time_limit(attrs, "time_limit") + self.get_memory_limit(attrs, "memory_limit") + self.get_task_type(attrs, "task_type", "task_type_parameters_") + self.get_score_type(attrs, "score_type", "score_type_parameters") + + # Update the dataset. + dataset.set_attrs(attrs) + + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + if self.try_commit(): + # Update the task and score on RWS. + self.application.service.proxy_service.dataset_updated( + task_id=task.id) + self.write('%d' % task.id) + else: + raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + + +class APIAddTestcase(BaseHandler): + """Add a testcase to the task's active dataset. + + Based on AddTestcaseHandler + """ + + @require_permission(BaseHandler.PERMISSION_ALL) + def post(self, task_id): + task = self.safe_get_item(Task, task_id) + dataset = task.active_dataset + + codename = self.get_argument("testcase_id") + + try: + input_ = self.request.files["input"][0] + output = self.request.files["output"][0] + except KeyError: + raise tornado.web.HTTPError(403, "Invalid data: Please fill both input and output.") + + public = True + task_name = task.name + self.sql_session.close() + + try: + input_digest = \ + self.application.service.file_cacher.put_file_content( + input_["body"], + "Testcase input for task %s" % task_name) + output_digest = \ + self.application.service.file_cacher.put_file_content( + output["body"], + "Testcase output for task %s" % task_name) + except Exception as error: + raise tornado.web.HTTPError(403, "Testcase storage failed: %s" % error) + + self.sql_session = Session() + + testcase = Testcase( + codename, public, input_digest, output_digest, dataset=dataset) + self.sql_session.add(testcase) + + if self.try_commit(): + # max_score and/or extra_headers might have changed. + self.application.service.proxy_service.reinitialize() + self.write('%d' % testcase.id) + else: + raise tornado.web.HTTPError(403, "Operation Unsuccessful!") diff --git a/cms/server/contest/handlers/__init__.py b/cms/server/contest/handlers/__init__.py index 541b6145ee..da0c78ffc6 100644 --- a/cms/server/contest/handlers/__init__.py +++ b/cms/server/contest/handlers/__init__.py @@ -61,6 +61,9 @@ from .communication import \ CommunicationHandler, \ QuestionHandler +from .api import \ + APIGenerateOutput, \ + APISumbitionDetails HANDLERS = [ @@ -106,6 +109,10 @@ (r"/communication", CommunicationHandler), (r"/question", QuestionHandler), + + # API + (r"/api/task/([0-9]+)/testcase/([0-9]+)/run", APIGenerateOutput), + (r"/api/task/([0-9]+)/test/([0-9]+)/result", APISumbitionDetails), ] diff --git a/cms/server/contest/handlers/api.py b/cms/server/contest/handlers/api.py new file mode 100644 index 0000000000..328bad2f40 --- /dev/null +++ b/cms/server/contest/handlers/api.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2010-2013 Giovanni Mascellani +# Copyright © 2010-2017 Stefano Maggiolo +# Copyright © 2010-2012 Matteo Boscariol +# Copyright © 2012-2014 Luca Wehrstedt +# Copyright © 2014 Artem Iglikov +# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> +# Copyright © 2016 Myungwoo Chun +# TODO: add your name +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Handlers for the API related to AWS + +""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import io +import logging +import os +import pickle +import re +import json + +from urllib import quote + +import tornado.web + +from sqlalchemy import func + +from cms import config +from cms.db import Task, UserTest, UserTestFile, UserTestManager, Testcase +from cms.grading.languagemanager import get_language +from cms.grading.tasktypes import get_task_type +from cms.server import actual_phase_required, format_size +from cmscommon.archive import Archive +from cmscommon.crypto import encrypt_number +from cmscommon.datetime import make_timestamp +from cmscommon.mimetypes import get_type_for_file_name + +from .base import BaseHandler, FileHandler, \ + NOTIFICATION_ERROR, NOTIFICATION_SUCCESS + + +logger = logging.getLogger(__name__) + + +class APIGenerateOutput(BaseHandler): + """Creates a user test on a task for a perticular testcase + + Based on UserTestHandler + """ + def safe_get_item(self, cls, ident, session=None): + """Get item from database of class cls and id ident, using + session if given, or self.sql_session if not given. If id is + not found, raise a 404. + + cls (type): class of object to retrieve. + ident (string): id of object. + session (Session|None): session to use. + + return (object): the object with the given id. + + raise (HTTPError): 404 if not found. + + """ + if session is None: + session = self.sql_session + entity = cls.get_from_id(ident, session) + if entity is None: + raise tornado.web.HTTPError(404) + return entity + + @tornado.web.authenticated + @actual_phase_required(0) + def post(self, task_id, testcase_id): + participation = self.current_user + + task = self.safe_get_item(Task, task_id) + testcase = self.safe_get_item(Testcase, testcase_id) + + input_digest = testcase.input + managers_names = [manager.filename for manager in task.active_dataset.managers.values()] + + # Check that the task is testable + task_type = get_task_type(dataset=task.active_dataset) + if not task_type.testable: + raise tornado.web.HTTPError(403, "This task type is not testable") + + # Alias for easy access + contest = self.contest + + # Required files from the user. + required = set([sfe.filename for sfe in task.submission_format] + + task_type.get_user_managers(task.submission_format) + + ["input"]) + + # TODO: Archive?? + + # Ensure that the user did not submit multiple files with the + # same name. + if any(len(filename) != 1 for filename in self.request.files.values()): + raise tornado.web.HTTPError(403, "Please select the correct files.") + + # This ensure that the user sent one file for every name in + # submission format and no more. + provided = set(list(self.request.files.keys()) + ["input"] + managers_names) + if not (required == provided): + raise tornado.web.HTTPError(403, "Please select the correct files.") + + # Add submitted files. After this, files is a dictionary indexed + # by *our* filenames (something like "output01.txt" or + # "taskname.%l", and whose value is a couple + # (user_assigned_filename, content). + files = {} + for uploaded, data in self.request.files.iteritems(): + files[uploaded] = (data[0]["filename"], data[0]["body"]) + + # Read the submission language provided in the request; we + # integrate it with the language fetched from the previous + # submission (if we use it) and later make sure it is + # recognized and allowed. + submission_lang = self.get_argument("language", None) + need_lang = any(our_filename.find(".%l") != -1 + for our_filename in files) + + # Throw an error if task needs a language, but we don't have + # it or it is not allowed / recognized. + if need_lang: + error = None + if submission_lang is None: + error = self._("Cannot recognize the user test language.") + elif submission_lang not in contest.languages: + error = self._("Language %s not allowed in this contest.") \ + % submission_lang + if error is not None: + raise tornado.web.HTTPError(403, "%s" % error) + + # Check if submitted files are small enough. + if any([len(f[1]) > config.max_submission_length + for n, f in files.items() if n != "input"]): + raise tornado.web.HTTPError(403, + "Each source file must be at most %d bytes long." + % config.max_submission_length) + + # All checks done, submission accepted. + + # Attempt to store the submission locally to be able to + # recover a failure. + if config.tests_local_copy: + try: + path = os.path.join( + config.tests_local_copy_path.replace("%s", + config.data_dir), + participation.user.username) + if not os.path.exists(path): + os.makedirs(path) + # Pickle in ASCII format produces str, not unicode, + # therefore we open the file in binary mode. + with io.open( + os.path.join(path, + "%d" % make_timestamp(self.timestamp)), + "wb") as file_: + pickle.dump((self.contest.id, + participation.user.id, + task.id, + files), file_) + except Exception as error: + logger.error("Test local copy failed.", exc_info=True) + + # We now have to send all the files to the destination... + file_digests = {} + try: + for filename in files: + digest = self.application.service.file_cacher.put_file_content( + files[filename][1], + "Test file %s sent by %s at %d." % ( + filename, participation.user.username, + make_timestamp(self.timestamp))) + file_digests[filename] = digest + + # Now Adding managers' digests + for manager in task.active_dataset.managers.values(): + file_digests[manager.filename] = manager.digest + + # Finally Adding testcase's digest + file_digests["input"] = input_digest + + # In case of error, the server aborts the submission + except Exception as error: + logger.error("Storage failed! %s", error) + raise tornado.web.HTTPError(403, "Test storage failed!") + + # All the files are stored, ready to submit! + logger.info("All files stored for test sent by %s", + participation.user.username) + user_test = UserTest(self.timestamp, + submission_lang, + file_digests["input"], + participation=participation, + task=task) + + for filename in [sfe.filename for sfe in task.submission_format]: + digest = file_digests[filename] + self.sql_session.add( + UserTestFile(filename, digest, user_test=user_test)) + for filename in task_type.get_user_managers(task.submission_format): + digest = file_digests[filename] + if submission_lang is not None: + extension = get_language(submission_lang).source_extension + filename = filename.replace(".%l", extension) + self.sql_session.add( + UserTestManager(filename, digest, user_test=user_test)) + + self.sql_session.add(user_test) + self.sql_session.commit() + self.application.service.evaluation_service.new_user_test( + user_test_id=user_test.id) + # The argument (encripted user test id) is not used by CWS + # (nor it discloses information to the user), but it is useful + # for automatic testing to obtain the user test id). + self.write('%d' % user_test.id) + + +class APISumbitionDetails(BaseHandler): + """Gets the result of the sumbission + + Based on UserTestDetailsHandler + """ + def safe_get_item(self, cls, ident, session=None): + """Get item from database of class cls and id ident, using + session if given, or self.sql_session if not given. If id is + not found, raise a 404. + + cls (type): class of object to retrieve. + ident (string): id of object. + session (Session|None): session to use. + + return (object): the object with the given id. + + raise (HTTPError): 404 if not found. + + """ + if session is None: + session = self.sql_session + entity = cls.get_from_id(ident, session) + if entity is None: + raise tornado.web.HTTPError(404) + return entity + + @tornado.web.authenticated + @actual_phase_required(0) + def get(self, task_id, user_test_num): + participation = self.current_user + + if not self.r_params["testing_enabled"]: + raise tornado.web.HTTPError(404) + + task = self.safe_get_item(Task, task_id) + + user_test = self.sql_session.query(UserTest) \ + .filter(UserTest.participation == participation) \ + .filter(UserTest.task == task) \ + .order_by(UserTest.timestamp) \ + .offset(int(user_test_num) - 1) \ + .first() + if user_test is None: + raise tornado.web.HTTPError(404) + + tr = user_test.get_result(task.active_dataset) + if tr is None: + raise tornado.web.HTTPError(404) + + result = {} + + if tr is not None and tr.evaluated(): + result['evalres'] = tr.evaluation_text + else: + result['evalres'] = 'none' + self.write(json.dumps(result)) + return + + if tr is not None and tr.compiled(): + result['compiled'] = tr.compilation_text + else: + result['compiled'] = 'none' + self.write(json.dumps(result)) + return + + if tr.compilation_time is None: + result['time'] = 'none' + else: + result['time'] = tr.compilation_time + + if tr.compilation_memory is None: + result['memory'] = 'none' + else: + result['memory'] = tr.compilation_memory + + self.write(json.dumps(result)) From 78891612144d66746e59bb70db1df528b382a0b7 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 27 Apr 2017 10:58:38 +0430 Subject: [PATCH 02/21] API working as a service --- cms/conf.py | 4 + cms/server/admin/handlers/__init__.py | 15 - cms/server/admin/handlers/api.py | 245 --------- cms/server/api/__init__.py | 27 + cms/server/api/handlers/__init__.py | 50 ++ cms/server/api/handlers/base.py | 488 ++++++++++++++++++ .../api.py => api/handlers/handler.py} | 322 ++++++++---- cms/server/api/server.py | 74 +++ cms/server/contest/handlers/__init__.py | 8 - scripts/cmsAPIWebServer | 57 ++ setup.py | 4 +- 11 files changed, 927 insertions(+), 367 deletions(-) delete mode 100644 cms/server/admin/handlers/api.py create mode 100644 cms/server/api/__init__.py create mode 100644 cms/server/api/handlers/__init__.py create mode 100644 cms/server/api/handlers/base.py rename cms/server/{contest/handlers/api.py => api/handlers/handler.py} (51%) create mode 100644 cms/server/api/server.py create mode 100644 scripts/cmsAPIWebServer diff --git a/cms/conf.py b/cms/conf.py index 558b24db3d..4517eab345 100644 --- a/cms/conf.py +++ b/cms/conf.py @@ -110,6 +110,10 @@ def __init__(self): self.admin_listen_port = 8889 self.admin_cookie_duration = 10 * 60 * 60 # 10 hours + # APIWebServer. + self.api_listen_address = "" + self.api_listen_port = 8890 + # ProxyService. self.rankings = ["http://usern4me:passw0rd@localhost:8890/"] self.https_certfile = None diff --git a/cms/server/admin/handlers/__init__.py b/cms/server/admin/handlers/__init__.py index f108983040..2142b425d8 100644 --- a/cms/server/admin/handlers/__init__.py +++ b/cms/server/admin/handlers/__init__.py @@ -103,13 +103,6 @@ from .usertest import \ UserTestHandler, \ UserTestFileHandler -from .api import \ - APITaskTypesHandler, \ - APIScoreTypesHandler, \ - APILanguages, \ - APIAddTask, \ - APIModifyTask, \ - APIAddTestcase HANDLERS = [ (r"/", OverviewHandler), @@ -222,14 +215,6 @@ (r"/user_test/([0-9]+)(?:/([0-9]+))?", UserTestHandler), (r"/user_test_file/([0-9]+)", UserTestFileHandler), - - # API - (r"/api/tasktypes", APITaskTypesHandler), - (r"/api/scoretypes", APIScoreTypesHandler), - (r"/api/languages", APILanguages), - (r"/api/tasks/add", APIAddTask), - (r"/api/task/([0-9]+)/modify", APIModifyTask), - (r"/api/task/([0-9]+)/testcases/add", APIAddTestcase), ] diff --git a/cms/server/admin/handlers/api.py b/cms/server/admin/handlers/api.py deleted file mode 100644 index 7195e47650..0000000000 --- a/cms/server/admin/handlers/api.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -# Contest Management System - http://cms-dev.github.io/ -# Copyright © 2010-2013 Giovanni Mascellani -# Copyright © 2010-2017 Stefano Maggiolo -# Copyright © 2010-2012 Matteo Boscariol -# Copyright © 2012-2014 Luca Wehrstedt -# Copyright © 2014 Artem Iglikov -# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> -# Copyright © 2016 Myungwoo Chun -# TODO: add your name -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -"""Handlers for the API related to AWS - -""" -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -import json -import logging -import traceback - -import tornado.web - -from cms.db import Attachment, Dataset, Session, Statement, Submission, \ - SubmissionFormatElement, Task, Testcase -from cmscommon.datetime import make_datetime -from cms import plugin_list -from cms.grading.languagemanager import LANGUAGES - -from .base import BaseHandler, SimpleHandler, require_permission - - -logger = logging.getLogger(__name__) - - -class APITaskTypesHandler(BaseHandler): - """Writes a list of task types names - - """ - - @require_permission(BaseHandler.PERMISSION_ALL) - def get(self): - task_type_list = plugin_list("cms.grading.tasktypes", "tasktypes") - task_types_name = [task_type.__name__ for task_type in task_type_list] - return self.write(json.dumps(task_types_name)) - - -class APIScoreTypesHandler(BaseHandler): - """Writes a list of task types names - - """ - - @require_permission(BaseHandler.PERMISSION_ALL) - def get(self): - score_type_list = plugin_list("cms.grading.scoretypes", "scoretypes") - score_types_name = [score_type.__name__ for score_type in score_type_list] - return self.write(json.dumps(score_types_name)) - - -class APILanguages(BaseHandler): - """Writes a list of language names - - """ - - @require_permission(BaseHandler.PERMISSION_ALL) - def get(self): - language_names = [lang.name for lang in LANGUAGES] - return self.write(json.dumps(language_names)) - - -class APIAddTask(BaseHandler): - """Creates a new task. - - Based on AddTaskHandler - """ - - @require_permission(BaseHandler.PERMISSION_ALL) - def post(self): - # - # TODO: helpers and managers - # TODO: add to a contest - # - try: - attrs = dict() - - self.get_string(attrs, "name", empty=None) - assert attrs.get("name") is not None, "No task name specified." - attrs["title"] = attrs["name"] - - # Set default submission format as ["taskname.%l"] - attrs["submission_format"] = \ - [SubmissionFormatElement("%s.%%l" % attrs["name"])] - - # Create the task. - task = Task(**attrs) - self.sql_session.add(task) - except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) - - try: - attrs = dict() - - # Create its first dataset. - attrs["description"] = "Default" - attrs["autojudge"] = True - self.get_time_limit(attrs, "time_limit") - self.get_memory_limit(attrs, "memory_limit") - self.get_task_type(attrs, "task_type", "task_type_parameters_") - self.get_score_type(attrs, "score_type", "score_type_parameters") - attrs["task"] = task - dataset = Dataset(**attrs) - self.sql_session.add(dataset) - - # Make the dataset active. Life works better that way. - task.active_dataset = dataset - - except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) - - if self.try_commit(): - # Create the task on RWS. - self.application.service.proxy_service.reinitialize() - self.write('%d' % task.id) - else: - raise tornado.web.HTTPError(403, "Operation Unsuccessful!") - - -class APIModifyTask(BaseHandler): - """Updates an existing task. - - Based on TaskHandler - """ - - @require_permission(BaseHandler.PERMISSION_ALL) - def post(self, task_id): - # - # TODO: helpers and managers - # - task = self.safe_get_item(Task, task_id) - - try: - attrs = task.get_attrs() - - self.get_string(attrs, "name", empty=None) - assert attrs.get("name") is not None, "No task name specified." - attrs["title"] = attrs["name"] - - # Set default submission format as ["taskname.%l"] - attrs["submission_format"] = \ - [SubmissionFormatElement("%s.%%l" % attrs["name"])] - - # Update the task. - task.set_attrs(attrs) - - except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) - - try: - dataset = task.active_dataset - attrs = dataset.get_attrs() - - # Create its first dataset. - self.get_time_limit(attrs, "time_limit") - self.get_memory_limit(attrs, "memory_limit") - self.get_task_type(attrs, "task_type", "task_type_parameters_") - self.get_score_type(attrs, "score_type", "score_type_parameters") - - # Update the dataset. - dataset.set_attrs(attrs) - - except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) - - if self.try_commit(): - # Update the task and score on RWS. - self.application.service.proxy_service.dataset_updated( - task_id=task.id) - self.write('%d' % task.id) - else: - raise tornado.web.HTTPError(403, "Operation Unsuccessful!") - - -class APIAddTestcase(BaseHandler): - """Add a testcase to the task's active dataset. - - Based on AddTestcaseHandler - """ - - @require_permission(BaseHandler.PERMISSION_ALL) - def post(self, task_id): - task = self.safe_get_item(Task, task_id) - dataset = task.active_dataset - - codename = self.get_argument("testcase_id") - - try: - input_ = self.request.files["input"][0] - output = self.request.files["output"][0] - except KeyError: - raise tornado.web.HTTPError(403, "Invalid data: Please fill both input and output.") - - public = True - task_name = task.name - self.sql_session.close() - - try: - input_digest = \ - self.application.service.file_cacher.put_file_content( - input_["body"], - "Testcase input for task %s" % task_name) - output_digest = \ - self.application.service.file_cacher.put_file_content( - output["body"], - "Testcase output for task %s" % task_name) - except Exception as error: - raise tornado.web.HTTPError(403, "Testcase storage failed: %s" % error) - - self.sql_session = Session() - - testcase = Testcase( - codename, public, input_digest, output_digest, dataset=dataset) - self.sql_session.add(testcase) - - if self.try_commit(): - # max_score and/or extra_headers might have changed. - self.application.service.proxy_service.reinitialize() - self.write('%d' % testcase.id) - else: - raise tornado.web.HTTPError(403, "Operation Unsuccessful!") diff --git a/cms/server/api/__init__.py b/cms/server/api/__init__.py new file mode 100644 index 0000000000..c03d9f4682 --- /dev/null +++ b/cms/server/api/__init__.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2017 Kiarash Golezardi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from .server import APIWebServer + + +__all__ = ["APIWebServer"] diff --git a/cms/server/api/handlers/__init__.py b/cms/server/api/handlers/__init__.py new file mode 100644 index 0000000000..9945a07ca1 --- /dev/null +++ b/cms/server/api/handlers/__init__.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2017 Kiarash Golezardi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from .handler import \ + TestHandler, \ + TaskTypesHandler, \ + ScoreTypesHandler, \ + LanguagesHandler, \ + AddTaskHandler, \ + ModifyTaskHandler, \ + AddTestcaseHandler, \ + GenerateOutputHandler, \ + SubmissionDetailsHandler + +HANDLERS = [ + (r"/", TestHandler), + (r"/tasktypes", TaskTypesHandler), + (r"/scoretypes", ScoreTypesHandler), + (r"/languages", LanguagesHandler), + (r"/tasks/add", AddTaskHandler), + + (r"/task/([0-9]+)/modify", ModifyTaskHandler), + (r"/task/([0-9]+)/testcases/add", AddTestcaseHandler), + + (r"/task/([0-9]+)/testcase/([0-9]+)/run", GenerateOutputHandler), + (r"/task/([0-9]+)/test/([0-9]+)/result", SubmissionDetailsHandler), +] + + +__all__ = ["HANDLERS"] diff --git a/cms/server/api/handlers/base.py b/cms/server/api/handlers/base.py new file mode 100644 index 0000000000..0de2ec0109 --- /dev/null +++ b/cms/server/api/handlers/base.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2010-2013 Giovanni Mascellani +# Copyright © 2010-2016 Stefano Maggiolo +# Copyright © 2010-2012 Matteo Boscariol +# Copyright © 2012-2016 Luca Wehrstedt +# Copyright © 2014 Artem Iglikov +# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> +# Copyright © 2016 Myungwoo Chun +# Copyright © 2016 Peyman Jabbarzade Ganje +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Base class for all handlers in AWS, and some utility functions. + +""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import json +import logging +import traceback + +from datetime import datetime, timedelta +from functools import wraps + +import tornado.web + +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import subqueryload + +from cms import __version__ +from cms.db import Admin, Contest, Participation, Question, \ + Submission, SubmissionFormatElement, SubmissionResult, Task, Team, User, \ + UserTest +from cms.grading.scoretypes import get_score_type_class +from cms.grading.tasktypes import get_task_type_class +from cms.server import CommonRequestHandler, file_handler_gen, get_url_root +from cmscommon.datetime import make_datetime + + +logger = logging.getLogger(__name__) + + +def argument_reader(func, empty=None): + """Return an helper method for reading and parsing form values. + + func (function): the parser and validator for the value. + empty (object): the value to store if an empty string is retrieved. + + return (function): a function to be used as a method of a + RequestHandler. + + """ + def helper(self, dest, name, empty=empty): + """Read the argument called "name" and save it in "dest". + + self (RequestHandler): a thing with a get_argument method. + dest (dict): a place to store the obtained value. + name (string): the name of the argument and of the item. + empty (object): overrides the default empty value. + + """ + value = self.get_argument(name, None) + if value is None: + return + if value == "": + dest[name] = empty + else: + dest[name] = func(value) + return helper + + +def parse_int(value): + """Parse and validate an integer.""" + try: + return int(value) + except: + raise ValueError("Can't cast %s to int." % value) + + +def parse_timedelta_sec(value): + """Parse and validate a timedelta (as number of seconds).""" + try: + return timedelta(seconds=float(value)) + except: + raise ValueError("Can't cast %s to timedelta." % value) + + +def parse_timedelta_min(value): + """Parse and validate a timedelta (as number of minutes).""" + try: + return timedelta(minutes=float(value)) + except: + raise ValueError("Can't cast %s to timedelta." % value) + + +def parse_datetime(value): + """Parse and validate a datetime (in pseudo-ISO8601).""" + if '.' not in value: + value += ".0" + try: + return datetime.strptime(value, "%Y-%m-%d %H:%M:%S.%f") + except: + raise ValueError("Can't cast %s to datetime." % value) + + +def parse_ip_address_or_subnet(ip_list): + """Validate a comma-separated list of IP addresses or subnets.""" + for value in ip_list.split(","): + address, sep, subnet = value.partition("/") + if sep != "": + subnet = int(subnet) + assert 0 <= subnet < 32 + fields = address.split(".") + assert len(fields) == 4 + for field in fields: + num = int(field) + assert 0 <= num < 256 + return ip_list + + +class BaseHandler(CommonRequestHandler): + """Base RequestHandler for this application. + + All the RequestHandler classes in this application should be a + child of this class. + + """ + + def __init__(self, *args, **kwargs): + super(BaseHandler, self).__init__(*args, **kwargs) + self._ = str # No translation + + def try_commit(self): + """Try to commit the current session. + + If not successful display a warning in the webpage. + + return (bool): True if commit was successful, False otherwise. + + """ + try: + self.sql_session.commit() + except IntegrityError as error: + return False + else: + return True + + def safe_get_item(self, cls, ident, session=None): + """Get item from database of class cls and id ident, using + session if given, or self.sql_session if not given. If id is + not found, raise a 404. + + cls (type): class of object to retrieve. + ident (string): id of object. + session (Session|None): session to use. + + return (object): the object with the given id. + + raise (HTTPError): 404 if not found. + + """ + if session is None: + session = self.sql_session + entity = cls.get_from_id(ident, session) + if entity is None: + raise tornado.web.HTTPError(404) + return entity + + def prepare(self): + """This method is executed at the beginning of each request. + + """ + super(BaseHandler, self).prepare() + self.r_params = self.render_params() + + def render_params(self): + """Return the default render params used by almost all handlers. + + return (dict): default render params + + """ + params = {} + params["rtd_version"] = "latest" if "dev" in __version__ \ + else "v" + __version__[:3] + params["timestamp"] = make_datetime() + params["contest"] = self.contest + params["url_root"] = get_url_root(self.request.path) + if self.current_user is not None: + params["current_user"] = self.current_user + if self.contest is not None: + params["phase"] = self.contest.phase(params["timestamp"]) + # Keep "== None" in filter arguments. SQLAlchemy does not + # understand "is None". + params["unanswered"] = self.sql_session.query(Question)\ + .join(Participation)\ + .filter(Participation.contest_id == self.contest.id)\ + .filter(Question.reply_timestamp == None)\ + .filter(Question.ignored == False)\ + .count() # noqa + # TODO: not all pages require all these data. + params["contest_list"] = self.sql_session.query(Contest).all() + params["task_list"] = self.sql_session.query(Task).all() + params["user_list"] = self.sql_session.query(User).all() + params["team_list"] = self.sql_session.query(Team).all() + return params + + def finish(self, *args, **kwds): + """Finish this response, ending the HTTP request. + + We override this method in order to properly close the database. + + TODO - Now that we have greenlet support, this method could be + refactored in terms of context manager or something like + that. So far I'm leaving it to minimize changes. + + """ + if self.sql_session: # Request was stopped early, no session to close. + self.sql_session.close() + try: + tornado.web.RequestHandler.finish(self, *args, **kwds) + except IOError: + # When the client closes the connection before we reply, + # Tornado raises an IOError exception, that would pollute + # our log with unnecessarily critical messages + logger.debug("Connection closed before our reply.") + + def write_error(self, status_code, **kwargs): + if "exc_info" in kwargs and \ + kwargs["exc_info"][0] != tornado.web.HTTPError: + exc_info = kwargs["exc_info"] + logger.error( + "Uncaught exception (%r) while processing a request: %s", + exc_info[1], ''.join(traceback.format_exception(*exc_info))) + + # Most of the handlers raise a 404 HTTP error before r_params + # is defined. If r_params is not defined we try to define it + # here, and if it fails we simply return a basic textual error notice. + if self.r_params is None: + try: + self.r_params = self.render_params() + except: + self.write("A critical error has occurred :-(") + self.finish() + return + self.render("error.html", status_code=status_code, **self.r_params) + + get_string = argument_reader(lambda a: a, empty="") + + # When a checkbox isn't active it's not sent at all, making it + # impossible to distinguish between missing and False. + def get_bool(self, dest, name): + """Parse a boolean. + + dest (dict): a place to store the result. + name (string): the name of the argument and of the item. + + """ + value = self.get_argument(name, False) + try: + dest[name] = bool(value) + except: + raise ValueError("Can't cast %s to bool." % value) + + get_int = argument_reader(parse_int) + + get_timedelta_sec = argument_reader(parse_timedelta_sec) + + get_timedelta_min = argument_reader(parse_timedelta_min) + + get_datetime = argument_reader(parse_datetime) + + get_ip_address_or_subnet = argument_reader(parse_ip_address_or_subnet) + + def get_submission_format(self, dest): + """Parse the submission format. + + Using the two arguments "submission_format_choice" and + "submission_format" set the "submission_format" item of the + given dictionary. + + dest (dict): a place to store the result. + + """ + choice = self.get_argument("submission_format_choice", "other") + if choice == "simple": + filename = "%s.%%l" % dest["name"] + format_ = [SubmissionFormatElement(filename)] + elif choice == "other": + value = self.get_argument("submission_format", "[]") + if value == "": + value = "[]" + format_ = [] + try: + for filename in json.loads(value): + format_ += [SubmissionFormatElement(filename)] + except ValueError: + raise ValueError("Submission format not recognized.") + else: + raise ValueError("Submission format not recognized.") + dest["submission_format"] = format_ + + def get_time_limit(self, dest, field): + """Parse the time limit. + + Read the argument with the given name and use its value to set + the "time_limit" item of the given dictionary. + + dest (dict): a place to store the result. + field (string): the name of the argument to use. + + """ + value = self.get_argument(field, None) + if value is None: + return + if value == "": + dest["time_limit"] = None + else: + try: + value = float(value) + except: + raise ValueError("Can't cast %s to float." % value) + if not 0 <= value < float("+inf"): + raise ValueError("Time limit out of range.") + dest["time_limit"] = value + + def get_memory_limit(self, dest, field): + """Parse the memory limit. + + Read the argument with the given name and use its value to set + the "memory_limit" item of the given dictionary. + + dest (dict): a place to store the result. + field (string): the name of the argument to use. + + """ + value = self.get_argument(field, None) + if value is None: + return + if value == "": + dest["memory_limit"] = None + else: + try: + value = int(value) + except: + raise ValueError("Can't cast %s to float." % value) + if not 0 < value: + raise ValueError("Invalid memory limit.") + dest["memory_limit"] = value + + def get_task_type(self, dest, name, params): + """Parse the task type. + + Parse the arguments to get the task type and its parameters, + and fill them in the "task_type" and "task_type_parameters" + items of the given dictionary. + + dest (dict): a place to store the result. + name (string): the name of the argument that holds the task + type name. + params (string): the prefix of the names of the arguments that + hold the parameters. + + """ + name = self.get_argument(name, None) + if name is None: + raise ValueError("Task type not found.") + try: + class_ = get_task_type_class(name) + except KeyError: + raise ValueError("Task type not recognized: %s." % name) + params = json.dumps(class_.parse_handler(self, params + name + "_")) + dest["task_type"] = name + dest["task_type_parameters"] = params + + def get_score_type(self, dest, name, params): + """Parse the score type. + + Parse the arguments to get the score type and its parameters, + and fill them in the "score_type" and "score_type_parameters" + items of the given dictionary. + + dest (dict): a place to store the result. + name (string): the name of the argument that holds the score + type name. + params (string): the name of the argument that hold the + parameters. + + """ + name = self.get_argument(name, None) + if name is None: + raise ValueError("Score type not found.") + try: + get_score_type_class(name) + except KeyError: + raise ValueError("Score type not recognized: %s." % name) + params = self.get_argument(params, None) + if params is None: + raise ValueError("Score type parameters not found.") + dest["score_type"] = name + dest["score_type_parameters"] = params + + def render_params_for_submissions(self, query, page, page_size=50): + """Add data about the requested submissions to r_params. + + query (sqlalchemy.orm.query.Query): the query giving back all + interesting submissions. + page (int): the index of the page to display. + page_size(int): the number of submissions per page. + + """ + query = query\ + .options(subqueryload(Submission.task))\ + .options(subqueryload(Submission.participation))\ + .options(subqueryload(Submission.files))\ + .options(subqueryload(Submission.token))\ + .options(subqueryload(Submission.results) + .subqueryload(SubmissionResult.evaluations))\ + .order_by(Submission.timestamp.desc()) + + offset = page * page_size + count = query.count() + + if self.r_params is None: + self.r_params = self.render_params() + + # A page showing paginated submissions can use these + # parameters: total number of submissions, submissions to + # display in this page, index of the current page, total + # number of pages. + self.r_params["submission_count"] = count + self.r_params["submissions"] = \ + query.slice(offset, offset + page_size).all() + self.r_params["submission_page"] = page + self.r_params["submission_pages"] = \ + (count + page_size - 1) // page_size + + def render_params_for_user_tests(self, query, page, page_size=50): + """Add data about the requested user tests to r_params. + + query (sqlalchemy.orm.query.Query): the query giving back all + interesting user tests. + page (int): the index of the page to display. + page_size(int): the number of submissions per page. + + """ + query = query\ + .options(subqueryload(UserTest.task))\ + .options(subqueryload(UserTest.participation))\ + .options(subqueryload(UserTest.files))\ + .options(subqueryload(UserTest.results))\ + .order_by(UserTest.timestamp.desc()) + + offset = page * page_size + count = query.count() + + if self.r_params is None: + self.r_params = self.render_params() + + self.r_params["user_test_count"] = count + self.r_params["user_tests"] = \ + query.slice(offset, offset + page_size).all() + self.r_params["user_test_page"] = page + self.r_params["user_test_pages"] = \ + (count + page_size - 1) // page_size + + def render_params_for_remove_confirmation(self, query): + count = query.count() + + if self.r_params is None: + self.r_params = self.render_params() + self.r_params["submission_count"] = count diff --git a/cms/server/contest/handlers/api.py b/cms/server/api/handlers/handler.py similarity index 51% rename from cms/server/contest/handlers/api.py rename to cms/server/api/handlers/handler.py index 328bad2f40..eceda1e252 100644 --- a/cms/server/contest/handlers/api.py +++ b/cms/server/api/handlers/handler.py @@ -2,14 +2,7 @@ # -*- coding: utf-8 -*- # Contest Management System - http://cms-dev.github.io/ -# Copyright © 2010-2013 Giovanni Mascellani -# Copyright © 2010-2017 Stefano Maggiolo -# Copyright © 2010-2012 Matteo Boscariol -# Copyright © 2012-2014 Luca Wehrstedt -# Copyright © 2014 Artem Iglikov -# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> -# Copyright © 2016 Myungwoo Chun -# TODO: add your name +# Copyright © 2017 Kiarash Golezardi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -24,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -"""Handlers for the API related to AWS +"""Handlers for the API """ from __future__ import absolute_import @@ -38,59 +31,226 @@ import re import json -from urllib import quote - import tornado.web -from sqlalchemy import func - from cms import config -from cms.db import Task, UserTest, UserTestFile, UserTestManager, Testcase -from cms.grading.languagemanager import get_language +from cms.db import Attachment, Dataset, Session, Statement, Submission, \ + SubmissionFormatElement, Task, UserTest, UserTestFile, \ + UserTestManager, Testcase, Participation from cms.grading.tasktypes import get_task_type -from cms.server import actual_phase_required, format_size -from cmscommon.archive import Archive -from cmscommon.crypto import encrypt_number -from cmscommon.datetime import make_timestamp -from cmscommon.mimetypes import get_type_for_file_name +from cms.grading.languagemanager import get_language +from cmscommon.datetime import make_datetime, make_timestamp +from cms import plugin_list +from cms.grading.languagemanager import LANGUAGES -from .base import BaseHandler, FileHandler, \ - NOTIFICATION_ERROR, NOTIFICATION_SUCCESS +from .base import BaseHandler logger = logging.getLogger(__name__) -class APIGenerateOutput(BaseHandler): - """Creates a user test on a task for a perticular testcase +class TestHandler(BaseHandler): + + def get(self): + return self.write('Works') + + +class TaskTypesHandler(BaseHandler): + """Writes a list of task types names + + """ + + def get(self): + task_type_list = plugin_list("cms.grading.tasktypes", "tasktypes") + task_types_name = [task_type.__name__ for task_type in task_type_list] + return self.write(json.dumps(task_types_name)) + + +class ScoreTypesHandler(BaseHandler): + """Writes a list of task types names - Based on UserTestHandler """ - def safe_get_item(self, cls, ident, session=None): - """Get item from database of class cls and id ident, using - session if given, or self.sql_session if not given. If id is - not found, raise a 404. - cls (type): class of object to retrieve. - ident (string): id of object. - session (Session|None): session to use. + def get(self): + score_type_list = plugin_list("cms.grading.scoretypes", "scoretypes") + score_types_name = [score_type.__name__ for score_type in score_type_list] + return self.write(json.dumps(score_types_name)) + + +class LanguagesHandler(BaseHandler): + """Writes a list of language names + + """ - return (object): the object with the given id. + def get(self): + language_names = [lang.name for lang in LANGUAGES] + return self.write(json.dumps(language_names)) - raise (HTTPError): 404 if not found. - """ - if session is None: - session = self.sql_session - entity = cls.get_from_id(ident, session) - if entity is None: - raise tornado.web.HTTPError(404) - return entity +class AddTaskHandler(BaseHandler): + """Creates a new task. + + Based on AWS AddTaskHandler + """ + + def post(self): + # + # TODO: helpers and managers + # + try: + attrs = dict() + + self.get_string(attrs, "name", empty=None) + assert attrs.get("name") is not None, "No task name specified." + attrs["title"] = attrs["name"] + + # Set default submission format as ["taskname.%l"] + attrs["submission_format"] = \ + [SubmissionFormatElement("%s.%%l" % attrs["name"])] + + # Create the task. + task = Task(**attrs) + self.sql_session.add(task) + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + try: + attrs = dict() + + # Create its first dataset. + attrs["description"] = "Default" + attrs["autojudge"] = True + self.get_time_limit(attrs, "time_limit") + self.get_memory_limit(attrs, "memory_limit") + self.get_task_type(attrs, "task_type", "task_type_parameters_") + self.get_score_type(attrs, "score_type", "score_type_parameters") + attrs["task"] = task + dataset = Dataset(**attrs) + self.sql_session.add(dataset) + + # Make the dataset active. Life works better that way. + task.active_dataset = dataset + + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + if self.try_commit(): + self.write('%d' % task.id) + else: + raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + + +class ModifyTaskHandler(BaseHandler): + """Updates an existing task. + + Based on AWS TaskHandler + """ + + def post(self, task_id): + # + # TODO: helpers and managers + # + task = self.safe_get_item(Task, task_id) + + try: + attrs = task.get_attrs() + + self.get_string(attrs, "name", empty=None) + assert attrs.get("name") is not None, "No task name specified." + attrs["title"] = attrs["name"] + + # Set default submission format as ["taskname.%l"] + attrs["submission_format"] = \ + [SubmissionFormatElement("%s.%%l" % attrs["name"])] + + # Update the task. + task.set_attrs(attrs) + + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + try: + dataset = task.active_dataset + attrs = dataset.get_attrs() + + # Create its first dataset. + self.get_time_limit(attrs, "time_limit") + self.get_memory_limit(attrs, "memory_limit") + self.get_task_type(attrs, "task_type", "task_type_parameters_") + self.get_score_type(attrs, "score_type", "score_type_parameters") + + # Update the dataset. + dataset.set_attrs(attrs) + + except Exception as error: + raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + + if self.try_commit(): + # Update the task and score on RWS. + self.application.service.proxy_service.dataset_updated( + task_id=task.id) + self.write('%d' % task.id) + else: + raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + + +class AddTestcaseHandler(BaseHandler): + """Add a testcase to the task's active dataset. + + Based on AWS AddTestcaseHandler + """ + + def post(self, task_id): + task = self.safe_get_item(Task, task_id) + dataset = task.active_dataset + + codename = self.get_argument("testcase_id") + + try: + input_ = self.request.files["input"][0] + output = self.request.files["output"][0] + except KeyError: + raise tornado.web.HTTPError(403, "Invalid data: Please fill both input and output.") + + public = True + task_name = task.name + self.sql_session.close() + + try: + input_digest = \ + self.application.service.file_cacher.put_file_content( + input_["body"], + "Testcase input for task %s" % task_name) + output_digest = \ + self.application.service.file_cacher.put_file_content( + output["body"], + "Testcase output for task %s" % task_name) + except Exception as error: + raise tornado.web.HTTPError(403, "Testcase storage failed: %s" % error) + + self.sql_session = Session() + + testcase = Testcase( + codename, public, input_digest, output_digest, dataset=dataset) + self.sql_session.add(testcase) + + if self.try_commit(): + # max_score and/or extra_headers might have changed. + self.application.service.proxy_service.reinitialize() + self.write('%d' % testcase.id) + else: + raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + + +class GenerateOutputHandler(BaseHandler): + """Creates a user test on a task for a perticular testcase + + Based on CWS UserTestHandler + """ - @tornado.web.authenticated - @actual_phase_required(0) def post(self, task_id, testcase_id): - participation = self.current_user + participation = self.current_user # TODO: create the contest, user, and participation + participation = self.sql_session.query(Participation).first() task = self.safe_get_item(Task, task_id) testcase = self.safe_get_item(Testcase, testcase_id) @@ -103,15 +263,13 @@ def post(self, task_id, testcase_id): if not task_type.testable: raise tornado.web.HTTPError(403, "This task type is not testable") - # Alias for easy access - contest = self.contest - # Required files from the user. required = set([sfe.filename for sfe in task.submission_format] + task_type.get_user_managers(task.submission_format) + ["input"]) # TODO: Archive?? + # TODO: multipe solution files # Ensure that the user did not submit multiple files with the # same name. @@ -145,10 +303,7 @@ def post(self, task_id, testcase_id): if need_lang: error = None if submission_lang is None: - error = self._("Cannot recognize the user test language.") - elif submission_lang not in contest.languages: - error = self._("Language %s not allowed in this contest.") \ - % submission_lang + error = "Cannot recognize the user test language." if error is not None: raise tornado.web.HTTPError(403, "%s" % error) @@ -163,12 +318,13 @@ def post(self, task_id, testcase_id): # Attempt to store the submission locally to be able to # recover a failure. + if config.tests_local_copy: try: path = os.path.join( config.tests_local_copy_path.replace("%s", config.data_dir), - participation.user.username) + 'API') if not os.path.exists(path): os.makedirs(path) # Pickle in ASCII format produces str, not unicode, @@ -177,8 +333,8 @@ def post(self, task_id, testcase_id): os.path.join(path, "%d" % make_timestamp(self.timestamp)), "wb") as file_: - pickle.dump((self.contest.id, - participation.user.id, + pickle.dump((None, + None, task.id, files), file_) except Exception as error: @@ -190,9 +346,8 @@ def post(self, task_id, testcase_id): for filename in files: digest = self.application.service.file_cacher.put_file_content( files[filename][1], - "Test file %s sent by %s at %d." % ( - filename, participation.user.username, - make_timestamp(self.timestamp))) + "Test file %s sent by API at %d." % ( + filename, make_timestamp(self.timestamp))) file_digests[filename] = digest # Now Adding managers' digests @@ -208,8 +363,7 @@ def post(self, task_id, testcase_id): raise tornado.web.HTTPError(403, "Test storage failed!") # All the files are stored, ready to submit! - logger.info("All files stored for test sent by %s", - participation.user.username) + logger.info("All files stored for test sent by API") user_test = UserTest(self.timestamp, submission_lang, file_digests["input"], @@ -229,7 +383,11 @@ def post(self, task_id, testcase_id): UserTestManager(filename, digest, user_test=user_test)) self.sql_session.add(user_test) - self.sql_session.commit() + try: + self.sql_session.commit() + except Exception as error: + self.write('error: %s' % error) + return self.application.service.evaluation_service.new_user_test( user_test_id=user_test.id) # The argument (encripted user test id) is not used by CWS @@ -238,54 +396,22 @@ def post(self, task_id, testcase_id): self.write('%d' % user_test.id) -class APISumbitionDetails(BaseHandler): - """Gets the result of the sumbission +class SubmissionDetailsHandler(BaseHandler): + """Gets the result of the submission - Based on UserTestDetailsHandler + Based on CWS UserTestDetailsHandler """ - def safe_get_item(self, cls, ident, session=None): - """Get item from database of class cls and id ident, using - session if given, or self.sql_session if not given. If id is - not found, raise a 404. - - cls (type): class of object to retrieve. - ident (string): id of object. - session (Session|None): session to use. - return (object): the object with the given id. - - raise (HTTPError): 404 if not found. - - """ - if session is None: - session = self.sql_session - entity = cls.get_from_id(ident, session) - if entity is None: - raise tornado.web.HTTPError(404) - return entity - - @tornado.web.authenticated - @actual_phase_required(0) def get(self, task_id, user_test_num): - participation = self.current_user - - if not self.r_params["testing_enabled"]: - raise tornado.web.HTTPError(404) - task = self.safe_get_item(Task, task_id) + user_test = self.safe_get_item(UserTest, user_test_num) - user_test = self.sql_session.query(UserTest) \ - .filter(UserTest.participation == participation) \ - .filter(UserTest.task == task) \ - .order_by(UserTest.timestamp) \ - .offset(int(user_test_num) - 1) \ - .first() if user_test is None: - raise tornado.web.HTTPError(404) + raise tornado.web.HTTPError(405) tr = user_test.get_result(task.active_dataset) if tr is None: - raise tornado.web.HTTPError(404) + raise tornado.web.HTTPError(403) result = {} diff --git a/cms/server/api/server.py b/cms/server/api/server.py new file mode 100644 index 0000000000..92bf511eae --- /dev/null +++ b/cms/server/api/server.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2010-2013 Giovanni Mascellani +# Copyright © 2010-2017 Stefano Maggiolo +# Copyright © 2010-2012 Matteo Boscariol +# Copyright © 2012-2016 Luca Wehrstedt +# Copyright © 2014 Artem Iglikov +# Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> +# Copyright © 2017 Kiarash Golezardi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Web server for the API. + +""" + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import base64 +import logging +import pkg_resources + +from cms import ConfigError, ServiceCoord, config +from cms.io import WebService +from cms.db.filecacher import FileCacher + +from .handlers import HANDLERS + + +logger = logging.getLogger(__name__) + + +class APIWebServer(WebService): + """Service that runs the web server for the API. + + """ + def __init__(self, shard): + parameters = { + "login_url": "/", + "template_path": "", + "static_files": [], + "cookie_secret": base64.b64encode(config.secret_key), + "debug": config.tornado_debug, + "is_proxy_used": config.is_proxy_used, + "num_proxies_used": config.num_proxies_used, + "xsrf_cookies": True, + } + super(APIWebServer, self).__init__( + config.api_listen_port, + HANDLERS, + parameters, + shard=shard, + listen_address=config.api_listen_address) + + self.contest = contest + + self.file_cacher = FileCacher(self) + self.evaluation_service = self.connect_to( + ServiceCoord("EvaluationService", 0)) diff --git a/cms/server/contest/handlers/__init__.py b/cms/server/contest/handlers/__init__.py index da0c78ffc6..68df83e8a6 100644 --- a/cms/server/contest/handlers/__init__.py +++ b/cms/server/contest/handlers/__init__.py @@ -61,10 +61,6 @@ from .communication import \ CommunicationHandler, \ QuestionHandler -from .api import \ - APIGenerateOutput, \ - APISumbitionDetails - HANDLERS = [ @@ -109,10 +105,6 @@ (r"/communication", CommunicationHandler), (r"/question", QuestionHandler), - - # API - (r"/api/task/([0-9]+)/testcase/([0-9]+)/run", APIGenerateOutput), - (r"/api/task/([0-9]+)/test/([0-9]+)/result", APISumbitionDetails), ] diff --git a/scripts/cmsAPIWebServer b/scripts/cmsAPIWebServer new file mode 100644 index 0000000000..6b385427cb --- /dev/null +++ b/scripts/cmsAPIWebServer @@ -0,0 +1,57 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# Contest Management System - http://cms-dev.github.io/ +# Copyright © 2013 Luca Wehrstedt +# Copyright © 2016 Stefano Maggiolo +# Copyright © 2017 Kiarash Golezardi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +# We enable monkey patching to make many libraries gevent-friendly +# (for instance, urllib3, used by requests) +import gevent.monkey +gevent.monkey.patch_all() + +import logging +import sys + +from cms import ConfigError, default_argument_parser +from cms.db import test_db_connection +from cms.server.api import APIWebServer + + +logger = logging.getLogger(__name__) + + +def main(): + """Parse arguments and launch service. + + """ + test_db_connection() + success = default_argument_parser("API web server for CMS.", + APIWebServer).run() + return 0 if success is True else 1 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except ConfigError as error: + logger.critical(error.message) + sys.exit(1) diff --git a/setup.py b/setup.py index 38411415bd..99d48ad279 100755 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ # Copyright © 2016 Myungwoo Chun # Copyright © 2016 Masaki Hara # Copyright © 2016 Peyman Jabbarzade Ganje +# Copyright © 2017 Kiarash Golezardi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -117,7 +118,8 @@ def find_version(): "scripts/cmsPrintingService", "scripts/cmsRankingWebServer", "scripts/cmsInitDB", - "scripts/cmsDropDB"], + "scripts/cmsDropDB", + "scripts/cmsAPIWebServer"], entry_points={ "console_scripts": [ "cmsRunTests=cmstestsuite.RunTests:main", From bcbe8a2e1e3cb83c7f4ba32505123c1489dbb359 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 27 Apr 2017 11:03:51 +0430 Subject: [PATCH 03/21] Fixed a bug in API WebServer --- cms/server/api/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/server/api/server.py b/cms/server/api/server.py index 92bf511eae..22321227d8 100644 --- a/cms/server/api/server.py +++ b/cms/server/api/server.py @@ -67,7 +67,7 @@ def __init__(self, shard): shard=shard, listen_address=config.api_listen_address) - self.contest = contest + self.contest = None self.file_cacher = FileCacher(self) self.evaluation_service = self.connect_to( From 66cde0a97c8ee5e05eada7363d4075404d0c5cf4 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Tue, 2 May 2017 06:35:21 +0430 Subject: [PATCH 04/21] Use task name instead of task ID in API --- cms/server/api/handlers/__init__.py | 12 ++++++------ cms/server/api/handlers/base.py | 6 ++++++ cms/server/api/handlers/handler.py | 22 +++++++++++++--------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/cms/server/api/handlers/__init__.py b/cms/server/api/handlers/__init__.py index 9945a07ca1..c803f9a819 100644 --- a/cms/server/api/handlers/__init__.py +++ b/cms/server/api/handlers/__init__.py @@ -33,17 +33,17 @@ SubmissionDetailsHandler HANDLERS = [ - (r"/", TestHandler), - (r"/tasktypes", TaskTypesHandler), + (r"/test/(.*)", TestHandler), + (r"/tasktypes/", TaskTypesHandler), (r"/scoretypes", ScoreTypesHandler), (r"/languages", LanguagesHandler), (r"/tasks/add", AddTaskHandler), - (r"/task/([0-9]+)/modify", ModifyTaskHandler), - (r"/task/([0-9]+)/testcases/add", AddTestcaseHandler), + (r"/task/(.*)/delete", ModifyTaskHandler), + (r"/task/(.*)/testcases/add", AddTestcaseHandler), - (r"/task/([0-9]+)/testcase/([0-9]+)/run", GenerateOutputHandler), - (r"/task/([0-9]+)/test/([0-9]+)/result", SubmissionDetailsHandler), + (r"/task/(.*)/testcase/([0-9]+)/run", GenerateOutputHandler), + (r"/task/(.*)/test/([0-9]+)/result", SubmissionDetailsHandler), ] diff --git a/cms/server/api/handlers/base.py b/cms/server/api/handlers/base.py index 0de2ec0109..19671af3fe 100644 --- a/cms/server/api/handlers/base.py +++ b/cms/server/api/handlers/base.py @@ -486,3 +486,9 @@ def render_params_for_remove_confirmation(self, query): if self.r_params is None: self.r_params = self.render_params() self.r_params["submission_count"] = count + + def get_task_by_name(self, task_name): + task = self.sql_session.query(Task) \ + .filter(Task.name == task_name) \ + .first() + return task diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index eceda1e252..10454a6afd 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -51,8 +51,12 @@ class TestHandler(BaseHandler): - def get(self): - return self.write('Works') + def get(self, task_name): + task = self.sql_session.query(Task)\ + .filter(Task.name == task_name)\ + .first() + a = task.active_dataset.time_limit + return self.write('%f' % a) class TaskTypesHandler(BaseHandler): @@ -148,7 +152,7 @@ class ModifyTaskHandler(BaseHandler): def post(self, task_id): # - # TODO: helpers and managers + # TODO: delete instead of modify # task = self.safe_get_item(Task, task_id) @@ -200,8 +204,8 @@ class AddTestcaseHandler(BaseHandler): Based on AWS AddTestcaseHandler """ - def post(self, task_id): - task = self.safe_get_item(Task, task_id) + def post(self, task_name): + task = self.get_task_by_name(task_name) dataset = task.active_dataset codename = self.get_argument("testcase_id") @@ -248,11 +252,11 @@ class GenerateOutputHandler(BaseHandler): Based on CWS UserTestHandler """ - def post(self, task_id, testcase_id): + def post(self, task_name, testcase_id): participation = self.current_user # TODO: create the contest, user, and participation participation = self.sql_session.query(Participation).first() - task = self.safe_get_item(Task, task_id) + task = self.get_task_by_name(task_name) testcase = self.safe_get_item(Testcase, testcase_id) input_digest = testcase.input @@ -402,8 +406,8 @@ class SubmissionDetailsHandler(BaseHandler): Based on CWS UserTestDetailsHandler """ - def get(self, task_id, user_test_num): - task = self.safe_get_item(Task, task_id) + def get(self, task_name, user_test_num): + task = self.get_task_by_name(task_name) user_test = self.safe_get_item(UserTest, user_test_num) if user_test is None: From 8a7fc0dc7bc6694a142a7270f61bce62f417d68d Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Tue, 2 May 2017 10:21:47 +0430 Subject: [PATCH 05/21] Add minor changes to API features --- cms/server/api/handlers/__init__.py | 10 +-- cms/server/api/handlers/base.py | 4 ++ cms/server/api/handlers/handler.py | 100 +++++++++++++--------------- 3 files changed, 56 insertions(+), 58 deletions(-) diff --git a/cms/server/api/handlers/__init__.py b/cms/server/api/handlers/__init__.py index c803f9a819..9ada370da8 100644 --- a/cms/server/api/handlers/__init__.py +++ b/cms/server/api/handlers/__init__.py @@ -27,23 +27,25 @@ ScoreTypesHandler, \ LanguagesHandler, \ AddTaskHandler, \ - ModifyTaskHandler, \ + RemoveTaskHandler, \ AddTestcaseHandler, \ GenerateOutputHandler, \ - SubmissionDetailsHandler + SubmissionDetailsHandler, \ + SubmissionOutputHandler HANDLERS = [ - (r"/test/(.*)", TestHandler), + (r"/test", TestHandler), (r"/tasktypes/", TaskTypesHandler), (r"/scoretypes", ScoreTypesHandler), (r"/languages", LanguagesHandler), (r"/tasks/add", AddTaskHandler), - (r"/task/(.*)/delete", ModifyTaskHandler), + (r"/task/(.*)/delete", RemoveTaskHandler), (r"/task/(.*)/testcases/add", AddTestcaseHandler), (r"/task/(.*)/testcase/([0-9]+)/run", GenerateOutputHandler), (r"/task/(.*)/test/([0-9]+)/result", SubmissionDetailsHandler), + (r"/task/(.*)/test/([0-9]+)/output", SubmissionOutputHandler), ] diff --git a/cms/server/api/handlers/base.py b/cms/server/api/handlers/base.py index 19671af3fe..41ff09c4dc 100644 --- a/cms/server/api/handlers/base.py +++ b/cms/server/api/handlers/base.py @@ -10,6 +10,7 @@ # Copyright © 2014 Fabian Gundlach <320pointsguy@gmail.com> # Copyright © 2016 Myungwoo Chun # Copyright © 2016 Peyman Jabbarzade Ganje +# Copyright © 2017 Kiarash Golezardi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -492,3 +493,6 @@ def get_task_by_name(self, task_name): .filter(Task.name == task_name) \ .first() return task + + +FileHandler = file_handler_gen(BaseHandler) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 10454a6afd..69582a9b4f 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -43,7 +43,7 @@ from cms import plugin_list from cms.grading.languagemanager import LANGUAGES -from .base import BaseHandler +from .base import BaseHandler, FileHandler logger = logging.getLogger(__name__) @@ -52,11 +52,19 @@ class TestHandler(BaseHandler): def get(self, task_name): - task = self.sql_session.query(Task)\ - .filter(Task.name == task_name)\ - .first() - a = task.active_dataset.time_limit - return self.write('%f' % a) + html_code = """!DOCTYPE html + + +Test page + + +
+ + +
+ +""" + return self.write('%s' % html_code) class TaskTypesHandler(BaseHandler): @@ -108,9 +116,7 @@ def post(self): assert attrs.get("name") is not None, "No task name specified." attrs["title"] = attrs["name"] - # Set default submission format as ["taskname.%l"] - attrs["submission_format"] = \ - [SubmissionFormatElement("%s.%%l" % attrs["name"])] + self.get_submission_format(attrs) # Create the task. task = Task(**attrs) @@ -144,58 +150,20 @@ def post(self): raise tornado.web.HTTPError(403, "Operation Unsuccessful!") -class ModifyTaskHandler(BaseHandler): +class RemoveTaskHandler(BaseHandler): """Updates an existing task. - Based on AWS TaskHandler + Based on AWS RemoveTaskHandler """ - def post(self, task_id): - # - # TODO: delete instead of modify - # - task = self.safe_get_item(Task, task_id) - - try: - attrs = task.get_attrs() - - self.get_string(attrs, "name", empty=None) - assert attrs.get("name") is not None, "No task name specified." - attrs["title"] = attrs["name"] - - # Set default submission format as ["taskname.%l"] - attrs["submission_format"] = \ - [SubmissionFormatElement("%s.%%l" % attrs["name"])] - - # Update the task. - task.set_attrs(attrs) - - except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) - - try: - dataset = task.active_dataset - attrs = dataset.get_attrs() - - # Create its first dataset. - self.get_time_limit(attrs, "time_limit") - self.get_memory_limit(attrs, "memory_limit") - self.get_task_type(attrs, "task_type", "task_type_parameters_") - self.get_score_type(attrs, "score_type", "score_type_parameters") - - # Update the dataset. - dataset.set_attrs(attrs) - - except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + def post(self, task_name): + task = self.get_task_by_name(task_name) + self.sql_session.delete(task) if self.try_commit(): - # Update the task and score on RWS. - self.application.service.proxy_service.dataset_updated( - task_id=task.id) - self.write('%d' % task.id) + return self.write('Successful') else: - raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + return self.write('Unsuccessful') class AddTestcaseHandler(BaseHandler): @@ -444,3 +412,27 @@ def get(self, task_name, user_test_num): result['memory'] = tr.compilation_memory self.write(json.dumps(result)) + + +class SubmissionOutputHandler(FileHandler): + """Send back a submission output. + + Based on CWS UserTestIOHandler + """ + def get(self, task_name, test_id): + task = self.get_task_by_name(task_name) + user_test = self.safe_get_item(UserTest, test_id) + + if user_test is None: + raise tornado.web.HTTPError(404) + + tr = user_test.get_result(task.active_dataset) + digest = tr.output if tr is not None else None + self.sql_session.close() + + if digest is None: + raise tornado.web.HTTPError(404) + + mimetype = 'text/plain' + + self.fetch(digest, mimetype, 'output') From 7158816846819db1e834bdf605469db659986d56 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Tue, 2 May 2017 12:20:32 +0430 Subject: [PATCH 06/21] Add templates to APIWebServer --- cms/server/api/handlers/handler.py | 21 +++++++-------------- cms/server/api/server.py | 3 ++- cms/server/api/templates/base.html | 16 ++++++++++++++++ cms/server/api/templates/test.html | 13 +++++++++++++ setup.py | 1 + 5 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 cms/server/api/templates/base.html create mode 100644 cms/server/api/templates/test.html diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 69582a9b4f..25463f2489 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -51,20 +51,13 @@ class TestHandler(BaseHandler): - def get(self, task_name): - html_code = """!DOCTYPE html - - -Test page - - -
- - -
- -""" - return self.write('%s' % html_code) + def get(self): + self.r_params = self.render_params() + self.render("test.html", **self.r_params) + + def post(self): + input_file = self.request.files["salam"][0] + self.write('%s' % input_file) class TaskTypesHandler(BaseHandler): diff --git a/cms/server/api/server.py b/cms/server/api/server.py index 22321227d8..fa9e7d5206 100644 --- a/cms/server/api/server.py +++ b/cms/server/api/server.py @@ -52,7 +52,8 @@ class APIWebServer(WebService): def __init__(self, shard): parameters = { "login_url": "/", - "template_path": "", + "template_path": pkg_resources.resource_filename( + "cms.server.api", "templates"), "static_files": [], "cookie_secret": base64.b64encode(config.secret_key), "debug": config.tornado_debug, diff --git a/cms/server/api/templates/base.html b/cms/server/api/templates/base.html new file mode 100644 index 0000000000..0466ddbf8c --- /dev/null +++ b/cms/server/api/templates/base.html @@ -0,0 +1,16 @@ +{% import time %} +{% import json %} +{% from cms import config %} +{% from cms.db import SubmissionResult, UserTestResult %} +{% from cms.grading.languagemanager import LANGUAGES, get_language %} +{% from cmscommon.datetime import make_timestamp %} +{% from cmscommon.crypto import get_hex_random_key %} + + + + API Web Page! + + + {% block core %}{% end %} + + \ No newline at end of file diff --git a/cms/server/api/templates/test.html b/cms/server/api/templates/test.html new file mode 100644 index 0000000000..f579f8d19f --- /dev/null +++ b/cms/server/api/templates/test.html @@ -0,0 +1,13 @@ +{% extends base.html %} + +{% block core %} +{% from cms.server import format_dataset_attrs %} +{% from cms import plugin_list, SCORE_MODE_MAX_TOKENED_LAST, SCORE_MODE_MAX %} + +
+ {% module xsrf_form_html() %} + + +
+ +{% end %} \ No newline at end of file diff --git a/setup.py b/setup.py index 99d48ad279..8e5f232aae 100755 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ os.path.join("contest", "static", "img", "mimetypes", "*.*"), os.path.join("contest", "static", "js", "*.*"), os.path.join("contest", "templates", "*.*"), + os.path.join("api", "templates", "*.*"), ], "cms.service": [ os.path.join("templates", "printing", "*.*"), From 1eaa5ee807c60df2169c6e2499187b5f12dc1dac Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Tue, 2 May 2017 21:45:38 +0430 Subject: [PATCH 07/21] Recieve files as base64 encoded strings in API --- cms/server/api/handlers/handler.py | 60 ++++++++++++++++-------------- cms/server/api/server.py | 2 +- cms/server/api/templates/test.html | 19 +++++++++- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 25463f2489..02e0821e37 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -30,6 +30,7 @@ import pickle import re import json +import base64 import tornado.web @@ -56,8 +57,9 @@ def get(self): self.render("test.html", **self.r_params) def post(self): - input_file = self.request.files["salam"][0] - self.write('%s' % input_file) + input_body = self.request.files["input"][0]["body"] + encoded = base64.b64encode(input_body) + self.write('%s' % encoded) class TaskTypesHandler(BaseHandler): @@ -172,10 +174,13 @@ def post(self, task_name): codename = self.get_argument("testcase_id") try: - input_ = self.request.files["input"][0] - output = self.request.files["output"][0] - except KeyError: - raise tornado.web.HTTPError(403, "Invalid data: Please fill both input and output.") + input_base64 = str(self.get_argument("input")) + input_body = str(base64.b64decode(input_base64)) + + except TypeError: + raise tornado.web.HTTPError(403, "Invalid data: Please give a valid input") + + output_body = str() public = True task_name = task.name @@ -184,11 +189,11 @@ def post(self, task_name): try: input_digest = \ self.application.service.file_cacher.put_file_content( - input_["body"], + input_body, "Testcase input for task %s" % task_name) output_digest = \ self.application.service.file_cacher.put_file_content( - output["body"], + output_body, "Testcase output for task %s" % task_name) except Exception as error: raise tornado.web.HTTPError(403, "Testcase storage failed: %s" % error) @@ -201,7 +206,6 @@ def post(self, task_name): if self.try_commit(): # max_score and/or extra_headers might have changed. - self.application.service.proxy_service.reinitialize() self.write('%d' % testcase.id) else: raise tornado.web.HTTPError(403, "Operation Unsuccessful!") @@ -214,14 +218,18 @@ class GenerateOutputHandler(BaseHandler): """ def post(self, task_name, testcase_id): - participation = self.current_user # TODO: create the contest, user, and participation + # TODO: Create a special contest, user, and participation instead of + # using the first one you see participation = self.sql_session.query(Participation).first() task = self.get_task_by_name(task_name) testcase = self.safe_get_item(Testcase, testcase_id) + request_files = json.loads(str(self.get_argument("files"))) + input_digest = testcase.input - managers_names = [manager.filename for manager in task.active_dataset.managers.values()] + managers_names = \ + [manager.filename for manager in task.active_dataset.managers.values()] # Check that the task is testable task_type = get_task_type(dataset=task.active_dataset) @@ -233,27 +241,24 @@ def post(self, task_name, testcase_id): task_type.get_user_managers(task.submission_format) + ["input"]) - # TODO: Archive?? - # TODO: multipe solution files - - # Ensure that the user did not submit multiple files with the - # same name. - if any(len(filename) != 1 for filename in self.request.files.values()): - raise tornado.web.HTTPError(403, "Please select the correct files.") + # TODO: If it is necessary, we may have to extract archives # This ensure that the user sent one file for every name in # submission format and no more. - provided = set(list(self.request.files.keys()) + ["input"] + managers_names) + provided = set(list(request_files.keys()) + ["input"] + managers_names) if not (required == provided): raise tornado.web.HTTPError(403, "Please select the correct files.") # Add submitted files. After this, files is a dictionary indexed # by *our* filenames (something like "output01.txt" or # "taskname.%l", and whose value is a couple - # (user_assigned_filename, content). - files = {} - for uploaded, data in self.request.files.iteritems(): - files[uploaded] = (data[0]["filename"], data[0]["body"]) + # (our_filename, content) + try: + files = {} + for filename, body in request_files.iteritems(): + files[filename] = (filename, base64.b64decode(body)) + except TypeError: + raise tornado.web.HTTPError(403, "Invalid data: Please provide a base64 encoded file") # Read the submission language provided in the request; we # integrate it with the language fetched from the previous @@ -355,9 +360,6 @@ def post(self, task_name, testcase_id): return self.application.service.evaluation_service.new_user_test( user_test_id=user_test.id) - # The argument (encripted user test id) is not used by CWS - # (nor it discloses information to the user), but it is useful - # for automatic testing to obtain the user test id). self.write('%d' % user_test.id) @@ -372,11 +374,11 @@ def get(self, task_name, user_test_num): user_test = self.safe_get_item(UserTest, user_test_num) if user_test is None: - raise tornado.web.HTTPError(405) + raise tornado.web.HTTPError(403) tr = user_test.get_result(task.active_dataset) if tr is None: - raise tornado.web.HTTPError(403) + raise tornado.web.HTTPError(405) result = {} @@ -429,3 +431,5 @@ def get(self, task_name, test_id): mimetype = 'text/plain' self.fetch(digest, mimetype, 'output') + # TODO: Probably best not send the file in this way, but to + # write the content, as a base64 encoded string. diff --git a/cms/server/api/server.py b/cms/server/api/server.py index fa9e7d5206..51ef532012 100644 --- a/cms/server/api/server.py +++ b/cms/server/api/server.py @@ -59,7 +59,7 @@ def __init__(self, shard): "debug": config.tornado_debug, "is_proxy_used": config.is_proxy_used, "num_proxies_used": config.num_proxies_used, - "xsrf_cookies": True, + "xsrf_cookies": False, } super(APIWebServer, self).__init__( config.api_listen_port, diff --git a/cms/server/api/templates/test.html b/cms/server/api/templates/test.html index f579f8d19f..3ff9e923d1 100644 --- a/cms/server/api/templates/test.html +++ b/cms/server/api/templates/test.html @@ -5,9 +5,24 @@ {% from cms import plugin_list, SCORE_MODE_MAX_TOKENED_LAST, SCORE_MODE_MAX %}
- {% module xsrf_form_html() %} - +
+


+ +
+ Codename:
+ Input file:
+ +
+ +


+ +
+ Files: For example: '{'simple.%l': 'things'}'
+ Language: For example: 'C++11 / g++'
+ +
+ {% end %} \ No newline at end of file From d6dcb128270a0f1219bdffbd2372f6f35c273373 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Tue, 2 May 2017 22:40:35 +0430 Subject: [PATCH 08/21] Handle managers when creating a new task in API --- cms/server/api/handlers/handler.py | 34 +++++++++++++++++++++++------- cms/server/api/templates/test.html | 6 +++++- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 02e0821e37..3f77c600c3 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -35,7 +35,7 @@ import tornado.web from cms import config -from cms.db import Attachment, Dataset, Session, Statement, Submission, \ +from cms.db import Attachment, Dataset, Session, Manager, Submission, \ SubmissionFormatElement, Task, UserTest, UserTestFile, \ UserTestManager, Testcase, Participation from cms.grading.tasktypes import get_task_type @@ -57,9 +57,13 @@ def get(self): self.render("test.html", **self.r_params) def post(self): - input_body = self.request.files["input"][0]["body"] - encoded = base64.b64encode(input_body) - self.write('%s' % encoded) + input_file = self.request.files["input"][0] + operation = self.get_argument("operation", "encode") + if operation == "encode": + encoded = base64.b64encode(input_file["body"]) + self.write('%s' % encoded) + else: + self.write('%s' % input_file) class TaskTypesHandler(BaseHandler): @@ -101,9 +105,6 @@ class AddTaskHandler(BaseHandler): """ def post(self): - # - # TODO: helpers and managers - # try: attrs = dict() @@ -139,6 +140,23 @@ def post(self): except Exception as error: raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + managers = json.loads(str(self.get_argument("managers"))) + for filename in managers: + try: + body = base64.b64decode(managers[filename]) + except TypeError: + raise tornado.web.HTTPError(403, "Invalid data: Please provide a base64 encoded file") + + try: + digest = self.application.service.file_cacher.put_file_content( + body, + "Task manager for %s" % attrs["name"]) + except Exception as error: + raise tornado.web.HTTPError(403, "Manager storage failed: %s" % error) + + manager = Manager(filename, digest, dataset=dataset) + self.sql_session.add(manager) + if self.try_commit(): self.write('%d' % task.id) else: @@ -151,7 +169,7 @@ class RemoveTaskHandler(BaseHandler): Based on AWS RemoveTaskHandler """ - def post(self, task_name): + def get(self, task_name): task = self.get_task_by_name(task_name) self.sql_session.delete(task) diff --git a/cms/server/api/templates/test.html b/cms/server/api/templates/test.html index 3ff9e923d1..7288bf46c6 100644 --- a/cms/server/api/templates/test.html +++ b/cms/server/api/templates/test.html @@ -5,7 +5,11 @@ {% from cms import plugin_list, SCORE_MODE_MAX_TOKENED_LAST, SCORE_MODE_MAX %}
-
+ +
From b03c802a4c0caa76caed588e7e306f3c7e32b36b Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 18 May 2017 11:45:38 +0430 Subject: [PATCH 09/21] Fix the required files for API-created usertest --- cms/server/api/handlers/handler.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 3f77c600c3..9210608249 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -246,8 +246,6 @@ def post(self, task_name, testcase_id): request_files = json.loads(str(self.get_argument("files"))) input_digest = testcase.input - managers_names = \ - [manager.filename for manager in task.active_dataset.managers.values()] # Check that the task is testable task_type = get_task_type(dataset=task.active_dataset) @@ -263,7 +261,7 @@ def post(self, task_name, testcase_id): # This ensure that the user sent one file for every name in # submission format and no more. - provided = set(list(request_files.keys()) + ["input"] + managers_names) + provided = set(list(request_files.keys()) + ["input"]) if not (required == provided): raise tornado.web.HTTPError(403, "Please select the correct files.") From a0512046eb4359131e37a51dcc73da8c6bdff046 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 18 May 2017 12:38:54 +0430 Subject: [PATCH 10/21] Change the output format of API --- cms/server/api/handlers/base.py | 4 ++ cms/server/api/handlers/handler.py | 62 ++++++++++++++---------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/cms/server/api/handlers/base.py b/cms/server/api/handlers/base.py index 41ff09c4dc..601734e80a 100644 --- a/cms/server/api/handlers/base.py +++ b/cms/server/api/handlers/base.py @@ -494,5 +494,9 @@ def get_task_by_name(self, task_name): .first() return task + def APIOutput(self, status, message): + res = {"status": status, "message": message} + self.write(json.dumps(res)) + FileHandler = file_handler_gen(BaseHandler) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 9210608249..904967d5c6 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -118,7 +118,7 @@ def post(self): task = Task(**attrs) self.sql_session.add(task) except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + return self.APIOutput(False, "Invalid fields: %s" % error) try: attrs = dict() @@ -138,29 +138,29 @@ def post(self): task.active_dataset = dataset except Exception as error: - raise tornado.web.HTTPError(403, "Invalid fields: %s" % error) + return self.APIOutput(False, "Invalid fields: %s" % error) managers = json.loads(str(self.get_argument("managers"))) for filename in managers: try: body = base64.b64decode(managers[filename]) except TypeError: - raise tornado.web.HTTPError(403, "Invalid data: Please provide a base64 encoded file") + return self.APIOutput(False, "Invalid data: Please provide a base64 encoded file") try: digest = self.application.service.file_cacher.put_file_content( body, "Task manager for %s" % attrs["name"]) except Exception as error: - raise tornado.web.HTTPError(403, "Manager storage failed: %s" % error) + return self.APIOutput(False, "Manager storage failed: %s" % error) manager = Manager(filename, digest, dataset=dataset) self.sql_session.add(manager) if self.try_commit(): - self.write('%d' % task.id) + return self.APIOutput(True, '%d' % task.id) else: - raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + return self.APIOutput(False, "Operation Unsuccessful!") class RemoveTaskHandler(BaseHandler): @@ -174,9 +174,9 @@ def get(self, task_name): self.sql_session.delete(task) if self.try_commit(): - return self.write('Successful') + return self.APIOutput(True, 'Successful') else: - return self.write('Unsuccessful') + return self.APIOutput(False, 'Unsuccessful') class AddTestcaseHandler(BaseHandler): @@ -194,11 +194,11 @@ def post(self, task_name): try: input_base64 = str(self.get_argument("input")) input_body = str(base64.b64decode(input_base64)) + output_base64 = str(self.get_argument("output")) + output_body = str(base64.b64decode(output_base64)) except TypeError: - raise tornado.web.HTTPError(403, "Invalid data: Please give a valid input") - - output_body = str() + return self.APIOutput(False, "Invalid data: Please give a valid input") public = True task_name = task.name @@ -214,7 +214,7 @@ def post(self, task_name): output_body, "Testcase output for task %s" % task_name) except Exception as error: - raise tornado.web.HTTPError(403, "Testcase storage failed: %s" % error) + return self.APIOutput(False, "Testcase storage failed: %s" % error) self.sql_session = Session() @@ -223,10 +223,9 @@ def post(self, task_name): self.sql_session.add(testcase) if self.try_commit(): - # max_score and/or extra_headers might have changed. - self.write('%d' % testcase.id) + return self.APIOutput(True, '%d' % testcase.id) else: - raise tornado.web.HTTPError(403, "Operation Unsuccessful!") + return self.APIOutput(False, "Operation Unsuccessful!") class GenerateOutputHandler(BaseHandler): @@ -250,7 +249,7 @@ def post(self, task_name, testcase_id): # Check that the task is testable task_type = get_task_type(dataset=task.active_dataset) if not task_type.testable: - raise tornado.web.HTTPError(403, "This task type is not testable") + return self.APIOutput(False, "This task type is not testable") # Required files from the user. required = set([sfe.filename for sfe in task.submission_format] + @@ -263,7 +262,7 @@ def post(self, task_name, testcase_id): # submission format and no more. provided = set(list(request_files.keys()) + ["input"]) if not (required == provided): - raise tornado.web.HTTPError(403, "Please select the correct files.") + return self.APIOutput(False, "Please send the correct files.") # Add submitted files. After this, files is a dictionary indexed # by *our* filenames (something like "output01.txt" or @@ -274,7 +273,7 @@ def post(self, task_name, testcase_id): for filename, body in request_files.iteritems(): files[filename] = (filename, base64.b64decode(body)) except TypeError: - raise tornado.web.HTTPError(403, "Invalid data: Please provide a base64 encoded file") + return self.APIOutput(False, "Invalid data: Please provide a base64 encoded file") # Read the submission language provided in the request; we # integrate it with the language fetched from the previous @@ -291,14 +290,14 @@ def post(self, task_name, testcase_id): if submission_lang is None: error = "Cannot recognize the user test language." if error is not None: - raise tornado.web.HTTPError(403, "%s" % error) + return self.APIOutput(False, "%s" % error) # Check if submitted files are small enough. if any([len(f[1]) > config.max_submission_length for n, f in files.items() if n != "input"]): - raise tornado.web.HTTPError(403, - "Each source file must be at most %d bytes long." - % config.max_submission_length) + return self.APIOutput(False, + "Each source file must be at most %d bytes long." + % config.max_submission_length) # All checks done, submission accepted. @@ -346,7 +345,7 @@ def post(self, task_name, testcase_id): # In case of error, the server aborts the submission except Exception as error: logger.error("Storage failed! %s", error) - raise tornado.web.HTTPError(403, "Test storage failed!") + return self.APIOutput(False, "Test storage failed!") # All the files are stored, ready to submit! logger.info("All files stored for test sent by API") @@ -372,11 +371,10 @@ def post(self, task_name, testcase_id): try: self.sql_session.commit() except Exception as error: - self.write('error: %s' % error) - return + return self.APIOutput(False, '%s' % error) self.application.service.evaluation_service.new_user_test( user_test_id=user_test.id) - self.write('%d' % user_test.id) + return self.APIOutput(True, '%d' % user_test.id) class SubmissionDetailsHandler(BaseHandler): @@ -390,11 +388,11 @@ def get(self, task_name, user_test_num): user_test = self.safe_get_item(UserTest, user_test_num) if user_test is None: - raise tornado.web.HTTPError(403) + return self.APIOutput(False, '') tr = user_test.get_result(task.active_dataset) if tr is None: - raise tornado.web.HTTPError(405) + return self.APIOutput(False, '') result = {} @@ -402,15 +400,13 @@ def get(self, task_name, user_test_num): result['evalres'] = tr.evaluation_text else: result['evalres'] = 'none' - self.write(json.dumps(result)) - return + return self.APIOutput(True, json.dumps(result)) if tr is not None and tr.compiled(): result['compiled'] = tr.compilation_text else: result['compiled'] = 'none' - self.write(json.dumps(result)) - return + return self.APIOutput(True, json.dumps(result)) if tr.compilation_time is None: result['time'] = 'none' @@ -422,7 +418,7 @@ def get(self, task_name, user_test_num): else: result['memory'] = tr.compilation_memory - self.write(json.dumps(result)) + return self.APIOutput(True, json.dumps(result)) class SubmissionOutputHandler(FileHandler): From ba5da907a77e1ed71d1ff868c3e911c65fbff699 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 18 May 2017 12:46:08 +0430 Subject: [PATCH 11/21] Remove managers from sent files for API-created usertest --- cms/server/api/handlers/handler.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 904967d5c6..f6cede7459 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -335,11 +335,7 @@ def post(self, task_name, testcase_id): filename, make_timestamp(self.timestamp))) file_digests[filename] = digest - # Now Adding managers' digests - for manager in task.active_dataset.managers.values(): - file_digests[manager.filename] = manager.digest - - # Finally Adding testcase's digest + # Adding testcase's digest file_digests["input"] = input_digest # In case of error, the server aborts the submission From f7c6a31807c3f4d25683db5a26b3533daa736ffd Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Sat, 27 May 2017 19:01:31 +0430 Subject: [PATCH 12/21] Use testcase codename instead of ID in API URLs --- cms/conf.py | 2 +- cms/server/api/handlers/__init__.py | 2 +- cms/server/api/handlers/handler.py | 39 +++++++++++------------------ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/cms/conf.py b/cms/conf.py index 4517eab345..0fef578821 100644 --- a/cms/conf.py +++ b/cms/conf.py @@ -112,7 +112,7 @@ def __init__(self): # APIWebServer. self.api_listen_address = "" - self.api_listen_port = 8890 + self.api_listen_port = 8897 # ProxyService. self.rankings = ["http://usern4me:passw0rd@localhost:8890/"] diff --git a/cms/server/api/handlers/__init__.py b/cms/server/api/handlers/__init__.py index 9ada370da8..718407064e 100644 --- a/cms/server/api/handlers/__init__.py +++ b/cms/server/api/handlers/__init__.py @@ -43,7 +43,7 @@ (r"/task/(.*)/delete", RemoveTaskHandler), (r"/task/(.*)/testcases/add", AddTestcaseHandler), - (r"/task/(.*)/testcase/([0-9]+)/run", GenerateOutputHandler), + (r"/task/(.*)/testcase/(.*)/run", GenerateOutputHandler), (r"/task/(.*)/test/([0-9]+)/result", SubmissionDetailsHandler), (r"/task/(.*)/test/([0-9]+)/output", SubmissionOutputHandler), ] diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index f6cede7459..ca28614cc4 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -234,13 +234,15 @@ class GenerateOutputHandler(BaseHandler): Based on CWS UserTestHandler """ - def post(self, task_name, testcase_id): + def post(self, task_name, testcase_codename): # TODO: Create a special contest, user, and participation instead of # using the first one you see participation = self.sql_session.query(Participation).first() task = self.get_task_by_name(task_name) - testcase = self.safe_get_item(Testcase, testcase_id) + testcase = self.sql_session.query(Testcase) \ + .filter(Testcase.codename == testcase_codename) \ + .first() request_files = json.loads(str(self.get_argument("files"))) @@ -256,11 +258,12 @@ def post(self, task_name, testcase_id): task_type.get_user_managers(task.submission_format) + ["input"]) - # TODO: If it is necessary, we may have to extract archives + # TODO: If it is necessary, we may need to extract archives # This ensure that the user sent one file for every name in # submission format and no more. - provided = set(list(request_files.keys()) + ["input"]) + provided = set(list(request_files.keys()) + ["input"] + + task_type.get_user_managers(task.submission_format)) if not (required == provided): return self.APIOutput(False, "Please send the correct files.") @@ -381,7 +384,9 @@ class SubmissionDetailsHandler(BaseHandler): def get(self, task_name, user_test_num): task = self.get_task_by_name(task_name) - user_test = self.safe_get_item(UserTest, user_test_num) + user_test = self.sql_session.query(UserTest) \ + .filter(UserTest.id == user_test_num) \ + .first() if user_test is None: return self.APIOutput(False, '') @@ -390,29 +395,15 @@ def get(self, task_name, user_test_num): if tr is None: return self.APIOutput(False, '') - result = {} + result = dict() - if tr is not None and tr.evaluated(): - result['evalres'] = tr.evaluation_text - else: - result['evalres'] = 'none' - return self.APIOutput(True, json.dumps(result)) + result['evalres'] = tr.evaluation_text - if tr is not None and tr.compiled(): - result['compiled'] = tr.compilation_text - else: - result['compiled'] = 'none' - return self.APIOutput(True, json.dumps(result)) + result['compiled'] = tr.compilation_text - if tr.compilation_time is None: - result['time'] = 'none' - else: - result['time'] = tr.compilation_time + result['time'] = tr.compilation_time - if tr.compilation_memory is None: - result['memory'] = 'none' - else: - result['memory'] = tr.compilation_memory + result['memory'] = tr.compilation_memory return self.APIOutput(True, json.dumps(result)) From 4cb9430dd0e78beb19eca35e98b8518265467ac0 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Sat, 27 May 2017 19:38:34 +0430 Subject: [PATCH 13/21] Change the output format of API usertest output file --- cms/server/api/handlers/handler.py | 30 +++++++++++++++++++++--------- cms/server/api/templates/test.html | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index ca28614cc4..11c4acdf91 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -31,6 +31,7 @@ import re import json import base64 +import gevent import tornado.web @@ -38,6 +39,7 @@ from cms.db import Attachment, Dataset, Session, Manager, Submission, \ SubmissionFormatElement, Task, UserTest, UserTestFile, \ UserTestManager, Testcase, Participation +from cms.db.filecacher import FileCacher from cms.grading.tasktypes import get_task_type from cms.grading.languagemanager import get_language from cmscommon.datetime import make_datetime, make_timestamp @@ -311,8 +313,8 @@ def post(self, task_name, testcase_codename): try: path = os.path.join( config.tests_local_copy_path.replace("%s", - config.data_dir), - 'API') + config.data_dir) + , 'API') if not os.path.exists(path): os.makedirs(path) # Pickle in ASCII format produces str, not unicode, @@ -408,7 +410,7 @@ def get(self, task_name, user_test_num): return self.APIOutput(True, json.dumps(result)) -class SubmissionOutputHandler(FileHandler): +class SubmissionOutputHandler(BaseHandler): """Send back a submission output. Based on CWS UserTestIOHandler @@ -418,17 +420,27 @@ def get(self, task_name, test_id): user_test = self.safe_get_item(UserTest, test_id) if user_test is None: - raise tornado.web.HTTPError(404) + return self.APIOutput(False, "No usertest with the given ID") tr = user_test.get_result(task.active_dataset) digest = tr.output if tr is not None else None self.sql_session.close() if digest is None: - raise tornado.web.HTTPError(404) + return self.APIOutput(False, "Digest is none") - mimetype = 'text/plain' + file = self.application.service.file_cacher.get_file(digest) + result = str() - self.fetch(digest, mimetype, 'output') - # TODO: Probably best not send the file in this way, but to - # write the content, as a base64 encoded string. + ret = True + while ret: + data = file.read(FileCacher.CHUNK_SIZE) + length = len(data) + result += data + if length < FileCacher.CHUNK_SIZE: + file.close() + ret = False + + gevent.sleep(0) + + return self.APIOutput(True, base64.b64encode(result)) diff --git a/cms/server/api/templates/test.html b/cms/server/api/templates/test.html index 7288bf46c6..ba9544bfa7 100644 --- a/cms/server/api/templates/test.html +++ b/cms/server/api/templates/test.html @@ -23,7 +23,7 @@


-
+ Files: For example: '{'simple.%l': 'things'}'
Language: For example: 'C++11 / g++'
From 7517a80e04a0e1f5ed8bb7c09ac5f8226cd5807b Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Sat, 27 May 2017 19:44:36 +0430 Subject: [PATCH 14/21] Check if there is a task with the given name before creating one by API --- cms/server/api/handlers/handler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 11c4acdf91..4f6359da98 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -111,9 +111,15 @@ def post(self): attrs = dict() self.get_string(attrs, "name", empty=None) - assert attrs.get("name") is not None, "No task name specified." + if attrs.get("name") is None: + self.APIOutput(False, "No task name specified.") attrs["title"] = attrs["name"] + # Check if the task already exists + task = self.get_task_by_name(attrs["name"]) + if task is not None: + return self.APIOutput(False, 'A problem with this name already exists') + self.get_submission_format(attrs) # Create the task. From f0e18ec89f7f1b8c72e36eeb447d97ff9ae024ff Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Sat, 3 Jun 2017 16:34:47 +0430 Subject: [PATCH 15/21] Add remove testcase handler to API --- cms/server/api/handlers/__init__.py | 4 +- cms/server/api/handlers/base.py | 40 ++++++++++++-------- cms/server/api/handlers/handler.py | 57 ++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/cms/server/api/handlers/__init__.py b/cms/server/api/handlers/__init__.py index 718407064e..1066f2928a 100644 --- a/cms/server/api/handlers/__init__.py +++ b/cms/server/api/handlers/__init__.py @@ -29,6 +29,7 @@ AddTaskHandler, \ RemoveTaskHandler, \ AddTestcaseHandler, \ + DeleteTestcaseHandler, \ GenerateOutputHandler, \ SubmissionDetailsHandler, \ SubmissionOutputHandler @@ -40,8 +41,9 @@ (r"/languages", LanguagesHandler), (r"/tasks/add", AddTaskHandler), - (r"/task/(.*)/delete", RemoveTaskHandler), + (r"/task/(.*)/remove", RemoveTaskHandler), (r"/task/(.*)/testcases/add", AddTestcaseHandler), + (r"/task/(.*)/testcase/(.*)/delete", DeleteTestcaseHandler), (r"/task/(.*)/testcase/(.*)/run", GenerateOutputHandler), (r"/task/(.*)/test/([0-9]+)/result", SubmissionDetailsHandler), diff --git a/cms/server/api/handlers/base.py b/cms/server/api/handlers/base.py index 601734e80a..ee7203c99c 100644 --- a/cms/server/api/handlers/base.py +++ b/cms/server/api/handlers/base.py @@ -36,6 +36,7 @@ import json import logging import traceback +import ipaddress from datetime import datetime, timedelta from functools import wraps @@ -51,7 +52,7 @@ UserTest from cms.grading.scoretypes import get_score_type_class from cms.grading.tasktypes import get_task_type_class -from cms.server import CommonRequestHandler, file_handler_gen, get_url_root +from cms.server import CommonRequestHandler, file_handler_gen from cmscommon.datetime import make_datetime @@ -121,19 +122,26 @@ def parse_datetime(value): raise ValueError("Can't cast %s to datetime." % value) -def parse_ip_address_or_subnet(ip_list): - """Validate a comma-separated list of IP addresses or subnets.""" - for value in ip_list.split(","): - address, sep, subnet = value.partition("/") - if sep != "": - subnet = int(subnet) - assert 0 <= subnet < 32 - fields = address.split(".") - assert len(fields) == 4 - for field in fields: - num = int(field) - assert 0 <= num < 256 - return ip_list +def parse_ip_networks(networks): + """Parse and validate a comma-separated list of IP networks. + + networks (unicode): a comma-separated list of IP networks, which + are IP addresses (both in v4 and v6 formats) with, possibly, a + subnet mask specified as a "/" suffix (in that case all + unmasked bits of the address have to be zeros). + + return ([ipaddress.IPv4Network|ipaddress.IPv6Network]): the parsed + and normalized networks converted to an appropriate type. + + """ + result = list() + for network in networks.split(","): + network = network.strip() + try: + result.append(ipaddress.ip_network(network)) + except ValueError: + raise ValueError("Can't cast %s to an IP network." % network) + return result class BaseHandler(CommonRequestHandler): @@ -202,7 +210,7 @@ def render_params(self): else "v" + __version__[:3] params["timestamp"] = make_datetime() params["contest"] = self.contest - params["url_root"] = get_url_root(self.request.path) + params["url"] = self.url if self.current_user is not None: params["current_user"] = self.current_user if self.contest is not None: @@ -287,7 +295,7 @@ def get_bool(self, dest, name): get_datetime = argument_reader(parse_datetime) - get_ip_address_or_subnet = argument_reader(parse_ip_address_or_subnet) + get_ip_networks = argument_reader(parse_ip_networks) def get_submission_format(self, dest): """Parse the submission format. diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 4f6359da98..839cba9189 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -38,7 +38,7 @@ from cms import config from cms.db import Attachment, Dataset, Session, Manager, Submission, \ SubmissionFormatElement, Task, UserTest, UserTestFile, \ - UserTestManager, Testcase, Participation + UserTestManager, Testcase, Participation, Contest from cms.db.filecacher import FileCacher from cms.grading.tasktypes import get_task_type from cms.grading.languagemanager import get_language @@ -128,6 +128,11 @@ def post(self): except Exception as error: return self.APIOutput(False, "Invalid fields: %s" % error) + # TODO: use another way to select the contest + contest = self.sql_session.query(Contest).first() + task.num = len(contest.tasks) + task.contest = contest + try: attrs = dict() @@ -179,8 +184,20 @@ class RemoveTaskHandler(BaseHandler): def get(self, task_name): task = self.get_task_by_name(task_name) + contest_id = task.contest_id + num = task.num self.sql_session.delete(task) + + # Keeping the tasks' nums to the range 0... n - 1. + if contest_id is not None: + following_tasks = self.sql_session.query(Task) \ + .filter(Task.contest_id == contest_id) \ + .filter(Task.num > num) \ + .all() + for task in following_tasks: + task.num -= 1 + if self.try_commit(): return self.APIOutput(True, 'Successful') else: @@ -199,10 +216,16 @@ def post(self, task_name): codename = self.get_argument("testcase_id") + testcase = self.sql_session.query(Testcase) \ + .filter(Testcase.codename == codename) \ + .first() + if testcase is not None: + return self.APIOutput(False, 'A testcase with this code already exists') + try: input_base64 = str(self.get_argument("input")) input_body = str(base64.b64decode(input_base64)) - output_base64 = str(self.get_argument("output")) + output_base64 = str(self.get_argument("output", '')) output_body = str(base64.b64decode(output_base64)) except TypeError: @@ -236,6 +259,30 @@ def post(self, task_name): return self.APIOutput(False, "Operation Unsuccessful!") +class DeleteTestcaseHandler(BaseHandler): + """Deletes a testcase + + Based on AWS DeleteTestcaseHandler + """ + + def get(self, task_name, codename): + testcase = self.sql_session.query(Testcase) \ + .filter(Testcase.codename == codename) \ + .first() + task = self.get_task_by_name(task_name) + dataset = task.active_dataset + + # Protect against URLs providing incompatible parameters. + if dataset is not testcase.dataset: + return self.APIOutput(False, 'Invalid info') + + self.sql_session.delete(testcase) + if self.try_commit(): + self.APIOutput(True, 'Successful') + else: + self.APIOutput(False, 'Unsuccessful') + + class GenerateOutputHandler(BaseHandler): """Creates a user test on a task for a perticular testcase @@ -387,7 +434,7 @@ def post(self, task_name, testcase_codename): class SubmissionDetailsHandler(BaseHandler): """Gets the result of the submission - Based on CWS UserTestDetailsHandler + Based on CWS UserTestStatusHandler """ def get(self, task_name, user_test_num): @@ -397,11 +444,11 @@ def get(self, task_name, user_test_num): .first() if user_test is None: - return self.APIOutput(False, '') + return self.APIOutput(False, 'No usertest') tr = user_test.get_result(task.active_dataset) if tr is None: - return self.APIOutput(False, '') + return self.APIOutput(False, 'No result') result = dict() From b5394b80da5128a77dc583c2b7f98887cb98e799 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Sun, 4 Jun 2017 22:05:05 +0430 Subject: [PATCH 16/21] Return execution time instead of compilation time in API --- cms/server/api/handlers/handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 839cba9189..e7803e3982 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -443,7 +443,7 @@ def get(self, task_name, user_test_num): .filter(UserTest.id == user_test_num) \ .first() - if user_test is None: + if user_test is None or task is None: return self.APIOutput(False, 'No usertest') tr = user_test.get_result(task.active_dataset) @@ -456,9 +456,9 @@ def get(self, task_name, user_test_num): result['compiled'] = tr.compilation_text - result['time'] = tr.compilation_time + result['time'] = tr.execution_time - result['memory'] = tr.compilation_memory + result['memory'] = tr.execution_memory return self.APIOutput(True, json.dumps(result)) From 54503a4f3ab43750c87d53f6d59b92f5ed843c10 Mon Sep 17 00:00:00 2001 From: Amir Keivan Mohtashami Date: Sun, 11 Jun 2017 22:00:25 +0430 Subject: [PATCH 17/21] Fixed bugs --- cms/server/api/handlers/handler.py | 17 ++++++++--------- scripts/cmsAPIWebServer | 0 2 files changed, 8 insertions(+), 9 deletions(-) mode change 100644 => 100755 scripts/cmsAPIWebServer diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index e7803e3982..6b983feb77 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -163,7 +163,7 @@ def post(self): try: digest = self.application.service.file_cacher.put_file_content( body, - "Task manager for %s" % attrs["name"]) + "Task manager for %s" % filename) except Exception as error: return self.APIOutput(False, "Manager storage failed: %s" % error) @@ -447,18 +447,17 @@ def get(self, task_name, user_test_num): return self.APIOutput(False, 'No usertest') tr = user_test.get_result(task.active_dataset) - if tr is None: - return self.APIOutput(False, 'No result') - result = dict() + if tr is None: + result['evalres'] = None + else: + result['evalres'] = tr.evaluation_text - result['evalres'] = tr.evaluation_text - - result['compiled'] = tr.compilation_text + result['compiled'] = tr.compilation_text - result['time'] = tr.execution_time + result['time'] = tr.execution_time - result['memory'] = tr.execution_memory + result['memory'] = tr.execution_memory return self.APIOutput(True, json.dumps(result)) diff --git a/scripts/cmsAPIWebServer b/scripts/cmsAPIWebServer old mode 100644 new mode 100755 From 9758fe20626313629157661051e8befac2f3c4ff Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Tue, 13 Jun 2017 16:04:17 +0430 Subject: [PATCH 18/21] Fix bugs --- cms/server/api/handlers/handler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index 6b983feb77..f45bacc1bf 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -449,14 +449,12 @@ def get(self, task_name, user_test_num): tr = user_test.get_result(task.active_dataset) result = dict() if tr is None: - result['evalres'] = None + result['result'] = False else: + result['result'] = True result['evalres'] = tr.evaluation_text - result['compiled'] = tr.compilation_text - result['time'] = tr.execution_time - result['memory'] = tr.execution_memory return self.APIOutput(True, json.dumps(result)) From e0215e65885688ea50f98935e72f2f8f7de7e0ae Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 15 Jun 2017 15:24:18 +0430 Subject: [PATCH 19/21] Compile communication manager sent to API --- cms/server/api/handlers/handler.py | 37 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index f45bacc1bf..e6cedc3b51 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -32,6 +32,7 @@ import json import base64 import gevent +import tempfile import tornado.web @@ -114,9 +115,10 @@ def post(self): if attrs.get("name") is None: self.APIOutput(False, "No task name specified.") attrs["title"] = attrs["name"] + name = attrs["name"] # Check if the task already exists - task = self.get_task_by_name(attrs["name"]) + task = self.get_task_by_name(name) if task is not None: return self.APIOutput(False, 'A problem with this name already exists') @@ -154,6 +156,25 @@ def post(self): return self.APIOutput(False, "Invalid fields: %s" % error) managers = json.loads(str(self.get_argument("managers"))) + if 'manager.cpp' in managers: + try: + body = base64.b64decode(managers['manager.cpp']) + except TypeError: + return self.APIOutput(False, "Invalid data: Please provide a base64 encoded file") + + tempdir = tempfile.mkdtemp() + src_path = os.path.join(tempdir, 'manager.cpp') + exec_path = os.path.join(tempdir, 'manager') + with open(src_path, 'wb') as src_file: + src_file.write(body) + os.system('g++ -x c++ -O2 -static -o %s %s' % (exec_path, src_path)) + digest = self.application.service.file_cacher.put_file_from_path( + exec_path, + "Manager for task %s" % name) + manager = Manager('manager', digest, dataset=dataset) + self.sql_session.add(manager) + del managers['manager.cpp'] + for filename in managers: try: body = base64.b64decode(managers[filename]) @@ -163,7 +184,7 @@ def post(self): try: digest = self.application.service.file_cacher.put_file_content( body, - "Task manager for %s" % filename) + "Task manager for %s" % name) except Exception as error: return self.APIOutput(False, "Manager storage failed: %s" % error) @@ -393,7 +414,11 @@ def post(self, task_name, testcase_codename): filename, make_timestamp(self.timestamp))) file_digests[filename] = digest - # Adding testcase's digest + # Now Adding managers' digests + for manager in task.active_dataset.managers.values(): + file_digests[manager.filename] = manager.digest + + # Finally Adding testcase's digest file_digests["input"] = input_digest # In case of error, the server aborts the submission @@ -414,10 +439,10 @@ def post(self, task_name, testcase_codename): self.sql_session.add( UserTestFile(filename, digest, user_test=user_test)) for filename in task_type.get_user_managers(task.submission_format): - digest = file_digests[filename] if submission_lang is not None: extension = get_language(submission_lang).source_extension filename = filename.replace(".%l", extension) + digest = file_digests[filename] self.sql_session.add( UserTestManager(filename, digest, user_test=user_test)) @@ -452,8 +477,8 @@ def get(self, task_name, user_test_num): result['result'] = False else: result['result'] = True - result['evalres'] = tr.evaluation_text - result['compiled'] = tr.compilation_text + result['evalres'] = json.loads(tr.evaluation_text) + result['compiled'] = json.loads(tr.compilation_text) result['time'] = tr.execution_time result['memory'] = tr.execution_memory From 6d849124fd0f8fb4f8246f5ae89cd76d1f5f7ffa Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Thu, 15 Jun 2017 16:51:59 +0430 Subject: [PATCH 20/21] Change usertest outcome representation in API --- cms/server/api/handlers/handler.py | 35 ++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index e6cedc3b51..db9e6472fd 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -477,11 +477,42 @@ def get(self, task_name, user_test_num): result['result'] = False else: result['result'] = True - result['evalres'] = json.loads(tr.evaluation_text) - result['compiled'] = json.loads(tr.compilation_text) + + if tr.evaluation_text is None: + result['evalres'] = None + else: + result['evalres'] = json.loads(tr.evaluation_text) + + if tr.compilation_text is None: + result['compiled'] = None + else: + result['compiled'] = json.loads(tr.compilation_text) + result['time'] = tr.execution_time result['memory'] = tr.execution_memory + digest = tr.output if tr is not None else None + self.sql_session.close() + + if digest is None: + result['output'] = None + else: + out_file = self.application.service.file_cacher.get_file(digest) + out_res = str() + + ret = True + while ret: + data = out_file.read(FileCacher.CHUNK_SIZE) + length = len(data) + out_res += data + if length < FileCacher.CHUNK_SIZE: + out_file.close() + ret = False + + gevent.sleep(0) + + result['output'] = base64.b64encode(out_res) + return self.APIOutput(True, json.dumps(result)) From f283b505895e46ad6e2c49ee4b36337c0de9cc83 Mon Sep 17 00:00:00 2001 From: Kiarash Golezardi Date: Mon, 17 Jul 2017 01:38:13 +0430 Subject: [PATCH 21/21] Show more execution details in API --- cms/server/api/handlers/handler.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cms/server/api/handlers/handler.py b/cms/server/api/handlers/handler.py index db9e6472fd..907290e232 100644 --- a/cms/server/api/handlers/handler.py +++ b/cms/server/api/handlers/handler.py @@ -477,21 +477,30 @@ def get(self, task_name, user_test_num): result['result'] = False else: result['result'] = True + result['message'] = str() if tr.evaluation_text is None: result['evalres'] = None else: - result['evalres'] = json.loads(tr.evaluation_text) + result['evalres'] = json.loads(tr.evaluation_text)[0] + result['message'] += result['evalres'] if tr.compilation_text is None: result['compiled'] = None else: - result['compiled'] = json.loads(tr.compilation_text) + result['compiled'] = json.loads(tr.compilation_text)[0] + result['message'] += '\n' + tr.compilation_stdout + '\n' + tr.compilation_stderr result['time'] = tr.execution_time result['memory'] = tr.execution_memory - digest = tr.output if tr is not None else None + # Remove starting and trailing new lines from message + while result['message'] and result['message'][0] == '\n': + result['message'] = result['message'][1:] + while result['message'] and result['message'][-1] == '\n': + result['message'] = result['message'][:-2] + + digest = tr.output self.sql_session.close() if digest is None: