Development of an internal social media platform with personalised dashboards for students
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 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import importlib
  2. import os
  3. import sys
  4. from django.apps import apps
  5. from django.db.models.fields import NOT_PROVIDED
  6. from django.utils import datetime_safe, timezone
  7. from .loader import MigrationLoader
  8. class MigrationQuestioner:
  9. """
  10. Give the autodetector responses to questions it might have.
  11. This base class has a built-in noninteractive mode, but the
  12. interactive subclass is what the command-line arguments will use.
  13. """
  14. def __init__(self, defaults=None, specified_apps=None, dry_run=None):
  15. self.defaults = defaults or {}
  16. self.specified_apps = specified_apps or set()
  17. self.dry_run = dry_run
  18. def ask_initial(self, app_label):
  19. """Should we create an initial migration for the app?"""
  20. # If it was specified on the command line, definitely true
  21. if app_label in self.specified_apps:
  22. return True
  23. # Otherwise, we look to see if it has a migrations module
  24. # without any Python files in it, apart from __init__.py.
  25. # Apps from the new app template will have these; the python
  26. # file check will ensure we skip South ones.
  27. try:
  28. app_config = apps.get_app_config(app_label)
  29. except LookupError: # It's a fake app.
  30. return self.defaults.get("ask_initial", False)
  31. migrations_import_path, _ = MigrationLoader.migrations_module(app_config.label)
  32. if migrations_import_path is None:
  33. # It's an application with migrations disabled.
  34. return self.defaults.get("ask_initial", False)
  35. try:
  36. migrations_module = importlib.import_module(migrations_import_path)
  37. except ImportError:
  38. return self.defaults.get("ask_initial", False)
  39. else:
  40. # getattr() needed on PY36 and older (replace with attribute access).
  41. if getattr(migrations_module, "__file__", None):
  42. filenames = os.listdir(os.path.dirname(migrations_module.__file__))
  43. elif hasattr(migrations_module, "__path__"):
  44. if len(migrations_module.__path__) > 1:
  45. return False
  46. filenames = os.listdir(list(migrations_module.__path__)[0])
  47. return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
  48. def ask_not_null_addition(self, field_name, model_name):
  49. """Adding a NOT NULL field to a model."""
  50. # None means quit
  51. return None
  52. def ask_not_null_alteration(self, field_name, model_name):
  53. """Changing a NULL field to NOT NULL."""
  54. # None means quit
  55. return None
  56. def ask_rename(self, model_name, old_name, new_name, field_instance):
  57. """Was this field really renamed?"""
  58. return self.defaults.get("ask_rename", False)
  59. def ask_rename_model(self, old_model_state, new_model_state):
  60. """Was this model really renamed?"""
  61. return self.defaults.get("ask_rename_model", False)
  62. def ask_merge(self, app_label):
  63. """Do you really want to merge these migrations?"""
  64. return self.defaults.get("ask_merge", False)
  65. def ask_auto_now_add_addition(self, field_name, model_name):
  66. """Adding an auto_now_add field to a model."""
  67. # None means quit
  68. return None
  69. class InteractiveMigrationQuestioner(MigrationQuestioner):
  70. def _boolean_input(self, question, default=None):
  71. result = input("%s " % question)
  72. if not result and default is not None:
  73. return default
  74. while not result or result[0].lower() not in "yn":
  75. result = input("Please answer yes or no: ")
  76. return result[0].lower() == "y"
  77. def _choice_input(self, question, choices):
  78. print(question)
  79. for i, choice in enumerate(choices):
  80. print(" %s) %s" % (i + 1, choice))
  81. result = input("Select an option: ")
  82. while True:
  83. try:
  84. value = int(result)
  85. except ValueError:
  86. pass
  87. else:
  88. if 0 < value <= len(choices):
  89. return value
  90. result = input("Please select a valid option: ")
  91. def _ask_default(self, default=''):
  92. """
  93. Prompt for a default value.
  94. The ``default`` argument allows providing a custom default value (as a
  95. string) which will be shown to the user and used as the return value
  96. if the user doesn't provide any other input.
  97. """
  98. print("Please enter the default value now, as valid Python")
  99. if default:
  100. print(
  101. "You can accept the default '{}' by pressing 'Enter' or you "
  102. "can provide another value.".format(default)
  103. )
  104. print("The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now")
  105. print("Type 'exit' to exit this prompt")
  106. while True:
  107. if default:
  108. prompt = "[default: {}] >>> ".format(default)
  109. else:
  110. prompt = ">>> "
  111. code = input(prompt)
  112. if not code and default:
  113. code = default
  114. if not code:
  115. print("Please enter some code, or 'exit' (with no quotes) to exit.")
  116. elif code == "exit":
  117. sys.exit(1)
  118. else:
  119. try:
  120. return eval(code, {}, {"datetime": datetime_safe, "timezone": timezone})
  121. except (SyntaxError, NameError) as e:
  122. print("Invalid input: %s" % e)
  123. def ask_not_null_addition(self, field_name, model_name):
  124. """Adding a NOT NULL field to a model."""
  125. if not self.dry_run:
  126. choice = self._choice_input(
  127. "You are trying to add a non-nullable field '%s' to %s without a default; "
  128. "we can't do that (the database needs something to populate existing rows).\n"
  129. "Please select a fix:" % (field_name, model_name),
  130. [
  131. ("Provide a one-off default now (will be set on all existing "
  132. "rows with a null value for this column)"),
  133. "Quit, and let me add a default in models.py",
  134. ]
  135. )
  136. if choice == 2:
  137. sys.exit(3)
  138. else:
  139. return self._ask_default()
  140. return None
  141. def ask_not_null_alteration(self, field_name, model_name):
  142. """Changing a NULL field to NOT NULL."""
  143. if not self.dry_run:
  144. choice = self._choice_input(
  145. "You are trying to change the nullable field '%s' on %s to non-nullable "
  146. "without a default; we can't do that (the database needs something to "
  147. "populate existing rows).\n"
  148. "Please select a fix:" % (field_name, model_name),
  149. [
  150. ("Provide a one-off default now (will be set on all existing "
  151. "rows with a null value for this column)"),
  152. ("Ignore for now, and let me handle existing rows with NULL myself "
  153. "(e.g. because you added a RunPython or RunSQL operation to handle "
  154. "NULL values in a previous data migration)"),
  155. "Quit, and let me add a default in models.py",
  156. ]
  157. )
  158. if choice == 2:
  159. return NOT_PROVIDED
  160. elif choice == 3:
  161. sys.exit(3)
  162. else:
  163. return self._ask_default()
  164. return None
  165. def ask_rename(self, model_name, old_name, new_name, field_instance):
  166. """Was this field really renamed?"""
  167. msg = "Did you rename %s.%s to %s.%s (a %s)? [y/N]"
  168. return self._boolean_input(msg % (model_name, old_name, model_name, new_name,
  169. field_instance.__class__.__name__), False)
  170. def ask_rename_model(self, old_model_state, new_model_state):
  171. """Was this model really renamed?"""
  172. msg = "Did you rename the %s.%s model to %s? [y/N]"
  173. return self._boolean_input(msg % (old_model_state.app_label, old_model_state.name,
  174. new_model_state.name), False)
  175. def ask_merge(self, app_label):
  176. return self._boolean_input(
  177. "\nMerging will only work if the operations printed above do not conflict\n" +
  178. "with each other (working on different fields or models)\n" +
  179. "Do you want to merge these migration branches? [y/N]",
  180. False,
  181. )
  182. def ask_auto_now_add_addition(self, field_name, model_name):
  183. """Adding an auto_now_add field to a model."""
  184. if not self.dry_run:
  185. choice = self._choice_input(
  186. "You are trying to add the field '{}' with 'auto_now_add=True' "
  187. "to {} without a default; the database needs something to "
  188. "populate existing rows.\n".format(field_name, model_name),
  189. [
  190. "Provide a one-off default now (will be set on all "
  191. "existing rows)",
  192. "Quit, and let me add a default in models.py",
  193. ]
  194. )
  195. if choice == 2:
  196. sys.exit(3)
  197. else:
  198. return self._ask_default(default='timezone.now')
  199. return None
  200. class NonInteractiveMigrationQuestioner(MigrationQuestioner):
  201. def ask_not_null_addition(self, field_name, model_name):
  202. # We can't ask the user, so act like the user aborted.
  203. sys.exit(3)
  204. def ask_not_null_alteration(self, field_name, model_name):
  205. # We can't ask the user, so set as not provided.
  206. return NOT_PROVIDED
  207. def ask_auto_now_add_addition(self, field_name, model_name):
  208. # We can't ask the user, so act like the user aborted.
  209. sys.exit(3)