Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

questioner.py 13KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import datetime
  2. import importlib
  3. import os
  4. import sys
  5. from django.apps import apps
  6. from django.core.management.base import OutputWrapper
  7. from django.db.models import NOT_PROVIDED
  8. from django.utils import timezone
  9. from django.utils.version import get_docs_version
  10. from .loader import MigrationLoader
  11. class MigrationQuestioner:
  12. """
  13. Give the autodetector responses to questions it might have.
  14. This base class has a built-in noninteractive mode, but the
  15. interactive subclass is what the command-line arguments will use.
  16. """
  17. def __init__(self, defaults=None, specified_apps=None, dry_run=None):
  18. self.defaults = defaults or {}
  19. self.specified_apps = specified_apps or set()
  20. self.dry_run = dry_run
  21. def ask_initial(self, app_label):
  22. """Should we create an initial migration for the app?"""
  23. # If it was specified on the command line, definitely true
  24. if app_label in self.specified_apps:
  25. return True
  26. # Otherwise, we look to see if it has a migrations module
  27. # without any Python files in it, apart from __init__.py.
  28. # Apps from the new app template will have these; the Python
  29. # file check will ensure we skip South ones.
  30. try:
  31. app_config = apps.get_app_config(app_label)
  32. except LookupError: # It's a fake app.
  33. return self.defaults.get("ask_initial", False)
  34. migrations_import_path, _ = MigrationLoader.migrations_module(app_config.label)
  35. if migrations_import_path is None:
  36. # It's an application with migrations disabled.
  37. return self.defaults.get("ask_initial", False)
  38. try:
  39. migrations_module = importlib.import_module(migrations_import_path)
  40. except ImportError:
  41. return self.defaults.get("ask_initial", False)
  42. else:
  43. if getattr(migrations_module, "__file__", None):
  44. filenames = os.listdir(os.path.dirname(migrations_module.__file__))
  45. elif hasattr(migrations_module, "__path__"):
  46. if len(migrations_module.__path__) > 1:
  47. return False
  48. filenames = os.listdir(list(migrations_module.__path__)[0])
  49. return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
  50. def ask_not_null_addition(self, field_name, model_name):
  51. """Adding a NOT NULL field to a model."""
  52. # None means quit
  53. return None
  54. def ask_not_null_alteration(self, field_name, model_name):
  55. """Changing a NULL field to NOT NULL."""
  56. # None means quit
  57. return None
  58. def ask_rename(self, model_name, old_name, new_name, field_instance):
  59. """Was this field really renamed?"""
  60. return self.defaults.get("ask_rename", False)
  61. def ask_rename_model(self, old_model_state, new_model_state):
  62. """Was this model really renamed?"""
  63. return self.defaults.get("ask_rename_model", False)
  64. def ask_merge(self, app_label):
  65. """Should these migrations really be merged?"""
  66. return self.defaults.get("ask_merge", False)
  67. def ask_auto_now_add_addition(self, field_name, model_name):
  68. """Adding an auto_now_add field to a model."""
  69. # None means quit
  70. return None
  71. def ask_unique_callable_default_addition(self, field_name, model_name):
  72. """Adding a unique field with a callable default."""
  73. # None means continue.
  74. return None
  75. class InteractiveMigrationQuestioner(MigrationQuestioner):
  76. def __init__(
  77. self, defaults=None, specified_apps=None, dry_run=None, prompt_output=None
  78. ):
  79. super().__init__(
  80. defaults=defaults, specified_apps=specified_apps, dry_run=dry_run
  81. )
  82. self.prompt_output = prompt_output or OutputWrapper(sys.stdout)
  83. def _boolean_input(self, question, default=None):
  84. self.prompt_output.write(f"{question} ", ending="")
  85. result = input()
  86. if not result and default is not None:
  87. return default
  88. while not result or result[0].lower() not in "yn":
  89. self.prompt_output.write("Please answer yes or no: ", ending="")
  90. result = input()
  91. return result[0].lower() == "y"
  92. def _choice_input(self, question, choices):
  93. self.prompt_output.write(f"{question}")
  94. for i, choice in enumerate(choices):
  95. self.prompt_output.write(" %s) %s" % (i + 1, choice))
  96. self.prompt_output.write("Select an option: ", ending="")
  97. result = input()
  98. while True:
  99. try:
  100. value = int(result)
  101. except ValueError:
  102. pass
  103. else:
  104. if 0 < value <= len(choices):
  105. return value
  106. self.prompt_output.write("Please select a valid option: ", ending="")
  107. result = input()
  108. def _ask_default(self, default=""):
  109. """
  110. Prompt for a default value.
  111. The ``default`` argument allows providing a custom default value (as a
  112. string) which will be shown to the user and used as the return value
  113. if the user doesn't provide any other input.
  114. """
  115. self.prompt_output.write("Please enter the default value as valid Python.")
  116. if default:
  117. self.prompt_output.write(
  118. f"Accept the default '{default}' by pressing 'Enter' or "
  119. f"provide another value."
  120. )
  121. self.prompt_output.write(
  122. "The datetime and django.utils.timezone modules are available, so "
  123. "it is possible to provide e.g. timezone.now as a value."
  124. )
  125. self.prompt_output.write("Type 'exit' to exit this prompt")
  126. while True:
  127. if default:
  128. prompt = "[default: {}] >>> ".format(default)
  129. else:
  130. prompt = ">>> "
  131. self.prompt_output.write(prompt, ending="")
  132. code = input()
  133. if not code and default:
  134. code = default
  135. if not code:
  136. self.prompt_output.write(
  137. "Please enter some code, or 'exit' (without quotes) to exit."
  138. )
  139. elif code == "exit":
  140. sys.exit(1)
  141. else:
  142. try:
  143. return eval(code, {}, {"datetime": datetime, "timezone": timezone})
  144. except (SyntaxError, NameError) as e:
  145. self.prompt_output.write("Invalid input: %s" % e)
  146. def ask_not_null_addition(self, field_name, model_name):
  147. """Adding a NOT NULL field to a model."""
  148. if not self.dry_run:
  149. choice = self._choice_input(
  150. f"It is impossible to add a non-nullable field '{field_name}' "
  151. f"to {model_name} without specifying a default. This is "
  152. f"because the database needs something to populate existing "
  153. f"rows.\n"
  154. f"Please select a fix:",
  155. [
  156. (
  157. "Provide a one-off default now (will be set on all existing "
  158. "rows with a null value for this column)"
  159. ),
  160. "Quit and manually define a default value in models.py.",
  161. ],
  162. )
  163. if choice == 2:
  164. sys.exit(3)
  165. else:
  166. return self._ask_default()
  167. return None
  168. def ask_not_null_alteration(self, field_name, model_name):
  169. """Changing a NULL field to NOT NULL."""
  170. if not self.dry_run:
  171. choice = self._choice_input(
  172. f"It is impossible to change a nullable field '{field_name}' "
  173. f"on {model_name} to non-nullable without providing a "
  174. f"default. This is because the database needs something to "
  175. f"populate existing rows.\n"
  176. f"Please select a fix:",
  177. [
  178. (
  179. "Provide a one-off default now (will be set on all existing "
  180. "rows with a null value for this column)"
  181. ),
  182. "Ignore for now. Existing rows that contain NULL values "
  183. "will have to be handled manually, for example with a "
  184. "RunPython or RunSQL operation.",
  185. "Quit and manually define a default value in models.py.",
  186. ],
  187. )
  188. if choice == 2:
  189. return NOT_PROVIDED
  190. elif choice == 3:
  191. sys.exit(3)
  192. else:
  193. return self._ask_default()
  194. return None
  195. def ask_rename(self, model_name, old_name, new_name, field_instance):
  196. """Was this field really renamed?"""
  197. msg = "Was %s.%s renamed to %s.%s (a %s)? [y/N]"
  198. return self._boolean_input(
  199. msg
  200. % (
  201. model_name,
  202. old_name,
  203. model_name,
  204. new_name,
  205. field_instance.__class__.__name__,
  206. ),
  207. False,
  208. )
  209. def ask_rename_model(self, old_model_state, new_model_state):
  210. """Was this model really renamed?"""
  211. msg = "Was the model %s.%s renamed to %s? [y/N]"
  212. return self._boolean_input(
  213. msg
  214. % (old_model_state.app_label, old_model_state.name, new_model_state.name),
  215. False,
  216. )
  217. def ask_merge(self, app_label):
  218. return self._boolean_input(
  219. "\nMerging will only work if the operations printed above do not conflict\n"
  220. + "with each other (working on different fields or models)\n"
  221. + "Should these migration branches be merged? [y/N]",
  222. False,
  223. )
  224. def ask_auto_now_add_addition(self, field_name, model_name):
  225. """Adding an auto_now_add field to a model."""
  226. if not self.dry_run:
  227. choice = self._choice_input(
  228. f"It is impossible to add the field '{field_name}' with "
  229. f"'auto_now_add=True' to {model_name} without providing a "
  230. f"default. This is because the database needs something to "
  231. f"populate existing rows.\n",
  232. [
  233. "Provide a one-off default now which will be set on all "
  234. "existing rows",
  235. "Quit and manually define a default value in models.py.",
  236. ],
  237. )
  238. if choice == 2:
  239. sys.exit(3)
  240. else:
  241. return self._ask_default(default="timezone.now")
  242. return None
  243. def ask_unique_callable_default_addition(self, field_name, model_name):
  244. """Adding a unique field with a callable default."""
  245. if not self.dry_run:
  246. version = get_docs_version()
  247. choice = self._choice_input(
  248. f"Callable default on unique field {model_name}.{field_name} "
  249. f"will not generate unique values upon migrating.\n"
  250. f"Please choose how to proceed:\n",
  251. [
  252. f"Continue making this migration as the first step in "
  253. f"writing a manual migration to generate unique values "
  254. f"described here: "
  255. f"https://docs.djangoproject.com/en/{version}/howto/"
  256. f"writing-migrations/#migrations-that-add-unique-fields.",
  257. "Quit and edit field options in models.py.",
  258. ],
  259. )
  260. if choice == 2:
  261. sys.exit(3)
  262. return None
  263. class NonInteractiveMigrationQuestioner(MigrationQuestioner):
  264. def __init__(
  265. self,
  266. defaults=None,
  267. specified_apps=None,
  268. dry_run=None,
  269. verbosity=1,
  270. log=None,
  271. ):
  272. self.verbosity = verbosity
  273. self.log = log
  274. super().__init__(
  275. defaults=defaults,
  276. specified_apps=specified_apps,
  277. dry_run=dry_run,
  278. )
  279. def log_lack_of_migration(self, field_name, model_name, reason):
  280. if self.verbosity > 0:
  281. self.log(
  282. f"Field '{field_name}' on model '{model_name}' not migrated: "
  283. f"{reason}."
  284. )
  285. def ask_not_null_addition(self, field_name, model_name):
  286. # We can't ask the user, so act like the user aborted.
  287. self.log_lack_of_migration(
  288. field_name,
  289. model_name,
  290. "it is impossible to add a non-nullable field without specifying "
  291. "a default",
  292. )
  293. sys.exit(3)
  294. def ask_not_null_alteration(self, field_name, model_name):
  295. # We can't ask the user, so set as not provided.
  296. self.log(
  297. f"Field '{field_name}' on model '{model_name}' given a default of "
  298. f"NOT PROVIDED and must be corrected."
  299. )
  300. return NOT_PROVIDED
  301. def ask_auto_now_add_addition(self, field_name, model_name):
  302. # We can't ask the user, so act like the user aborted.
  303. self.log_lack_of_migration(
  304. field_name,
  305. model_name,
  306. "it is impossible to add a field with 'auto_now_add=True' without "
  307. "specifying a default",
  308. )
  309. sys.exit(3)