importing-backup.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2010-2013 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. """Utility functions for importers"""
  20. import functools
  21. from cms.db import Contest, Dataset, Task
  22. __all__ = [
  23. "contest_from_db", "task_from_db",
  24. "update_contest", "update_task"
  25. ]
  26. class ImportDataError(Exception):
  27. pass
  28. def contest_from_db(contest_id, session):
  29. """Return the contest object with the given id
  30. contest_id (int|None): the id of the contest, or None to return None.
  31. session (Session): SQLAlchemy session to use.
  32. return (Contest|None): None if contest_id is None, or the contest.
  33. raise (ImportDataError): if there is no contest with the given id.
  34. """
  35. if contest_id is None:
  36. return None
  37. contest = Contest.get_from_id(contest_id, session)
  38. if contest is None:
  39. raise ImportDataError(
  40. "The specified contest (id %s) does not exist." % contest_id)
  41. return contest
  42. def task_from_db(task_name, session):
  43. """Return the task object with the given name
  44. task_name (string|None): the name of the task, or None to return None.
  45. session (Session): SQLAlchemy session to use.
  46. return (Task|None): None if task_name is None, or the task.
  47. raise (ImportDataError): if there is no task with the given name.
  48. """
  49. if task_name is None:
  50. return None
  51. task = session.query(Task).filter(Task.name == task_name).first()
  52. if task is None:
  53. raise ImportDataError(
  54. "The specified task (name %s) does not exist." % task_name)
  55. return task
  56. def _update_columns(old_object, new_object, spec=None):
  57. """Update the scalar columns of the object
  58. Update all non-relationship columns of old_object with the values in
  59. new_object, unless spec[attribute] is False.
  60. """
  61. assert type(old_object) == type(new_object)
  62. spec = spec if spec is not None else {}
  63. for prp in old_object._col_props:
  64. if spec.get(prp.class_attribute, True) is False:
  65. continue
  66. if hasattr(new_object, prp.key):
  67. setattr(old_object, prp.key, getattr(new_object, prp.key))
  68. def _update_object(old_object, new_object, spec=None, parent=None):
  69. """Update old_object with the values in new_object
  70. Update all columns with this strategy:
  71. - for non-relationship columns, use _update_columns (in particular, all
  72. columns are updated by default, unless spec[attribute] is false);
  73. - for relationship columns:
  74. - if the name is equal to parent, then it is ignored;
  75. - otherwise, it needs to be defined in spec; if spec is False, the
  76. column is ignored; if it is True, it is updated with the default
  77. strategy (see _update_list and _update_dict); otherwise if spec is
  78. a function, that function is used to update.
  79. old_object (Base): object to update.
  80. new_object (Base): object whose values will be used.
  81. spec (
  82. {sqlalchemy.orm.attributes.InstrumentedAttribute: boolean|function}
  83. |None): a dictionary mapping attributes to a boolean (if not
  84. updating or using the default strategy) or to an updating function,
  85. with signature fn(old_value, new_value, parent=None).
  86. parent (string|None): the name of the relationship in the parent object,
  87. which is ignored.
  88. """
  89. assert type(old_object) == type(new_object)
  90. spec = spec if spec is not None else {}
  91. # Update all scalar columns by default, unless spec says otherwise.
  92. _update_columns(old_object, new_object, spec)
  93. for prp in old_object._rel_props:
  94. # Don't update the parent relationship (works both for backref and
  95. # back_populates relationships).
  96. if parent is not None:
  97. if (prp.backref is not None and prp.backref[0] == parent) \
  98. or prp.back_populates == parent:
  99. continue
  100. # To avoid bugs when new relationships are introduced, we force the
  101. # caller to describe how to update all other relationships.
  102. assert prp.class_attribute in spec, (
  103. "Programming error: update specification not complete, "
  104. "missing relationship for %s.%s"
  105. % (prp.parent.class_, prp.class_attribute))
  106. # Spec is false, it means we should not update this relationship.
  107. if spec[prp.class_attribute] is False:
  108. continue
  109. old_value = getattr(old_object, prp.key)
  110. new_value = getattr(new_object, prp.key)
  111. if spec[prp.class_attribute] is True:
  112. # Spec is true, it means we update the relationship with the
  113. # default update method (for lists or dicts). Note that the
  114. # values cannot have other relationships than the parent's,
  115. # otherwise _update_object will complain it doesn't have the
  116. # spec for them.
  117. update_fn = functools.partial(_update_object, parent=prp.key)
  118. if isinstance(old_value, dict):
  119. _update_dict(old_value, new_value, update_fn)
  120. elif isinstance(old_value, list):
  121. _update_list(old_value, new_value, update_fn)
  122. else:
  123. raise AssertionError(
  124. "Programming error: unknown type of relationship for "
  125. "%s.%s." % (prp.parent.class_, prp.class_attribute))
  126. else:
  127. # Spec is not true, then it must be an update function, which
  128. # we duly apply.
  129. spec[prp.class_attribute](old_value, new_value, parent=prp.key)
  130. def _update_list(old_list, new_list, update_value_fn=None):
  131. """Update a SQLAlchemy relationship with type list
  132. Make old_list look like new_list, by:
  133. - up to the minimum length, calling update_value_fn on each element, to
  134. overwrite the values;
  135. - deleting additional entries in old_list, if they exist;
  136. - moving additional entries in new_list, if they exist.
  137. """
  138. if update_value_fn is None:
  139. update_value_fn = _update_object
  140. old_len = len(old_list)
  141. new_len = len(new_list)
  142. # Update common elements.
  143. for old_value, new_value in zip(old_list, new_list):
  144. update_value_fn(old_value, new_value)
  145. # Delete additional elements of old_list.
  146. del old_list[new_len:]
  147. # Move additional elements from new_list to old_list.
  148. for _ in range(old_len, new_len):
  149. # For some funny behavior of SQLAlchemy-instrumented collections when
  150. # copying values, that resulted in new objects being added to the
  151. # session.
  152. temp = new_list[old_len]
  153. del new_list[old_len]
  154. old_list.append(temp)
  155. def _update_dict(old_dict, new_dict, update_value_fn=None):
  156. """Update a SQLAlchemy relationship with type dict
  157. Make old_dict look like new_dict, by:
  158. - calling update_value_fn to overwrite the values of old_dict with a
  159. corresponding value in new_dict;
  160. - deleting all entries in old_dict whose key is not in new_dict;
  161. - moving all entries in new_dict whose key is not in old_dict.
  162. """
  163. if update_value_fn is None:
  164. update_value_fn = _update_object
  165. for key in set(old_dict.keys()) | set(new_dict.keys()):
  166. if key in new_dict:
  167. if key not in old_dict:
  168. # Move the object from new_dict to old_dict. For some funny
  169. # behavior of SQLAlchemy-instrumented collections when
  170. # copying values, that resulted in new objects being added
  171. # to the session.
  172. temp = new_dict[key]
  173. del new_dict[key]
  174. old_dict[key] = temp
  175. else:
  176. # Update the old value with the new value.
  177. update_value_fn(old_dict[key], new_dict[key])
  178. else:
  179. # Delete the old value if no new value for that key.
  180. del old_dict[key]
  181. def _update_list_with_key(old_list, new_list, key,
  182. preserve_old=False, update_value_fn=None):
  183. """Update a SQLAlchemy list-relationship, using key for identity
  184. Make old_list look like new_list, in a similar way to _update_dict, as
  185. if the list was a dictionary with key computed using the key function.
  186. If preserve_old is true, elements in old_list with a key not present in
  187. new_list will be preserved.
  188. """
  189. if update_value_fn is None:
  190. update_value_fn = _update_object
  191. old_dict = dict((key(v), v) for v in old_list)
  192. new_dict = dict((key(v), v) for v in new_list)
  193. for k in set(old_dict.keys()) | set(new_dict.keys()):
  194. if k in new_dict:
  195. if k not in old_dict:
  196. # Add new value to the old dictionary.
  197. temp = new_dict[k]
  198. new_list.remove(temp)
  199. old_list.append(temp)
  200. else:
  201. # Update the value in old_dict with the new value.
  202. update_value_fn(old_dict[k], new_dict[k])
  203. elif not preserve_old:
  204. # Remove the old value not anymore present.
  205. old_list.remove(old_dict[k])
  206. def update_dataset(old_dataset, new_dataset, parent=None):
  207. """Update old_dataset with information from new_dataset"""
  208. _update_object(old_dataset, new_dataset, {
  209. # Since we know it, hardcode to ignore the parent relationship.
  210. Dataset.task: False,
  211. # Relationships to update (all others).
  212. Dataset.managers: True,
  213. Dataset.testcases: True,
  214. }, parent=parent)
  215. def update_task(old_task, new_task, parent=None, get_statements=True):
  216. """Update old_task with information from new_task"""
  217. def update_datasets_fn(o, n, parent=None):
  218. _update_list_with_key(
  219. o, n, key= d.description, preserve_old=True,
  220. update_value_fn=functools.partial(update_dataset, parent=parent))
  221. _update_object(old_task, new_task, {
  222. # Since we know it, hardcode to ignore the parent relationship.
  223. Task.contest: False,
  224. # Relationships not to update because not provided by the loader.
  225. Task.active_dataset: False,
  226. Task.submissions: False,
  227. Task.user_tests: False,
  228. # Relationships to update.
  229. Task.statements: get_statements,
  230. Task.datasets: update_datasets_fn,
  231. Task.attachments: True,
  232. # Scalar columns exceptions.
  233. Task.num: False,
  234. Task.primary_statements: get_statements,
  235. }, parent=parent)
  236. def update_contest(old_contest, new_contest, parent=None):
  237. """Update old_contest with information from new_contest"""
  238. _update_object(old_contest, new_contest, {
  239. # Announcements are not provided by the loader, we should keep
  240. # those we have.
  241. Contest.announcements: False,
  242. # Tasks and participations are top level objects for the loader, so
  243. # must be handled differently.
  244. Contest.tasks: False,
  245. Contest.participations: False,
  246. }, parent=parent)