def extract_files_from_archive(data): """Return the files contained in the given archive. Given the binary data of an archive in any of the formats supported by patool, extract its contents and return them in our format. The archive's contents must be a valid directory structure (i.e., its contents cannot have conflicting/duplicated paths) but the structure will be ignored and the files will be returned with their basename. data (bytes): the raw contents of the archive. return ([ReceivedFile]): the files contained in the archive, with their filename filled in but their codename set to None. raise (InvalidArchive): if the data doesn't seem to encode an archive, its contents are invalid, or other issues. """ archive = Archive.from_raw_data(data) if archive is None: raise InvalidArchive() result = list() try: archive.unpack() for name in archive.namelist(): with archive.read(name) as f: result.append( ReceivedFile(None, os.path.basename(name), f.read())) # When dropping py2, replace EnvironmentError by OSError. except (PatoolError, EnvironmentError): raise InvalidArchive() finally: archive.cleanup() return result
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.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 submission_handler(self): if local.data['action'] == 'list': task = local.session.query(Task)\ .filter(Task.name == local.data['task_name']).first() if task is None: return 'Not found' if local.user is None: return 'Unauthorized' subs = local.session.query(Submission)\ .filter(Submission.participation_id == local.participation.id)\ .filter(Submission.task_id == task.id)\ .order_by(desc(Submission.timestamp)).all() submissions = [] for s in subs: submission = dict() submission['id'] = s.id submission['task_id'] = s.task_id submission['timestamp'] = make_timestamp(s.timestamp) submission['files'] = [] for name, f in s.files.iteritems(): fi = dict() if s.language is None: fi['name'] = name else: fi['name'] = name.replace('%l', s.language) fi['digest'] = f.digest submission['files'].append(fi) result = s.get_result() for i in ['compilation_outcome', 'evaluation_outcome']: submission[i] = getattr(result, i, None) if result is not None and result.score is not None: submission['score'] = round(result.score, 2) submissions.append(submission) local.resp['submissions'] = submissions elif local.data['action'] == 'details': s = local.session.query(Submission)\ .filter(Submission.id == local.data['id']).first() if s is None: return 'Not found' if local.user is None or s.participation_id != local.participation.id: return 'Unauthorized' submission = dict() submission['id'] = s.id submission['task_id'] = s.task_id submission['timestamp'] = make_timestamp(s.timestamp) submission['language'] = s.language submission['files'] = [] for name, f in s.files.iteritems(): fi = dict() if s.language is None: fi['name'] = name else: fi['name'] = name.replace('%l', s.language) fi['digest'] = f.digest submission['files'].append(fi) result = s.get_result() for i in ['compilation_outcome', 'evaluation_outcome', 'compilation_stdout', 'compilation_stderr', 'compilation_time', 'compilation_memory']: submission[i] = getattr(result, i, None) if result is not None and result.score is not None: submission['score'] = round(result.score, 2) if result is not None and result.score_details is not None: tmp = json.loads(result.score_details) if len(tmp) > 0 and 'text' in tmp[0]: subt = dict() subt['testcases'] = tmp subt['score'] = submission['score'] subt['max_score'] = 100 submission['score_details'] = [subt] else: submission['score_details'] = tmp for subtask in submission['score_details']: for testcase in subtask['testcases']: data = json.loads(testcase['text']) testcase['text'] = data[0] % tuple(data[1:]) else: submission['score_details'] = None local.resp = submission elif local.data['action'] == 'new': if local.user is None: return 'Unauthorized' lastsub = local.session.query(Submission)\ .filter(Submission.participation_id == local.participation.id)\ .order_by(desc(Submission.timestamp)).first() if lastsub is not None and \ make_datetime() - lastsub.timestamp < timedelta(seconds=20): return 'Too frequent submissions!' try: task = local.session.query(Task)\ .join(SocialTask)\ .filter(Task.name == local.data['task_name'])\ .filter(SocialTask.access_level >= local.access_level).first() except KeyError: return 'Not found' def decode_file(f): f['data'] = f['data'].split(',')[-1] f['body'] = b64decode(f['data']) del f['data'] return f if len(local.data['files']) == 1 and \ 'submission' in local.data['files']: archive_data = decode_file(local.data['files']['submission']) del local.data['files']['submission'] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: return 'Invalid archive!' # 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() local.data['files'][filename] = { 'filename': filename, 'body': body } files_sent = local.data['files'] archive.cleanup() else: files_sent = \ dict([(k, decode_file(v)) for k, v in local.data['files'].iteritems()]) # TODO: implement partial submissions (?) # Detect language files = [] sub_lang = None for sfe in task.submission_format: f = files_sent.get(sfe.filename) if f is None: return 'Some files are missing!' if len(f['body']) > config.get("core", "max_submission_length"): return 'The files you sent are too big!' f['name'] = sfe.filename files.append(f) if sfe.filename.endswith('.%l'): language = None for ext, l in SOURCE_EXT_TO_LANGUAGE_MAP.iteritems(): if f['filename'].endswith(ext): language = l if language is None: return 'The language of the files you sent is not ' + \ 'recognized!' elif sub_lang is not None and sub_lang != language: return 'The files you sent are in different languages!' else: sub_lang = language # Add the submission timestamp = make_datetime() submission = Submission(timestamp, sub_lang, participation=local.participation, task=task) for f in files: digest = self.file_cacher.put_file_content( f['body'], 'Submission file %s sent by %s at %d.' % ( f['name'], local.user.username, make_timestamp(timestamp))) local.session.add(File(f['name'], digest, submission=submission)) local.session.add(submission) local.session.commit() # Notify ES self.evaluation_service.new_submission( submission_id=submission.id ) # Answer with submission data local.resp['id'] = submission.id local.resp['task_id'] = submission.task_id local.resp['timestamp'] = make_timestamp(submission.timestamp) local.resp['compilation_outcome'] = None local.resp['evaluation_outcome'] = None local.resp['score'] = None local.resp['files'] = [] for name, f in submission.files.iteritems(): fi = dict() if submission.language is None: fi['name'] = name else: fi['name'] = name.replace('%l', submission.language) fi['digest'] = f.digest local.resp['files'].append(fi) else: return 'Bad request'
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 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)))
def submission_handler(self): if local.data["action"] == "list": task = local.session.query(Task).filter(Task.name == local.data["task_name"]).first() if task is None: return "Not found" if local.user is None: return "Unauthorized" subs = ( local.session.query(Submission) .filter(Submission.user_id == local.user.id) .filter(Submission.task_id == task.id) .order_by(desc(Submission.timestamp)) .all() ) submissions = [] for s in subs: submission = dict() submission["id"] = s.id submission["task_id"] = s.task_id submission["timestamp"] = make_timestamp(s.timestamp) submission["files"] = [] for name, f in s.files.iteritems(): fi = dict() if s.language is None: fi["name"] = name else: fi["name"] = name.replace("%l", s.language) fi["digest"] = f.digest submission["files"].append(fi) result = s.get_result() for i in ["compilation_outcome", "evaluation_outcome"]: submission[i] = getattr(result, i, None) if result is not None and result.score is not None: submission["score"] = round(result.score, 2) submissions.append(submission) local.resp["submissions"] = submissions elif local.data["action"] == "details": s = local.session.query(Submission).filter(Submission.id == local.data["id"]).first() if s is None: return "Not found" if local.user is None or s.user_id != local.user.id: return "Unauthorized" submission = dict() submission["id"] = s.id submission["task_id"] = s.task_id submission["timestamp"] = make_timestamp(s.timestamp) submission["language"] = s.language submission["files"] = [] for name, f in s.files.iteritems(): fi = dict() if s.language is None: fi["name"] = name else: fi["name"] = name.replace("%l", s.language) fi["digest"] = f.digest submission["files"].append(fi) result = s.get_result() for i in [ "compilation_outcome", "evaluation_outcome", "compilation_stdout", "compilation_stderr", "compilation_time", "compilation_memory", ]: submission[i] = getattr(result, i, None) if result is not None and result.score is not None: submission["score"] = round(result.score, 2) if result is not None and result.score_details is not None: tmp = json.loads(result.score_details) if len(tmp) > 0 and "text" in tmp[0]: subt = dict() subt["testcases"] = tmp subt["score"] = submission["score"] subt["max_score"] = 100 submission["score_details"] = [subt] else: submission["score_details"] = tmp for subtask in submission["score_details"]: for testcase in subtask["testcases"]: data = json.loads(testcase["text"]) testcase["text"] = data[0] % tuple(data[1:]) else: submission["score_details"] = None local.resp = submission elif local.data["action"] == "new": if local.user is None: return "Unauthorized" lastsub = ( local.session.query(Submission) .filter(Submission.user_id == local.user.id) .order_by(desc(Submission.timestamp)) .first() ) if lastsub is not None and make_datetime() - lastsub.timestamp < timedelta(seconds=20): return "Too frequent submissions!" try: task = ( local.session.query(Task) .filter(Task.name == local.data["task_name"]) .filter(Task.access_level >= local.access_level) .first() ) except KeyError: return "Not found" def decode_file(f): f["data"] = f["data"].split(",")[-1] f["body"] = b64decode(f["data"]) del f["data"] return f if len(local.data["files"]) == 1 and "submission" in local.data["files"]: archive_data = decode_file(local.data["files"]["submission"]) del local.data["files"]["submission"] # Create the archive. archive = Archive.from_raw_data(archive_data["body"]) if archive is None: return "Invalid archive!" # 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}] files_sent = dict([(i["filename"], i) for i in archive_contents]) archive.cleanup() else: files_sent = dict([(k, decode_file(v)) for k, v in local.data["files"].iteritems()]) # TODO: implement partial submissions (?) # Detect language files = [] sub_lang = None for sfe in task.submission_format: f = files_sent.get(sfe.filename) if f is None: return "Some files are missing!" if len(f["body"]) > config.max_submission_length: return "The files you sent are too big!" f["name"] = sfe.filename files.append(f) if sfe.filename.endswith(".%l"): language = None for ext, l in SOURCE_EXT_TO_LANGUAGE_MAP.iteritems(): if f["filename"].endswith(ext): language = l if language is None: return "The language of the files you sent is not " + "recognized!" elif sub_lang is not None and sub_lang != language: return "The files you sent are in different languages!" else: sub_lang = language # Add the submission timestamp = make_datetime() submission = Submission(timestamp, sub_lang, user=local.user, task=task) for f in files: digest = self.file_cacher.put_file_content( f["body"], "Submission file %s sent by %s at %d." % (f["name"], local.user.username, make_timestamp(timestamp)), ) local.session.add(File(f["name"], digest, submission=submission)) local.session.add(submission) local.session.commit() # Notify ES self.evaluation_service.new_submission(submission_id=submission.id) # Answer with submission data local.resp["id"] = submission.id local.resp["task_id"] = submission.task_id local.resp["timestamp"] = make_timestamp(submission.timestamp) local.resp["compilation_outcome"] = None local.resp["evaluation_outcome"] = None local.resp["score"] = None local.resp["files"] = [] for name, f in submission.files.iteritems(): fi = dict() if submission.language is None: fi["name"] = name else: fi["name"] = name.replace("%l", submission.language) fi["digest"] = f.digest local.resp["files"].append(fi) else: return "Bad request"
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)))