def post(self, task_name): task = self.get_task(task_name) if task is None: raise tornado.web.HTTPError(404) # Only set the official bit when the user can compete and we are not in # analysis mode. official = self.r_params["actual_phase"] == 0 query_args = dict() # fill in any missing files with text contents for filename in task.submission_format: if filename not in self.request.files: ta_input = self.get_argument('ta_' + filename) # potentially append new file if ta_input: stored_filename = filename language = self.get_argument("language", None) if language: extension = get_language(language).source_extension stored_filename = filename.replace(".%l", extension) logger.info("Appending file from contents: " + stored_filename) ta_file = HTTPFile(filename=stored_filename, body=ta_input.encode(), type='text/plain') self.request.files[filename] = [ta_file] try: submission = accept_submission(self.sql_session, self.service.file_cacher, self.current_user, task, self.timestamp, self.request.files, self.get_argument("language", None), official) self.sql_session.commit() except UnacceptableSubmission as e: logger.info("Sent error: `%s' - `%s'", e.subject, e.formatted_text) self.notify_error(e.subject, e.text, e.text_params) else: self.service.evaluation_service.new_submission( submission_id=submission.id) self.notify_success( N_("Submission received"), N_("Your submission has been received " "and is currently being evaluated.")) # The argument (encrypted submission id) is not used by CWS # (nor it discloses information to the user), but it is # useful for automatic testing to obtain the submission id). query_args["submission_id"] = \ encrypt_number(submission.id, config.secret_key) query_args["tab"] = "submissions" # self.redirect(self.contest_url("tasks", task.name, "submissions", # **query_args)) self.redirect( self.contest_url("tasks", task.name, "full", **query_args))
def post(self, task_name): task = self.get_task(task_name) if task is None: raise tornado.web.HTTPError(404) # Only set the official bit when the user can compete and we are not in # analysis mode. official = self.r_params["actual_phase"] == 0 query_args = dict() try: submission = accept_submission( self.sql_session, self.service.file_cacher, self.current_user, task, self.timestamp, self.request.files, self.get_argument("language", None), official) self.sql_session.commit() except UnacceptableSubmission as e: logger.info("Sent error: `%s' - `%s'", e.subject, e.formatted_text) self.notify_error(e.subject, e.text, e.text_params) else: self.service.evaluation_service.new_submission( submission_id=submission.id) self.notify_success(N_("Submission received"), N_("Your submission has been received " "and is currently being evaluated.")) # The argument (encrypted submission id) is not used by CWS # (nor it discloses information to the user), but it is # useful for automatic testing to obtain the submission id). query_args["submission_id"] = \ encrypt_number(submission.id, config.secret_key) self.redirect(self.contest_url("tasks", task.name, "submissions", **query_args))
def post(self, task_name): task = self.get_task(task_name) if task is None: raise tornado.web.HTTPError(404) # Only set the official bit when the user can compete and we are not in # analysis mode. official = self.r_params["actual_phase"] == 0 query_args = dict() try: submission = accept_submission( self.sql_session, self.service.file_cacher, self.current_user, task, self.timestamp, self.request.files, self.get_argument("language", None), official) self.sql_session.commit() except UnacceptableSubmission as e: logger.info("Sent error: `%s' - `%s'", e.subject, e.text) self.notify_error(e.subject, e.text) else: self.service.evaluation_service.new_submission( submission_id=submission.id) self.notify_success(N_("Submission received"), N_("Your submission has been received " "and is currently being evaluated.")) # The argument (encrypted submission id) is not used by CWS # (nor it discloses information to the user), but it is # useful for automatic testing to obtain the submission id). query_args["submission_id"] = \ encrypt_number(submission.id, config.secret_key) self.redirect(self.contest_url("tasks", task.name, "submissions", **query_args))
def __init__(self, browser, task, base_url=None, submissions_path=None): GenericRequest.__init__(self, browser, base_url) self.url = "%ssubmit/%s" % (self.base_url, encrypt_number(task[0])) self.task = task self.submissions_path = submissions_path self.data = {}
def post(self, task_name): if not self.r_params["testing_enabled"]: raise tornado.web.HTTPError(404) task = self.get_task(task_name) if task is None: raise tornado.web.HTTPError(404) query_args = dict() try: user_test = accept_user_test(self.sql_session, self.service.file_cacher, self.current_user, task, self.timestamp, self.request.files, self.get_argument("language", None)) self.sql_session.commit() except TestingNotAllowed: logger.warning("User %s tried to make test on task %s.", self.current_user.user.username, task_name) raise tornado.web.HTTPError(404) except UnacceptableUserTest as e: logger.info("Sent error: `%s' - `%s'", e.subject, e.text) self.notify_error(e.subject, e.text) else: self.service.evaluation_service.new_user_test( user_test_id=user_test.id) self.notify_success( N_("Test received"), N_("Your test has been received " "and is currently being executed.")) # The argument (encrypted 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). query_args["user_test_id"] = \ encrypt_number(user_test.id, config.secret_key) self.redirect( self.contest_url("testing", task_name=task.name, **query_args))
def post(self, task_name): if not self.r_params["testing_enabled"]: raise tornado.web.HTTPError(404) task = self.get_task(task_name) if task is None: raise tornado.web.HTTPError(404) query_args = dict() try: user_test = accept_user_test( self.sql_session, self.service.file_cacher, self.current_user, task, self.timestamp, self.request.files, self.get_argument("language", None)) self.sql_session.commit() except TestingNotAllowed: logger.warning("User %s tried to make test on task %s.", self.current_user.user.username, task_name) raise tornado.web.HTTPError(404) except UnacceptableUserTest as e: logger.info("Sent error: `%s' - `%s'", e.subject, e.text) self.notify_error(e.subject, e.text) else: self.service.evaluation_service.new_user_test( user_test_id=user_test.id) self.notify_success(N_("Test received"), N_("Your test has been received " "and is currently being executed.")) # The argument (encrypted 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). query_args["user_test_id"] = \ encrypt_number(user_test.id, config.secret_key) self.redirect(self.contest_url("testing", task_name=task.name, **query_args))
def post(self, task_name): participation = self.current_user if not self.r_params["testing_enabled"]: self.redirect("/") return try: task = self.contest.get_task(task_name) except KeyError: raise tornado.web.HTTPError(404) # Check that the task is testable task_type = get_task_type(dataset=task.active_dataset) if not task_type.testable: logger.warning("User %s tried to make test on task %s.", participation.user.username, task_name) raise tornado.web.HTTPError(404) # Alias for easy access contest = self.contest # Enforce maximum number of user_tests try: if contest.max_user_test_number is not None: user_test_c = self.sql_session.query(func.count(UserTest.id))\ .join(UserTest.task)\ .filter(Task.contest == contest)\ .filter(UserTest.participation == participation)\ .scalar() if user_test_c >= contest.max_user_test_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d tests among all tasks.") % contest.max_user_test_number) if task.max_user_test_number is not None: user_test_t = self.sql_session.query(func.count(UserTest.id))\ .filter(UserTest.task == task)\ .filter(UserTest.participation == participation)\ .scalar() if user_test_t >= task.max_user_test_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d tests on this task.") % task.max_user_test_number) except ValueError as error: self._send_error(self._("Too many tests!"), error.message, task) return # Enforce minimum time between user_tests try: if contest.min_user_test_interval is not None: last_user_test_c = self.sql_session.query(UserTest)\ .join(UserTest.task)\ .filter(Task.contest == contest)\ .filter(UserTest.participation == participation)\ .order_by(UserTest.timestamp.desc())\ .first() if last_user_test_c is not None and \ self.timestamp - last_user_test_c.timestamp < \ contest.min_user_test_interval and \ not self.current_user.unrestricted: raise ValueError( self._("Among all tasks, you can test again " "after %d seconds from last test.") % contest.min_user_test_interval.total_seconds()) # We get the last user_test even if we may not need it # for min_user_test_interval because we may need it later, # in case this is a ALLOW_PARTIAL_SUBMISSION task. last_user_test_t = self.sql_session.query(UserTest)\ .filter(UserTest.participation == participation)\ .filter(UserTest.task == task)\ .order_by(UserTest.timestamp.desc())\ .first() if task.min_user_test_interval is not None: if last_user_test_t is not None and \ self.timestamp - last_user_test_t.timestamp < \ task.min_user_test_interval and \ not self.current_user.unrestricted: raise ValueError( self._("For this task, you can test again " "after %d seconds from last test.") % task.min_user_test_interval.total_seconds()) except ValueError as error: self._send_error( self._("Tests too frequent!"), error.message, task) return # Required files from the user. required = set([sfe.filename for sfe in task.submission_format] + task_type.get_user_managers(task.submission_format) + ["input"]) # 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()): self._send_error( self._("Invalid test format!"), self._("Please select the correct files."), task) return # If the user submitted an archive, extract it and use content # as request.files. But only valid for "output only" (i.e., # not for submissions requiring a programming language # identification). if len(self.request.files) == 1 and \ self.request.files.keys()[0] == "submission": if any(filename.endswith(".%l") for filename in required): self._send_error( self._("Invalid test format!"), self._("Please select the correct files."), task) return archive_data = self.request.files["submission"][0] del self.request.files["submission"] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: self._send_error( self._("Invalid archive format!"), self._("The submitted archive could not be opened."), task) return # Extract the archive. unpacked_dir = archive.unpack() for name in archive.namelist(): filename = os.path.basename(name) body = open(os.path.join(unpacked_dir, filename), "r").read() self.request.files[filename] = [{ 'filename': filename, 'body': body }] archive.cleanup() # This ensure that the user sent one file for every name in # submission format and no more. Less is acceptable if task # type says so. provided = set(self.request.files.keys()) if not (required == provided or (task_type.ALLOW_PARTIAL_SUBMISSION and required.issuperset(provided))): self._send_error( self._("Invalid test format!"), self._("Please select the correct files."), task) return # 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"]) # If we allow partial submissions, implicitly we recover the # non-submitted files from the previous submission. And put them # in file_digests (i.e. like they have already been sent to FS). submission_lang = None file_digests = {} if task_type.ALLOW_PARTIAL_SUBMISSION and last_user_test_t is not None: for filename in required.difference(provided): if filename in last_user_test_t.files: # If we retrieve a language-dependent file from # last submission, we take not that language must # be the same. if "%l" in filename: submission_lang = last_user_test_t.language file_digests[filename] = \ last_user_test_t.files[filename].digest # We need to ensure that everytime we have a .%l in our # filenames, the user has one amongst ".cpp", ".c", or ".pas, # and that all these are the same (i.e., no mixed-language # submissions). error = None for our_filename in files: user_filename = files[our_filename][0] if our_filename.find(".%l") != -1: lang = filename_to_language(user_filename) if lang is None: error = self._("Cannot recognize test's language.") break elif submission_lang is not None and \ submission_lang != lang: error = self._("All sources must be in the same language.") break else: submission_lang = lang if error is not None: self._send_error(self._("Invalid test!"), error, task) return # 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"]): self._send_error( self._("Test too big!"), self._("Each source file must be at most %d bytes long.") % config.max_submission_length, task) return if len(files["input"][1]) > config.max_input_length: self._send_error( self._("Input too big!"), self._("The input file must be at most %d bytes long.") % config.max_input_length, task) return # 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... 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 # In case of error, the server aborts the submission except Exception as error: logger.error("Storage failed! %s", error) self._send_error( self._("Test storage failed!"), self._("Please try again."), task) return # 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: filename = filename.replace("%l", submission_lang) 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) self.application.service.add_notification( participation.user.username, self.timestamp, self._("Test received"), self._("Your test has been received " "and is currently being executed."), NOTIFICATION_SUCCESS) # 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.redirect("/testing?%s&%s" % ( quote(task.name, safe=''), encrypt_number(user_test.id)))
def __init__(self, browser, task_id, base_url=None): GenericRequest.__init__(self, browser, base_url) self.url = "%stasks/%s" % (self.base_url, encrypt_number(task_id)) self.task_id = task_id
def post(self, task_name): participation = self.current_user try: task = self.contest.get_task(task_name) except KeyError: raise tornado.web.HTTPError(404) self.fallback_page = ["tasks", task.name, "submissions"] # Alias for easy access contest = self.contest # Enforce maximum number of submissions try: if contest.max_submission_number is not None: submission_c = self.sql_session\ .query(func.count(Submission.id))\ .join(Submission.task)\ .filter(Task.contest == contest)\ .filter(Submission.participation == participation)\ .scalar() if submission_c >= contest.max_submission_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d submissions among all tasks.") % contest.max_submission_number) if task.max_submission_number is not None: submission_t = self.sql_session\ .query(func.count(Submission.id))\ .filter(Submission.task == task)\ .filter(Submission.participation == participation)\ .scalar() if submission_t >= task.max_submission_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d submissions on this task.") % task.max_submission_number) except ValueError as error: self._send_error(self._("Too many submissions!"), error.message) return # Enforce minimum time between submissions try: if contest.min_submission_interval is not None: last_submission_c = self.sql_session.query(Submission)\ .join(Submission.task)\ .filter(Task.contest == contest)\ .filter(Submission.participation == participation)\ .order_by(Submission.timestamp.desc())\ .first() if last_submission_c is not None and \ self.timestamp - last_submission_c.timestamp < \ contest.min_submission_interval and \ not self.current_user.unrestricted: raise ValueError( self._("Among all tasks, you can submit again " "after %d seconds from last submission.") % contest.min_submission_interval.total_seconds()) # We get the last submission even if we may not need it # for min_submission_interval because we may need it later, # in case this is a ALLOW_PARTIAL_SUBMISSION task. last_submission_t = self.sql_session.query(Submission)\ .filter(Submission.task == task)\ .filter(Submission.participation == participation)\ .order_by(Submission.timestamp.desc())\ .first() if task.min_submission_interval is not None: if last_submission_t is not None and \ self.timestamp - last_submission_t.timestamp < \ task.min_submission_interval and \ not self.current_user.unrestricted: raise ValueError( self._("For this task, you can submit again " "after %d seconds from last submission.") % task.min_submission_interval.total_seconds()) except ValueError as error: self._send_error(self._("Submissions too frequent!"), error.message) return # Required files from the user. required = set([sfe.filename for sfe in task.submission_format]) # 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()): self._send_error(self._("Invalid submission format!"), self._("Please select the correct files.")) return # If the user submitted an archive, extract it and use content # as request.files. But only valid for "output only" (i.e., # not for submissions requiring a programming language # identification). if len(self.request.files) == 1 and \ self.request.files.keys()[0] == "submission": if any(filename.endswith(".%l") for filename in required): self._send_error(self._("Invalid submission format!"), self._("Please select the correct files."), task) return archive_data = self.request.files["submission"][0] del self.request.files["submission"] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: self._send_error( self._("Invalid archive format!"), self._("The submitted archive could not be opened.")) return # Extract the archive. unpacked_dir = archive.unpack() for name in archive.namelist(): filename = os.path.basename(name) if filename not in required: continue body = open(os.path.join(unpacked_dir, name), "r").read() self.request.files[filename] = [{ 'filename': filename, 'body': body }] archive.cleanup() # This ensure that the user sent one file for every name in # submission format and no more. Less is acceptable if task # type says so. task_type = get_task_type(dataset=task.active_dataset) provided = set(self.request.files.keys()) if not (required == provided or (task_type.ALLOW_PARTIAL_SUBMISSION and required.issuperset(provided))): self._send_error(self._("Invalid submission format!"), self._("Please select the correct files.")) return # 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) # If we allow partial submissions, we implicitly recover the # non-submitted files from the previous submission (if it has # the same programming language of the current one), and put # them in file_digests (since they are already in FS). file_digests = {} # if task_type.ALLOW_PARTIAL_SUBMISSION and \ # last_submission_t is not None and \ # (submission_lang is None or # submission_lang == last_submission_t.language): # submission_lang = last_submission_t.language # for filename in required.difference(provided): # if filename in last_submission_t.files: # file_digests[filename] = \ # last_submission_t.files[filename].digest # 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 submission language.") elif submission_lang not in contest.languages: error = self._("Language %s not allowed in this contest.") \ % submission_lang if error is not None: self._send_error(self._("Invalid submission!"), error) return # Check if submitted files are small enough. if sum([len(f[1]) for f in files.values()]) > config.max_submission_length: self._send_error( self._("Submission too big!"), self._("Size of each submission must be at most %d bytes.") % config.max_submission_length) return # All checks done, submission accepted. # Attempt to store the submission locally to be able to # recover a failure. if config.submit_local_copy: try: path = os.path.join( config.submit_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.warning("Submission local copy failed.", exc_info=True) # We now have to send all the files to the destination... try: for filename in files: digest = self.application.service.file_cacher.put_file_content( files[filename][1], "Submission file %s sent by %s at %d." % (filename, participation.user.username, make_timestamp(self.timestamp))) file_digests[filename] = digest # In case of error, the server aborts the submission except Exception as error: logger.error("Storage failed! %s", error) self._send_error(self._("Submission storage failed!"), self._("Please try again.")) return # All the files are stored, ready to submit! logger.info("All files stored for submission sent by %s", participation.user.username) # Only set the official bit when the user can compete and we are not in # analysis mode. official = self.r_params["actual_phase"] == 0 submission = Submission(self.timestamp, submission_lang, task=task, participation=participation, official=official) for filename, digest in file_digests.items(): self.sql_session.add(File(filename, digest, submission=submission)) self.sql_session.add(submission) self.sql_session.commit() # Store some data out of the session so we can close it before issuing # RPCs. username = participation.user.username submission_id = submission.id logger.metric("submission_added", submission_id=submission.id, language=submission.language, task_id=task.id, participant_id=participation.id, value=1) self.sql_session.close() try: random_service(self.application.service.evaluation_services)\ .new_submission(submission_id=submission_id) except IndexError: logger.error("No evaluation services found. " "Leaving the submission to be " "discovered by sweep. ") self.application.service.add_notification( username, self.timestamp, self._("Submission received"), self._("Your submission has been received " "and is currently being evaluated."), NOTIFICATION_SUCCESS) # The argument (encripted submission id) is not used by CWS # (nor it discloses information to the user), but it is useful # for automatic testing to obtain the submission id). self.redirect( self.contest_url(*self.fallback_page, submission_id=encrypt_number(submission.id)))
def test_encrypt_and_decrypt(self): self.assertEqual( decrypt_number(encrypt_number(123, self.key), self.key), 123)
def test_encrypt_and_decrypt_big(self): self.assertEqual( decrypt_number(encrypt_number(10**42, self.key), self.key), 10**42)
def test_encrypt_and_decrypt_negative(self): self.assertEqual( decrypt_number(encrypt_number(-123, self.key), self.key), -123)
def test_encrypt_and_decrypt_big(self): self.assertEqual( decrypt_number(encrypt_number(10 ** 42, self.key), self.key), 10 ** 42)
def post(self, task_name): participation = self.current_user try: task = self.contest.get_task(task_name) except KeyError: raise tornado.web.HTTPError(404) # Alias for easy access contest = self.contest # Enforce maximum number of submissions try: if contest.max_submission_number is not None: submission_c = self.sql_session\ .query(func.count(Submission.id))\ .join(Submission.task)\ .filter(Task.contest == contest)\ .filter(Submission.participation == participation)\ .scalar() if submission_c >= contest.max_submission_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d submissions among all tasks.") % contest.max_submission_number) if task.max_submission_number is not None: submission_t = self.sql_session\ .query(func.count(Submission.id))\ .filter(Submission.task == task)\ .filter(Submission.participation == participation)\ .scalar() if submission_t >= task.max_submission_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d submissions on this task.") % task.max_submission_number) except ValueError as error: self.application.service.add_notification( participation.user.username, self.timestamp, self._("Too many submissions!"), error.message, NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # Enforce minimum time between submissions try: if contest.min_submission_interval is not None: last_submission_c = self.sql_session.query(Submission)\ .join(Submission.task)\ .filter(Task.contest == contest)\ .filter(Submission.participation == participation)\ .order_by(Submission.timestamp.desc())\ .first() if last_submission_c is not None and \ self.timestamp - last_submission_c.timestamp < \ contest.min_submission_interval and \ not self.current_user.unrestricted: raise ValueError( self._("Among all tasks, you can submit again " "after %d seconds from last submission.") % contest.min_submission_interval.total_seconds()) # We get the last submission even if we may not need it # for min_submission_interval because we may need it later, # in case this is a ALLOW_PARTIAL_SUBMISSION task. last_submission_t = self.sql_session.query(Submission)\ .filter(Submission.task == task)\ .filter(Submission.participation == participation)\ .order_by(Submission.timestamp.desc())\ .first() if task.min_submission_interval is not None: if last_submission_t is not None and \ self.timestamp - last_submission_t.timestamp < \ task.min_submission_interval and \ not self.current_user.unrestricted: raise ValueError( self._("For this task, you can submit again " "after %d seconds from last submission.") % task.min_submission_interval.total_seconds()) except ValueError as error: self.application.service.add_notification( participation.user.username, self.timestamp, self._("Submissions too frequent!"), error.message, NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # 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()): self.application.service.add_notification( participation.user.username, self.timestamp, self._("Invalid submission format!"), self._("Please select the correct files."), NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # If the user submitted an archive, extract it and use content # as request.files. if len(self.request.files) == 1 and \ self.request.files.keys()[0] == "submission": archive_data = self.request.files["submission"][0] del self.request.files["submission"] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: self.application.service.add_notification( participation.user.username, self.timestamp, self._("Invalid archive format!"), self._("The submitted archive could not be opened."), NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # Extract the archive. unpacked_dir = archive.unpack() for name in archive.namelist(): filename = os.path.basename(name) body = open(os.path.join(unpacked_dir, filename), "r").read() self.request.files[filename] = [{ 'filename': filename, 'body': body }] archive.cleanup() # This ensure that the user sent one file for every name in # submission format and no more. Less is acceptable if task # type says so. task_type = get_task_type(dataset=task.active_dataset) required = set([sfe.filename for sfe in task.submission_format]) provided = set(self.request.files.keys()) if not (required == provided or (task_type.ALLOW_PARTIAL_SUBMISSION and required.issuperset(provided))): self.application.service.add_notification( participation.user.username, self.timestamp, self._("Invalid submission format!"), self._("Please select the correct files."), NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # 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"]) # If we allow partial submissions, implicitly we recover the # non-submitted files from the previous submission. And put them # in file_digests (i.e. like they have already been sent to FS). submission_lang = None file_digests = {} if task_type.ALLOW_PARTIAL_SUBMISSION and \ last_submission_t is not None: for filename in required.difference(provided): if filename in last_submission_t.files: # If we retrieve a language-dependent file from # last submission, we take not that language must # be the same. if "%l" in filename: submission_lang = last_submission_t.language file_digests[filename] = \ last_submission_t.files[filename].digest # We need to ensure that everytime we have a .%l in our # filenames, the user has the extension of an allowed # language, and that all these are the same (i.e., no # mixed-language submissions). error = None for our_filename in files: user_filename = files[our_filename][0] if our_filename.find(".%l") != -1: lang = filename_to_language(user_filename) if lang is None: error = self._("Cannot recognize submission's language.") break elif submission_lang is not None and \ submission_lang != lang: error = self._("All sources must be in the same language.") break elif lang not in contest.languages: error = self._("Language %s not allowed in this contest." % lang) break else: submission_lang = lang if error is not None: self.application.service.add_notification( participation.user.username, self.timestamp, self._("Invalid submission!"), error, NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # Check if submitted files are small enough. if any( [len(f[1]) > config.max_submission_length for f in files.values()]): self.application.service.add_notification( participation.user.username, self.timestamp, self._("Submission too big!"), self._("Each source file must be at most %d bytes long.") % config.max_submission_length, NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # All checks done, submission accepted. # Attempt to store the submission locally to be able to # recover a failure. if config.submit_local_copy: try: path = os.path.join( config.submit_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.warning("Submission local copy failed.", exc_info=True) # We now have to send all the files to the destination... try: for filename in files: digest = self.application.service.file_cacher.put_file_content( files[filename][1], "Submission file %s sent by %s at %d." % (filename, participation.user.username, make_timestamp(self.timestamp))) file_digests[filename] = digest # In case of error, the server aborts the submission except Exception as error: logger.error("Storage failed! %s", error) self.application.service.add_notification( participation.user.username, self.timestamp, self._("Submission storage failed!"), self._("Please try again."), NOTIFICATION_ERROR) self.redirect("/tasks/%s/submissions" % quote(task.name, safe='')) return # All the files are stored, ready to submit! logger.info("All files stored for submission sent by %s", participation.user.username) submission = Submission(self.timestamp, submission_lang, task=task, participation=participation) for filename, digest in file_digests.items(): self.sql_session.add(File(filename, digest, submission=submission)) self.sql_session.add(submission) self.sql_session.commit() self.application.service.evaluation_service.new_submission( submission_id=submission.id) self.application.service.add_notification( participation.user.username, self.timestamp, self._("Submission received"), self._("Your submission has been received " "and is currently being evaluated."), NOTIFICATION_SUCCESS) # The argument (encripted submission id) is not used by CWS # (nor it discloses information to the user), but it is useful # for automatic testing to obtain the submission id). # FIXME is it actually used by something? self.redirect( "/tasks/%s/submissions?%s" % (quote(task.name, safe=''), encrypt_number(submission.id)))
def post(self, task_name): participation = self.current_user if not self.r_params["testing_enabled"]: raise tornado.web.HTTPError(404) try: task = self.contest.get_task(task_name) except KeyError: raise tornado.web.HTTPError(404) self.fallback_page = ["testing"] self.fallback_args = {"task_name": task.name} # Check that the task is testable task_type = get_task_type(dataset=task.active_dataset) if not task_type.testable: logger.warning("User %s tried to make test on task %s.", participation.user.username, task_name) raise tornado.web.HTTPError(404) # Alias for easy access contest = self.contest # Enforce maximum number of user_tests try: if contest.max_user_test_number is not None: user_test_c = self.sql_session.query(func.count(UserTest.id))\ .join(UserTest.task)\ .filter(Task.contest == contest)\ .filter(UserTest.participation == participation)\ .scalar() if user_test_c >= contest.max_user_test_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d tests among all tasks.") % contest.max_user_test_number) if task.max_user_test_number is not None: user_test_t = self.sql_session.query(func.count(UserTest.id))\ .filter(UserTest.task == task)\ .filter(UserTest.participation == participation)\ .scalar() if user_test_t >= task.max_user_test_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d tests on this task.") % task.max_user_test_number) except ValueError as error: self._send_error(self._("Too many tests!"), str(error)) return # Enforce minimum time between user_tests try: if contest.min_user_test_interval is not None: last_user_test_c = self.sql_session.query(UserTest)\ .join(UserTest.task)\ .filter(Task.contest == contest)\ .filter(UserTest.participation == participation)\ .order_by(UserTest.timestamp.desc())\ .first() if last_user_test_c is not None and \ self.timestamp - last_user_test_c.timestamp < \ contest.min_user_test_interval and \ not self.current_user.unrestricted: raise ValueError( self._("Among all tasks, you can test again " "after %d seconds from last test.") % contest.min_user_test_interval.total_seconds()) # We get the last user_test even if we may not need it # for min_user_test_interval because we may need it later, # in case this is a ALLOW_PARTIAL_SUBMISSION task. last_user_test_t = self.sql_session.query(UserTest)\ .filter(UserTest.participation == participation)\ .filter(UserTest.task == task)\ .order_by(UserTest.timestamp.desc())\ .first() if task.min_user_test_interval is not None: if last_user_test_t is not None and \ self.timestamp - last_user_test_t.timestamp < \ task.min_user_test_interval and \ not self.current_user.unrestricted: raise ValueError( self._("For this task, you can test again " "after %d seconds from last test.") % task.min_user_test_interval.total_seconds()) except ValueError as error: self._send_error(self._("Tests too frequent!"), str(error)) return # Required files from the user. required = set([sfe.filename for sfe in task.submission_format] + task_type.get_user_managers(task.submission_format) + ["input"]) # Ensure that the user did not submit multiple files with the # same name. if any( len(filename) != 1 for filename in itervalues(self.request.files)): self._send_error(self._("Invalid test format!"), self._("Please select the correct files.")) return # If the user submitted an archive, extract it and use content # as request.files. But only valid for "output only" (i.e., # not for submissions requiring a programming language # identification). if len(self.request.files) == 1 and \ next(iterkeys(self.request.files)) == "submission": if any(filename.endswith(".%l") for filename in required): self._send_error(self._("Invalid test format!"), self._("Please select the correct files."), task) return archive_data = self.request.files["submission"][0] del self.request.files["submission"] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: self._send_error( self._("Invalid archive format!"), self._("The submitted archive could not be opened.")) return # Extract the archive. unpacked_dir = archive.unpack() for name in archive.namelist(): filename = os.path.basename(name) body = open(os.path.join(unpacked_dir, filename), "r").read() self.request.files[filename] = [{ 'filename': filename, 'body': body }] archive.cleanup() # This ensure that the user sent one file for every name in # submission format and no more. Less is acceptable if task # type says so. provided = set(iterkeys(self.request.files)) if not (required == provided or (task_type.ALLOW_PARTIAL_SUBMISSION and required.issuperset(provided))): self._send_error(self._("Invalid test format!"), self._("Please select the correct files.")) return # 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 iteritems(self.request.files): 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) # If we allow partial submissions, implicitly we recover the # non-submitted files from the previous user test. And put them # in file_digests (i.e. like they have already been sent to FS). file_digests = {} if task_type.ALLOW_PARTIAL_SUBMISSION and \ last_user_test_t is not None and \ (submission_lang is None or submission_lang == last_user_test_t.language): submission_lang = last_user_test_t.language for filename in required.difference(provided): if filename in last_user_test_t.files: file_digests[filename] = \ last_user_test_t.files[filename].digest # 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: self._send_error(self._("Invalid test!"), error) return # Check if submitted files are small enough. if any([ len(f[1]) > config.max_submission_length for n, f in iteritems(files) if n != "input" ]): self._send_error( self._("Test too big!"), self._("Each source file must be at most %d bytes long.") % config.max_submission_length) return if len(files["input"][1]) > config.max_input_length: self._send_error( self._("Input too big!"), self._("The input file must be at most %d bytes long.") % config.max_input_length) return # 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... try: for filename in files: digest = self.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 # In case of error, the server aborts the submission except Exception as error: logger.error("Storage failed! %s", error) self._send_error(self._("Test storage failed!"), self._("Please try again.")) return # 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.service.evaluation_service.new_user_test( user_test_id=user_test.id) self.service.add_notification( participation.user.username, self.timestamp, self._("Test received"), self._("Your test has been received " "and is currently being executed."), NOTIFICATION_SUCCESS) # 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.redirect( self.contest_url(*self.fallback_page, user_test_id=encrypt_number(user_test.id), **self.fallback_args))
def post(self, task_name): participation = self.current_user try: task = self.contest.get_task(task_name) except KeyError: raise tornado.web.HTTPError(404) # Alias for easy access contest = self.contest # Enforce maximum number of submissions try: if contest.max_submission_number is not None: submission_c = self.sql_session\ .query(func.count(Submission.id))\ .join(Submission.task)\ .filter(Task.contest == contest)\ .filter(Submission.participation == participation)\ .scalar() if submission_c >= contest.max_submission_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d submissions among all tasks.") % contest.max_submission_number) if task.max_submission_number is not None: submission_t = self.sql_session\ .query(func.count(Submission.id))\ .filter(Submission.task == task)\ .filter(Submission.participation == participation)\ .scalar() if submission_t >= task.max_submission_number and \ not self.current_user.unrestricted: raise ValueError( self._("You have reached the maximum limit of " "at most %d submissions on this task.") % task.max_submission_number) except ValueError as error: self._send_error( self._("Too many submissions!"), error.message, task) return # Enforce minimum time between submissions try: if contest.min_submission_interval is not None: last_submission_c = self.sql_session.query(Submission)\ .join(Submission.task)\ .filter(Task.contest == contest)\ .filter(Submission.participation == participation)\ .order_by(Submission.timestamp.desc())\ .first() if last_submission_c is not None and \ self.timestamp - last_submission_c.timestamp < \ contest.min_submission_interval and \ not self.current_user.unrestricted: raise ValueError( self._("Among all tasks, you can submit again " "after %d seconds from last submission.") % contest.min_submission_interval.total_seconds()) # We get the last submission even if we may not need it # for min_submission_interval because we may need it later, # in case this is a ALLOW_PARTIAL_SUBMISSION task. last_submission_t = self.sql_session.query(Submission)\ .filter(Submission.task == task)\ .filter(Submission.participation == participation)\ .order_by(Submission.timestamp.desc())\ .first() if task.min_submission_interval is not None: if last_submission_t is not None and \ self.timestamp - last_submission_t.timestamp < \ task.min_submission_interval and \ not self.current_user.unrestricted: raise ValueError( self._("For this task, you can submit again " "after %d seconds from last submission.") % task.min_submission_interval.total_seconds()) except ValueError as error: self._send_error( self._("Submissions too frequent!"), error.message, task) return # Required files from the user. required = set([sfe.filename for sfe in task.submission_format]) # 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()): self._send_error( self._("Invalid submission format!"), self._("Please select the correct files."), task) return # If the user submitted an archive, extract it and use content # as request.files. But only valid for "output only" (i.e., # not for submissions requiring a programming language # identification). if len(self.request.files) == 1 and \ self.request.files.keys()[0] == "submission": if any(filename.endswith(".%l") for filename in required): self._send_error( self._("Invalid submission format!"), self._("Please select the correct files."), task) return archive_data = self.request.files["submission"][0] del self.request.files["submission"] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: self._send_error( self._("Invalid archive format!"), self._("The submitted archive could not be opened."), task) return # Extract the archive. unpacked_dir = archive.unpack() for name in archive.namelist(): filename = os.path.basename(name) body = open(os.path.join(unpacked_dir, filename), "r").read() self.request.files[filename] = [{ 'filename': filename, 'body': body }] archive.cleanup() # This ensure that the user sent one file for every name in # submission format and no more. Less is acceptable if task # type says so. task_type = get_task_type(dataset=task.active_dataset) provided = set(self.request.files.keys()) if not (required == provided or (task_type.ALLOW_PARTIAL_SUBMISSION and required.issuperset(provided))): self._send_error( self._("Invalid submission format!"), self._("Please select the correct files."), task) return # 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) # If we allow partial submissions, we implicitly recover the # non-submitted files from the previous submission (if it has # the same programming language of the current one), and put # them in file_digests (since they are already in FS). file_digests = {} if task_type.ALLOW_PARTIAL_SUBMISSION and \ last_submission_t is not None and \ (submission_lang is None or submission_lang == last_submission_t.language): submission_lang = last_submission_t.language for filename in required.difference(provided): if filename in last_submission_t.files: file_digests[filename] = \ last_submission_t.files[filename].digest # 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 submission language.") elif submission_lang not in contest.languages: error = self._("Language %s not allowed in this contest.") \ % submission_lang if error is not None: self._send_error(self._("Invalid submission!"), error, task) return # Check if submitted files are small enough. if any([len(f[1]) > config.max_submission_length for f in files.values()]): self._send_error( self._("Submission too big!"), self._("Each source file must be at most %d bytes long.") % config.max_submission_length, task) return # All checks done, submission accepted. # Attempt to store the submission locally to be able to # recover a failure. if config.submit_local_copy: try: path = os.path.join( config.submit_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.warning("Submission local copy failed.", exc_info=True) # We now have to send all the files to the destination... try: for filename in files: digest = self.application.service.file_cacher.put_file_content( files[filename][1], "Submission file %s sent by %s at %d." % ( filename, participation.user.username, make_timestamp(self.timestamp))) file_digests[filename] = digest # In case of error, the server aborts the submission except Exception as error: logger.error("Storage failed! %s", error) self._send_error( self._("Submission storage failed!"), self._("Please try again."), task) return # All the files are stored, ready to submit! logger.info("All files stored for submission sent by %s", participation.user.username) submission = Submission(self.timestamp, submission_lang, task=task, participation=participation) for filename, digest in file_digests.items(): self.sql_session.add(File(filename, digest, submission=submission)) self.sql_session.add(submission) self.sql_session.commit() self.application.service.evaluation_service.new_submission( submission_id=submission.id) self.application.service.add_notification( participation.user.username, self.timestamp, self._("Submission received"), self._("Your submission has been received " "and is currently being evaluated."), NOTIFICATION_SUCCESS) # The argument (encripted submission id) is not used by CWS # (nor it discloses information to the user), but it is useful # for automatic testing to obtain the submission id). self.redirect("/tasks/%s/submissions?%s" % ( quote(task.name, safe=''), encrypt_number(submission.id)))