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.
 
 
 
 

197 lines
8.0 KiB

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