SpoolExporter.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2010-2012 Giovanni Mascellani <mascellani@poisson.phc.unipi.it>
  4. # Copyright © 2010-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
  5. # Copyright © 2010-2012 Matteo Boscariol <boscarim@hotmail.com>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. # FIXME: update to latest database version 15
  20. """This service creates a tree structure "similar" to the one used in
  21. Italian IOI repository for storing the results of a contest.
  22. """
  23. # We enable monkey patching to make many libraries gevent-friendly
  24. # (for instance, urllib3, used by requests)
  25. import gevent.monkey
  26. gevent.monkey.patch_all() # noqa
  27. import argparse
  28. import logging
  29. import os
  30. import sys
  31. import time
  32. from sqlalchemy import not_
  33. from cms import utf8_decoder
  34. from cms.db import SessionGen, Contest, ask_for_contest, Submission, \
  35. Participation, get_submissions
  36. from cms.db.filecacher import FileCacher
  37. from cms.grading import languagemanager
  38. from cms.grading.scoring import task_score
  39. logger = logging.getLogger(__name__)
  40. # TODO: review this file to avoid print.
  41. class SpoolExporter:
  42. """This service creates a tree structure "similar" to the one used
  43. in Italian IOI repository for storing the results of a contest.
  44. """
  45. def __init__(self, contest_id, spool_dir):
  46. self.contest_id = contest_id
  47. self.spool_dir = spool_dir
  48. self.upload_dir = os.path.join(self.spool_dir, "upload")
  49. self.contest = None
  50. self.submissions = None
  51. self.file_cacher = FileCacher()
  52. def run(self):
  53. """Interface to make the class do its job."""
  54. return self.do_export()
  55. def do_export(self):
  56. """Run the actual export code.
  57. """
  58. logger.operation = "exporting contest %s" % self.contest_id
  59. logger.info("Starting export.")
  60. logger.info("Creating dir structure.")
  61. try:
  62. os.mkdir(self.spool_dir)
  63. except OSError:
  64. logger.critical("The specified directory already exists, "
  65. "I won't overwrite it.")
  66. return False
  67. os.mkdir(self.upload_dir)
  68. with SessionGen() as session:
  69. self.contest = Contest.get_from_id(self.contest_id, session)
  70. self.submissions = \
  71. get_submissions(session, contest_id=self.contest_id) \
  72. .filter(not_(Participation.hidden)) \
  73. .order_by(Submission.timestamp).all()
  74. # Creating users' directory.
  75. for participation in self.contest.participations:
  76. if not participation.hidden:
  77. os.mkdir(os.path.join(
  78. self.upload_dir, participation.user.username))
  79. try:
  80. self.export_submissions()
  81. self.export_ranking()
  82. except Exception:
  83. logger.critical("Generic error.", exc_info=True)
  84. return False
  85. logger.info("Export finished.")
  86. logger.operation = ""
  87. return True
  88. def export_submissions(self):
  89. """Export submissions' source files.
  90. """
  91. logger.info("Exporting submissions.")
  92. with open(os.path.join(self.spool_dir, "queue"),
  93. "wt", encoding="utf-8") as queue_file:
  94. for submission in sorted(self.submissions,
  95. key=lambda x: x.timestamp):
  96. logger.info("Exporting submission %s.", submission.id)
  97. username = submission.participation.user.username
  98. task = submission.task.name
  99. timestamp = time.mktime(submission.timestamp.timetuple())
  100. # Get source files to the spool directory.
  101. ext = languagemanager.get_language(submission.language)\
  102. .source_extension
  103. submission_dir = os.path.join(
  104. self.upload_dir, username,
  105. "%s.%d.%s" % (task, timestamp, ext))
  106. os.mkdir(submission_dir)
  107. for filename, file_ in submission.files.items():
  108. self.file_cacher.get_file_to_path(
  109. file_.digest,
  110. os.path.join(submission_dir,
  111. filename.replace(".%l", ext)))
  112. last_submission_dir = os.path.join(
  113. self.upload_dir, username, "%s.%s" % (task, ext))
  114. try:
  115. os.unlink(last_submission_dir)
  116. except OSError:
  117. pass
  118. os.symlink(os.path.basename(submission_dir),
  119. last_submission_dir)
  120. print("./upload/%s/%s.%d.%s" % (username, task, timestamp, ext),
  121. file=queue_file)
  122. # Write results file for the submission.
  123. active_dataset = submission.task.active_dataset
  124. result = submission.get_result(active_dataset)
  125. if result.evaluated():
  126. with open(os.path.join(self.spool_dir,
  127. "%d.%s.%s.%s.res"
  128. % (timestamp, username, task, ext)),
  129. "wt", encoding="utf-8") as res_file, \
  130. open(os.path.join(self.spool_dir,
  131. "%s.%s.%s.res"
  132. % (username, task, ext)),
  133. "wt", encoding="utf-8") as res2_file:
  134. total = 0.0
  135. for evaluation in result.evaluations:
  136. outcome = float(evaluation.outcome)
  137. total += outcome
  138. line = (
  139. "Executing on file with codename '%s' %s (%.4f)"
  140. % (evaluation.testcase.codename,
  141. evaluation.text, outcome))
  142. print(line, file=res_file)
  143. print(line, file=res2_file)
  144. line = "Score: %.6f" % total
  145. print(line, file=res_file)
  146. print(line, file=res2_file)
  147. print("", file=queue_file)
  148. def export_ranking(self):
  149. """Exports the ranking in csv and txt (human-readable) form.
  150. """
  151. logger.info("Exporting ranking.")
  152. # Create the structure to store the scores.
  153. scores = dict((participation.user.username, 0.0)
  154. for participation in self.contest.participations
  155. if not participation.hidden)
  156. task_scores = dict(
  157. (task.id, dict((participation.user.username, 0.0)
  158. for participation in self.contest.participations
  159. if not participation.hidden))
  160. for task in self.contest.tasks)
  161. is_partial = False
  162. for task in self.contest.tasks:
  163. for participation in self.contest.participations:
  164. if participation.hidden:
  165. continue
  166. score, partial = task_score(participation, task)
  167. is_partial = is_partial or partial
  168. task_scores[task.id][participation.user.username] = score
  169. scores[participation.user.username] += score
  170. if is_partial:
  171. logger.warning("Some of the scores are not definitive.")
  172. sorted_usernames = sorted(scores.keys(),
  173. key=lambda username: (scores[username],
  174. username),
  175. reverse=True)
  176. sorted_tasks = sorted(self.contest.tasks,
  177. key=lambda task: task.num)
  178. with open(os.path.join(self.spool_dir, "ranking.txt"),
  179. "wt", encoding="utf-8") as ranking_file, \
  180. open(os.path.join(self.spool_dir, "ranking.csv"),
  181. "wt", encoding="utf-8") as ranking_csv:
  182. # Write rankings' header.
  183. n_tasks = len(sorted_tasks)
  184. print("Final Ranking of Contest `%s'" %
  185. self.contest.description, file=ranking_file)
  186. points_line = " %10s" * n_tasks
  187. csv_points_line = ",%s" * n_tasks
  188. print(("%20s %10s" % ("User", "Total")) +
  189. (points_line % tuple([t.name for t in sorted_tasks])),
  190. file=ranking_file)
  191. print(("%s,%s" % ("user", "total")) +
  192. (csv_points_line % tuple([t.name for t in sorted_tasks])),
  193. file=ranking_csv)
  194. # Write rankings' content.
  195. points_line = " %10.3f" * n_tasks
  196. csv_points_line = ",%.6f" * n_tasks
  197. for username in sorted_usernames:
  198. user_scores = [task_scores[task.id][username]
  199. for task in sorted_tasks]
  200. print(("%20s %10.3f" % (username, scores[username])) +
  201. (points_line % tuple(user_scores)),
  202. file=ranking_file)
  203. print(("%s,%.6f" % (username, scores[username])) +
  204. (csv_points_line % tuple(user_scores)),
  205. file=ranking_csv)
  206. def main():
  207. """Parse arguments and launch process.
  208. """
  209. parser = argparse.ArgumentParser(
  210. description="Exporter for the Italian repository for CMS.")
  211. parser.add_argument("-c", "--contest-id", action="store", type=int,
  212. help="id of contest to export")
  213. parser.add_argument("export_directory", action="store", type=utf8_decoder,
  214. help="target directory where to export")
  215. args = parser.parse_args()
  216. if args.contest_id is None:
  217. args.contest_id = ask_for_contest()
  218. exporter = SpoolExporter(contest_id=args.contest_id,
  219. spool_dir=args.export_directory)
  220. success = exporter.run()
  221. return 0 if success is True else 1
  222. if __name__ == "__main__":
  223. sys.exit(main())