__init__.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. """
  2. Drop-in replacement for ``django.conf.settings`` that provides a
  3. consistent access method for settings defined in applications, the project
  4. or Django itself. Settings can also be made editable via the admin.
  5. """
  6. from __future__ import unicode_literals
  7. from weakref import WeakKeyDictionary
  8. from future.builtins import bytes, str
  9. from functools import partial
  10. from importlib import import_module
  11. from warnings import warn
  12. from django.conf import settings as django_settings
  13. from django.utils.functional import Promise
  14. from django.utils.module_loading import module_has_submodule
  15. from mezzanine import __version__ # noqa
  16. from mezzanine.core.request import current_request
  17. registry = {}
  18. def register_setting(name=None, label=None, editable=False, description=None,
  19. default=None, choices=None, append=False,
  20. translatable=False):
  21. """
  22. Registers a setting that can be edited via the admin. This mostly
  23. equates to storing the given args as a dict in the ``registry``
  24. dict by name.
  25. """
  26. if name is None:
  27. raise TypeError("mezzanine.conf.register_setting requires the "
  28. "'name' keyword argument.")
  29. if editable and default is None:
  30. raise TypeError("mezzanine.conf.register_setting requires the "
  31. "'default' keyword argument when 'editable' is True.")
  32. # append is True when called from an app (typically external)
  33. # after the setting has already been registered, with the
  34. # intention of appending to its default value.
  35. if append and name in registry:
  36. registry[name]["default"] += default
  37. return
  38. # If an editable setting has a value defined in the
  39. # project's settings.py module, it can't be editable, since
  40. # these lead to a lot of confusion once its value gets
  41. # defined in the db.
  42. if hasattr(django_settings, name):
  43. editable = False
  44. if label is None:
  45. label = name.replace("_", " ").title()
  46. # Python 2/3 compatibility. isinstance() is overridden by future
  47. # on Python 2 to behave as Python 3 in conjunction with either
  48. # Python 2's native types or the future.builtins types.
  49. if isinstance(default, bool):
  50. # Prevent bools treated as ints
  51. setting_type = bool
  52. elif isinstance(default, int):
  53. # An int or long or subclass on Py2
  54. setting_type = int
  55. elif isinstance(default, (str, Promise)):
  56. # A unicode or subclass on Py2
  57. setting_type = str
  58. elif isinstance(default, bytes):
  59. # A byte-string or subclass on Py2
  60. setting_type = bytes
  61. else:
  62. setting_type = type(default)
  63. registry[name] = {"name": name, "label": label, "editable": editable,
  64. "description": description, "default": default,
  65. "choices": choices, "type": setting_type,
  66. "translatable": translatable}
  67. class Settings(object):
  68. """
  69. An object that provides settings via dynamic attribute access.
  70. Settings that are registered as editable will be stored in the
  71. database once the site settings form in the admin is first saved.
  72. When these values are accessed via this settings object, *all*
  73. database stored settings get retrieved from the database.
  74. When accessing uneditable settings their default values are used,
  75. unless they've been given a value in the project's settings.py
  76. module.
  77. The settings object also provides access to Django settings via
  78. ``django.conf.settings``, in order to provide a consistent method
  79. of access for all settings.
  80. """
  81. class Placeholder(object):
  82. """A Weakly-referable wrapper of ``object``."""
  83. pass
  84. NULL_REQUEST = Placeholder()
  85. # These functions map setting types to the functions that should be
  86. # used to convert them from the Unicode string stored in the database.
  87. # If a type doesn't appear in this map, the type itself will be used.
  88. TYPE_FUNCTIONS = {
  89. bool: lambda val: val != "False",
  90. bytes: partial(bytes, encoding='utf8')
  91. }
  92. def __init__(self):
  93. """
  94. The ``_editable_caches`` attribute maps Request objects to dicts of
  95. editable settings loaded from the database. We cache settings per-
  96. request to ensure that the database is hit at most once per request,
  97. and that each request sees the same settings for its duration.
  98. """
  99. self._editable_caches = WeakKeyDictionary()
  100. @property
  101. def _current_request(self):
  102. return current_request() or self.NULL_REQUEST
  103. def use_editable(self):
  104. """
  105. Clear the cache for the current request so that editable settings are
  106. fetched from the database on next access. Using editable settings
  107. is the default, so this is deprecated in favour of ``clear_cache()``.
  108. """
  109. self.clear_cache()
  110. warn("Because editable settings are now used by default, "
  111. "settings.use_editable() is deprecated. If you need to re-load "
  112. "settings from the database during a request, please use "
  113. "settings.clear_cache() instead.",
  114. DeprecationWarning,
  115. stacklevel=2)
  116. def clear_cache(self):
  117. """Clear the settings cache for the current request."""
  118. self._editable_caches.pop(self._current_request, None)
  119. def _get_editable(self, request):
  120. """
  121. Get the dictionary of editable settings for a given request. Settings
  122. are fetched from the database once per request and then stored in
  123. ``_editable_caches``, a WeakKeyDictionary that will automatically
  124. discard each entry when no more references to the request exist.
  125. """
  126. try:
  127. editable_settings = self._editable_caches[request]
  128. except KeyError:
  129. editable_settings = self._load()
  130. if request is not self.NULL_REQUEST:
  131. self._editable_caches[request] = editable_settings
  132. return editable_settings
  133. @classmethod
  134. def _to_python(cls, setting, raw_value):
  135. """
  136. Convert a value stored in the database for a particular setting
  137. to its correct type, as determined by ``register_setting()``.
  138. """
  139. type_fn = cls.TYPE_FUNCTIONS.get(setting["type"], setting["type"])
  140. try:
  141. value = type_fn(raw_value)
  142. except ValueError:
  143. # Shouldn't occur, but just a safeguard in case
  144. # the db value somehow ended up as an invalid type.
  145. warn("The setting %s should be of type %s, but the value "
  146. "retrieved from the database (%s) could not be converted. "
  147. "Using the default instead: %s"
  148. % (setting["name"], setting["type"].__name__,
  149. repr(raw_value), repr(setting["default"])))
  150. value = setting["default"]
  151. return value
  152. def _load(self):
  153. """
  154. Load editable settings from the database and return them as a dict.
  155. Delete any settings from the database that are no longer registered,
  156. and emit a warning if there are settings that are defined in both
  157. settings.py and the database.
  158. """
  159. from mezzanine.conf.models import Setting
  160. removed_settings = []
  161. conflicting_settings = []
  162. new_cache = {}
  163. for setting_obj in Setting.objects.all():
  164. # Check that the Setting object corresponds to a setting that has
  165. # been declared in code using ``register_setting()``. If not, add
  166. # it to a list of items to be deleted from the database later.
  167. try:
  168. setting = registry[setting_obj.name]
  169. except KeyError:
  170. removed_settings.append(setting_obj.name)
  171. continue
  172. # Convert a string from the database to the correct Python type.
  173. setting_value = self._to_python(setting, setting_obj.value)
  174. # If a setting is defined both in the database and in settings.py,
  175. # raise a warning and use the value defined in settings.py.
  176. if hasattr(django_settings, setting["name"]):
  177. if setting_value != setting["default"]:
  178. conflicting_settings.append(setting_obj.name)
  179. continue
  180. # If nothing went wrong, use the value from the database!
  181. new_cache[setting["name"]] = setting_value
  182. if removed_settings:
  183. Setting.objects.filter(name__in=removed_settings).delete()
  184. if conflicting_settings:
  185. warn("These settings are defined in both settings.py and "
  186. "the database: %s. The settings.py values will be used."
  187. % ", ".join(conflicting_settings))
  188. return new_cache
  189. def __getattr__(self, name):
  190. # If this setting isn't registered, defer to Django's settings object
  191. try:
  192. setting = registry[name]
  193. except KeyError:
  194. return getattr(django_settings, name)
  195. # If the setting is editable, try the Django setting, then a value
  196. # fetched from the database, then the registered default.
  197. if setting["editable"]:
  198. editable_cache = self._get_editable(request=self._current_request)
  199. return getattr(django_settings, name,
  200. editable_cache.get(name, setting["default"]))
  201. # If if isn't editable, just try Django and then default.
  202. return getattr(django_settings, name, setting["default"])
  203. def __setattr__(self, key, value):
  204. """Forward attribute setting to the Django settings object."""
  205. setattr(django_settings, key, value)
  206. def __delattr__(self, item):
  207. """Forward attribute deletion to the Django settings object."""
  208. delattr(django_settings, item)
  209. mezz_first = lambda app: not app.startswith("mezzanine.")
  210. for app in sorted(django_settings.INSTALLED_APPS, key=mezz_first):
  211. try:
  212. module = import_module(app)
  213. except ImportError:
  214. pass
  215. else:
  216. try:
  217. import_module("%s.defaults" % app)
  218. except:
  219. if module_has_submodule(module, "defaults"):
  220. raise
  221. settings = Settings()