__init__.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. """
  2. An app that is forced to the top of the list in ``INSTALLED_APPS``
  3. for the purpose of hooking into Django's ``class_prepared`` signal
  4. and adding custom fields as defined by the ``EXTRA_MODEL_FIELDS``
  5. setting. Also patches ``django.contrib.admin.site`` to use
  6. ``LazyAdminSite`` that defers certains register/unregister calls
  7. until ``admin.autodiscover`` to avoid some timing issues around
  8. custom fields not being available when custom admin classes are
  9. registered.
  10. """
  11. from __future__ import unicode_literals
  12. from collections import defaultdict
  13. from django import VERSION as DJANGO_VERSION
  14. from django.apps import apps
  15. from django.conf import settings
  16. from django.contrib import admin
  17. from django.core.exceptions import ImproperlyConfigured
  18. from django.db.models.signals import class_prepared
  19. from mezzanine.boot.lazy_admin import LazyAdminSite
  20. from mezzanine.utils.importing import import_dotted_path
  21. def parse_field_path(field_path):
  22. """
  23. Take a path to a field like "mezzanine.pages.models.Page.feature_image"
  24. and return a model key, which is a tuple of the form ('pages', 'page'),
  25. and a field name, e.g. "feature_image".
  26. """
  27. model_path, field_name = field_path.rsplit(".", 1)
  28. app_name, model_name = model_path.split('.models.')
  29. _, app_label = app_name.rsplit('.', 1)
  30. return (app_label, model_name.lower()), field_name
  31. def import_field(field_classpath):
  32. """
  33. Imports a field by its dotted class path, prepending "django.db.models"
  34. to raw class names and raising an exception if the import fails.
  35. """
  36. if '.' in field_classpath:
  37. fully_qualified = field_classpath
  38. else:
  39. fully_qualified = "django.db.models.%s" % field_classpath
  40. try:
  41. return import_dotted_path(fully_qualified)
  42. except ImportError:
  43. raise ImproperlyConfigured("The EXTRA_MODEL_FIELDS setting contains "
  44. "the field '%s' which could not be "
  45. "imported." % field_classpath)
  46. def parse_extra_model_fields(extra_model_fields):
  47. """
  48. Parses the value of EXTRA_MODEL_FIELDS, grouping the entries by model
  49. and instantiating the extra fields. Returns a sequence of tuples of
  50. the form (model_key, fields) where model_key is a pair of app_label,
  51. model_name and fields is a list of (field_name, field_instance) pairs.
  52. """
  53. fields = defaultdict(list)
  54. for entry in extra_model_fields:
  55. model_key, field_name = parse_field_path(entry[0])
  56. field_class = import_field(entry[1])
  57. field_args, field_kwargs = entry[2:]
  58. try:
  59. field = field_class(*field_args, **field_kwargs)
  60. except TypeError as e:
  61. raise ImproperlyConfigured(
  62. "The EXTRA_MODEL_FIELDS setting contains arguments for the "
  63. "field '%s' which could not be applied: %s" % (entry[1], e))
  64. fields[model_key].append((field_name, field))
  65. return fields
  66. fields = parse_extra_model_fields(getattr(settings, "EXTRA_MODEL_FIELDS", []))
  67. def add_extra_model_fields(sender, **kwargs):
  68. """
  69. Injects custom fields onto the given sender model as defined
  70. by the ``EXTRA_MODEL_FIELDS`` setting. This is a closure over
  71. the "fields" variable.
  72. """
  73. model_key = sender._meta.app_label, sender._meta.model_name
  74. for field_name, field in fields.get(model_key, {}):
  75. field.contribute_to_class(sender, field_name)
  76. if DJANGO_VERSION < (1, 9):
  77. if fields:
  78. class_prepared.connect(add_extra_model_fields,
  79. dispatch_uid="FQFEQ#rfq3r")
  80. else:
  81. for model_key in fields:
  82. apps.lazy_model_operation(add_extra_model_fields, model_key)
  83. # Override django.contrib.admin.site with LazyAdminSite. It must
  84. # be bound to a separate name (admin_site) for access in autodiscover
  85. # below.
  86. admin_site = LazyAdminSite()
  87. admin.site = admin_site
  88. django_autodiscover = admin.autodiscover
  89. def autodiscover(*args, **kwargs):
  90. """
  91. Replaces django's original autodiscover to add a call to
  92. LazyAdminSite's lazy_registration.
  93. """
  94. django_autodiscover(*args, **kwargs)
  95. admin_site.lazy_registration()
  96. admin.autodiscover = autodiscover