AddSubmission.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2015-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. """Utility to submit a solution for a user.
  18. """
  19. import argparse
  20. import logging
  21. import sys
  22. from cms import utf8_decoder, ServiceCoord
  23. from cms.db import File, Participation, SessionGen, Submission, Task, User, \
  24. ask_for_contest
  25. from cms.db.filecacher import FileCacher
  26. from cms.grading.languagemanager import filename_to_language
  27. from cms.io import RemoteServiceClient
  28. from cmscommon.datetime import make_datetime
  29. logger = logging.getLogger(__name__)
  30. def maybe_send_notification(submission_id):
  31. """Non-blocking attempt to notify a running ES of the submission"""
  32. rs = RemoteServiceClient(ServiceCoord("EvaluationService", 0))
  33. rs.connect()
  34. rs.new_submission(submission_id=submission_id)
  35. rs.disconnect()
  36. def language_from_submitted_files(files):
  37. """Return the language inferred from the submitted files.
  38. files ({str: str}): dictionary mapping the expected filename to a path in
  39. the file system.
  40. return (Language|None): the language inferred from the files.
  41. raise (ValueError): if different files point to different languages, or if
  42. it is impossible to extract the language from a file when it should be.
  43. """
  44. # TODO: deduplicate with the code in SubmitHandler.
  45. language = None
  46. for filename in files.keys():
  47. this_language = filename_to_language(files[filename])
  48. if this_language is None and ".%l" in filename:
  49. raise ValueError(
  50. "Cannot recognize language for file `%s'." % filename)
  51. if language is None:
  52. language = this_language
  53. elif this_language is not None and language != this_language:
  54. raise ValueError("Mixed-language submission detected.")
  55. return language
  56. def add_submission(contest_id, username, task_name, timestamp, files):
  57. file_cacher = FileCacher()
  58. with SessionGen() as session:
  59. participation = session.query(Participation)\
  60. .join(Participation.user)\
  61. .filter(Participation.contest_id == contest_id)\
  62. .filter(User.username == username)\
  63. .first()
  64. if participation is None:
  65. logging.critical("User `%s' does not exists or "
  66. "does not participate in the contest.", username)
  67. return False
  68. task = session.query(Task)\
  69. .filter(Task.contest_id == contest_id)\
  70. .filter(Task.name == task_name)\
  71. .first()
  72. if task is None:
  73. logging.critical("Unable to find task `%s'.", task_name)
  74. return False
  75. elements = set(task.submission_format)
  76. for file_ in files:
  77. if file_ not in elements:
  78. logging.critical("File `%s' is not in the submission format "
  79. "for the task.", file_)
  80. return False
  81. if any(element not in files for element in elements):
  82. logger.warning("Not all files from the submission format were "
  83. "provided.")
  84. # files is now a subset of elements.
  85. # We ensure we can infer a language if the task requires it.
  86. language = None
  87. need_lang = any(element.find(".%l") != -1 for element in elements)
  88. if need_lang:
  89. try:
  90. language = language_from_submitted_files(files)
  91. except ValueError as e:
  92. logger.critical(e)
  93. return False
  94. if language is None:
  95. # This might happen in case not all files were provided.
  96. logger.critical("Unable to infer language from submission.")
  97. return False
  98. language_name = None if language is None else language.name
  99. # Store all files from the arguments, and obtain their digests..
  100. file_digests = {}
  101. try:
  102. for file_ in files:
  103. digest = file_cacher.put_file_from_path(
  104. files[file_],
  105. "Submission file %s sent by %s at %d."
  106. % (file_, username, timestamp))
  107. file_digests[file_] = digest
  108. except Exception as e:
  109. logger.critical("Error while storing submission's file: %s.", e)
  110. return False
  111. # Create objects in the DB.
  112. submission = Submission(make_datetime(timestamp), language_name,
  113. participation=participation, task=task)
  114. for filename, digest in file_digests.items():
  115. session.add(File(filename, digest, submission=submission))
  116. session.add(submission)
  117. session.commit()
  118. maybe_send_notification(submission.id)
  119. return True
  120. def main():
  121. """Parse arguments and launch process.
  122. return (int): exit code of the program.
  123. """
  124. parser = argparse.ArgumentParser(
  125. description="Adds a submission to a contest in CMS.")
  126. parser.add_argument("-c", "--contest-id", action="store", type=int,
  127. help="id of contest where to add the submission")
  128. parser.add_argument("-f", "--file", action="append", type=utf8_decoder,
  129. help="in the form <name>:<file>, where name is the "
  130. "name as required by CMS, and file is the name of "
  131. "the file in the filesystem - may be specified "
  132. "multiple times", required=True)
  133. parser.add_argument("username", action="store", type=utf8_decoder,
  134. help="user doing the submission")
  135. parser.add_argument("task_name", action="store", type=utf8_decoder,
  136. help="name of task the submission is for")
  137. parser.add_argument("-t", "--timestamp", action="store", type=int,
  138. help="timestamp of the submission in seconds from "
  139. "epoch, e.g. `date +%%s` (now if not set)")
  140. args = parser.parse_args()
  141. if args.contest_id is None:
  142. args.contest_id = ask_for_contest()
  143. if args.timestamp is None:
  144. import time
  145. args.timestamp = time.time()
  146. split_files = [file_.split(":", 1) for file_ in args.file]
  147. if any(len(file_) != 2 for file_ in split_files):
  148. parser.error("Invalid value for the file argument: format is "
  149. "<name>:<file>.")
  150. return 1
  151. files = {}
  152. for name, filename in split_files:
  153. if name in files:
  154. parser.error("Duplicate assignment for file `%s'." % name)
  155. return 1
  156. files[name] = filename
  157. success = add_submission(contest_id=args.contest_id,
  158. username=args.username,
  159. task_name=args.task_name,
  160. timestamp=args.timestamp,
  161. files=files)
  162. return 0 if success is True else 1
  163. if __name__ == "__main__":
  164. sys.exit(main())