admin_generator.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. # -*- coding: utf-8 -*-
  2. """
  3. The Django Admin Generator is a project which can automatically generate
  4. (scaffold) a Django Admin for you. By doing this it will introspect your
  5. models and automatically generate an Admin with properties like:
  6. - `list_display` for all local fields
  7. - `list_filter` for foreign keys with few items
  8. - `raw_id_fields` for foreign keys with a lot of items
  9. - `search_fields` for name and `slug` fields
  10. - `prepopulated_fields` for `slug` fields
  11. - `date_hierarchy` for `created_at`, `updated_at` or `joined_at` fields
  12. The original source and latest version can be found here:
  13. https://github.com/WoLpH/django-admin-generator/
  14. """
  15. import re
  16. import six
  17. from django.apps import apps
  18. from django.conf import settings
  19. from django.core.management.base import LabelCommand, CommandError
  20. from django.db import models
  21. from django_extensions.management.utils import signalcommand
  22. # Configurable constants
  23. MAX_LINE_WIDTH = getattr(settings, 'MAX_LINE_WIDTH', 78)
  24. INDENT_WIDTH = getattr(settings, 'INDENT_WIDTH', 4)
  25. LIST_FILTER_THRESHOLD = getattr(settings, 'LIST_FILTER_THRESHOLD', 25)
  26. RAW_ID_THRESHOLD = getattr(settings, 'RAW_ID_THRESHOLD', 100)
  27. LIST_FILTER = getattr(settings, 'LIST_FILTER', (
  28. models.DateField,
  29. models.DateTimeField,
  30. models.ForeignKey,
  31. models.BooleanField,
  32. ))
  33. SEARCH_FIELD_NAMES = getattr(settings, 'SEARCH_FIELD_NAMES', (
  34. 'name',
  35. 'slug',
  36. ))
  37. DATE_HIERARCHY_NAMES = getattr(settings, 'DATE_HIERARCHY_NAMES', (
  38. 'joined_at',
  39. 'updated_at',
  40. 'created_at',
  41. ))
  42. PREPOPULATED_FIELD_NAMES = getattr(settings, 'PREPOPULATED_FIELD_NAMES', (
  43. 'slug=name',
  44. ))
  45. PRINT_IMPORTS = getattr(settings, 'PRINT_IMPORTS', '''# -*- coding: utf-8 -*-
  46. from django.contrib import admin
  47. from .models import %(models)s
  48. ''')
  49. PRINT_ADMIN_CLASS = getattr(settings, 'PRINT_ADMIN_CLASS', '''
  50. @admin.register(%(name)s)
  51. class %(name)sAdmin(admin.ModelAdmin):%(class_)s
  52. ''')
  53. PRINT_ADMIN_PROPERTY = getattr(settings, 'PRINT_ADMIN_PROPERTY', '''
  54. %(key)s = %(value)s''')
  55. class UnicodeMixin:
  56. """
  57. Mixin class to handle defining the proper __str__/__unicode__
  58. methods in Python 2 or 3.
  59. """
  60. def __str__(self):
  61. return self.__unicode__()
  62. class AdminApp(UnicodeMixin):
  63. def __init__(self, app_config, model_res, **options):
  64. self.app_config = app_config
  65. self.model_res = model_res
  66. self.options = options
  67. def __iter__(self):
  68. for model in self.app_config.get_models():
  69. admin_model = AdminModel(model, **self.options)
  70. for model_re in self.model_res:
  71. if model_re.search(admin_model.name):
  72. break
  73. else:
  74. if self.model_res:
  75. continue
  76. yield admin_model
  77. def __unicode__(self):
  78. return ''.join(self._unicode_generator())
  79. def _unicode_generator(self):
  80. models_list = [admin_model.name for admin_model in self]
  81. yield PRINT_IMPORTS % dict(models=', '.join(models_list))
  82. admin_model_names = []
  83. for admin_model in self:
  84. yield PRINT_ADMIN_CLASS % dict(
  85. name=admin_model.name,
  86. class_=admin_model,
  87. )
  88. admin_model_names.append(admin_model.name)
  89. def __repr__(self):
  90. return '<%s[%s]>' % (
  91. self.__class__.__name__,
  92. self.app.name,
  93. )
  94. class AdminModel(UnicodeMixin):
  95. PRINTABLE_PROPERTIES = (
  96. 'list_display',
  97. 'list_filter',
  98. 'raw_id_fields',
  99. 'search_fields',
  100. 'prepopulated_fields',
  101. 'date_hierarchy',
  102. )
  103. def __init__(self, model, raw_id_threshold=RAW_ID_THRESHOLD,
  104. list_filter_threshold=LIST_FILTER_THRESHOLD,
  105. search_field_names=SEARCH_FIELD_NAMES,
  106. date_hierarchy_names=DATE_HIERARCHY_NAMES,
  107. prepopulated_field_names=PREPOPULATED_FIELD_NAMES, **options):
  108. self.model = model
  109. self.list_display = []
  110. self.list_filter = []
  111. self.raw_id_fields = []
  112. self.search_fields = []
  113. self.prepopulated_fields = {}
  114. self.date_hierarchy = None
  115. self.search_field_names = search_field_names
  116. self.raw_id_threshold = raw_id_threshold
  117. self.list_filter_threshold = list_filter_threshold
  118. self.date_hierarchy_names = date_hierarchy_names
  119. self.prepopulated_field_names = prepopulated_field_names
  120. def __repr__(self):
  121. return '<%s[%s]>' % (
  122. self.__class__.__name__,
  123. self.name,
  124. )
  125. @property
  126. def name(self):
  127. return self.model.__name__
  128. def _process_many_to_many(self, meta):
  129. raw_id_threshold = self.raw_id_threshold
  130. for field in meta.local_many_to_many:
  131. if hasattr(field, 'remote_field'):
  132. related_model = getattr(field.remote_field, 'related_model', field.remote_field.model)
  133. else:
  134. raise CommandError("Unable to process ManyToMany relation")
  135. related_objects = related_model.objects.all()
  136. if related_objects[:raw_id_threshold].count() < raw_id_threshold:
  137. yield field.name
  138. def _process_fields(self, meta):
  139. parent_fields = meta.parents.values()
  140. for field in meta.fields:
  141. name = self._process_field(field, parent_fields)
  142. if name:
  143. yield name
  144. def _process_foreign_key(self, field):
  145. raw_id_threshold = self.raw_id_threshold
  146. list_filter_threshold = self.list_filter_threshold
  147. max_count = max(list_filter_threshold, raw_id_threshold)
  148. if hasattr(field, 'remote_field'):
  149. related_model = getattr(field.remote_field, 'related_model', field.remote_field.model)
  150. else:
  151. raise CommandError("Unable to process ForeignKey relation")
  152. related_count = related_model.objects.all()
  153. related_count = related_count[:max_count].count()
  154. if related_count >= raw_id_threshold:
  155. self.raw_id_fields.append(field.name)
  156. elif related_count < list_filter_threshold:
  157. self.list_filter.append(field.name)
  158. else: # pragma: no cover
  159. pass # Do nothing :)
  160. def _process_field(self, field, parent_fields):
  161. if field in parent_fields:
  162. return
  163. field_name = six.text_type(field.name)
  164. self.list_display.append(field_name)
  165. if isinstance(field, LIST_FILTER):
  166. if isinstance(field, models.ForeignKey):
  167. self._process_foreign_key(field)
  168. else:
  169. self.list_filter.append(field_name)
  170. if field.name in self.search_field_names:
  171. self.search_fields.append(field_name)
  172. return field_name
  173. def __unicode__(self):
  174. return ''.join(self._unicode_generator())
  175. def _yield_value(self, key, value):
  176. if isinstance(value, (list, set, tuple)):
  177. return self._yield_tuple(key, tuple(value))
  178. elif isinstance(value, dict):
  179. return self._yield_dict(key, value)
  180. elif isinstance(value, str):
  181. return self._yield_string(key, value)
  182. else: # pragma: no cover
  183. raise TypeError('%s is not supported in %r' % (type(value), value))
  184. def _yield_string(self, key, value, converter=repr):
  185. return PRINT_ADMIN_PROPERTY % dict(
  186. key=key,
  187. value=converter(value),
  188. )
  189. def _yield_dict(self, key, value):
  190. row_parts = []
  191. row = self._yield_string(key, value)
  192. if len(row) > MAX_LINE_WIDTH:
  193. row_parts.append(self._yield_string(key, '{', str))
  194. for k, v in value.items():
  195. row_parts.append('%s%r: %r' % (2 * INDENT_WIDTH * ' ', k, v))
  196. row_parts.append(INDENT_WIDTH * ' ' + '}')
  197. row = '\n'.join(row_parts)
  198. return row
  199. def _yield_tuple(self, key, value):
  200. row_parts = []
  201. row = self._yield_string(key, value)
  202. if len(row) > MAX_LINE_WIDTH:
  203. row_parts.append(self._yield_string(key, '(', str))
  204. for v in value:
  205. row_parts.append(2 * INDENT_WIDTH * ' ' + repr(v) + ',')
  206. row_parts.append(INDENT_WIDTH * ' ' + ')')
  207. row = '\n'.join(row_parts)
  208. return row
  209. def _unicode_generator(self):
  210. self._process()
  211. for key in self.PRINTABLE_PROPERTIES:
  212. value = getattr(self, key)
  213. if value:
  214. yield self._yield_value(key, value)
  215. def _process(self):
  216. meta = self.model._meta
  217. self.raw_id_fields += list(self._process_many_to_many(meta))
  218. field_names = list(self._process_fields(meta))
  219. for field_name in self.date_hierarchy_names[::-1]:
  220. if field_name in field_names and not self.date_hierarchy:
  221. self.date_hierarchy = field_name
  222. break
  223. for k in sorted(self.prepopulated_field_names):
  224. k, vs = k.split('=', 1)
  225. vs = vs.split(',')
  226. if k in field_names:
  227. incomplete = False
  228. for v in vs:
  229. if v not in field_names:
  230. incomplete = True
  231. break
  232. if not incomplete:
  233. self.prepopulated_fields[k] = vs
  234. self.processed = True
  235. class Command(LabelCommand):
  236. help = '''Generate a `admin.py` file for the given app (models)'''
  237. # args = "[app_name]"
  238. can_import_settings = True
  239. def add_arguments(self, parser):
  240. parser.add_argument('app_name')
  241. parser.add_argument('model_name', nargs='*')
  242. parser.add_argument(
  243. '-s', '--search-field', action='append',
  244. default=SEARCH_FIELD_NAMES,
  245. help='Fields named like this will be added to `search_fields`'
  246. ' [default: %(default)s]')
  247. parser.add_argument(
  248. '-d', '--date-hierarchy', action='append',
  249. default=DATE_HIERARCHY_NAMES,
  250. help='A field named like this will be set as `date_hierarchy`'
  251. ' [default: %(default)s]')
  252. parser.add_argument(
  253. '-p', '--prepopulated-fields', action='append',
  254. default=PREPOPULATED_FIELD_NAMES,
  255. help='These fields will be prepopulated by the other field.'
  256. 'The field names can be specified like `spam=eggA,eggB,eggC`'
  257. ' [default: %(default)s]')
  258. parser.add_argument(
  259. '-l', '--list-filter-threshold', type=int,
  260. default=LIST_FILTER_THRESHOLD, metavar='LIST_FILTER_THRESHOLD',
  261. help='If a foreign key has less than LIST_FILTER_THRESHOLD items '
  262. 'it will be added to `list_filter` [default: %(default)s]')
  263. parser.add_argument(
  264. '-r', '--raw-id-threshold', type=int,
  265. default=RAW_ID_THRESHOLD, metavar='RAW_ID_THRESHOLD',
  266. help='If a foreign key has more than RAW_ID_THRESHOLD items '
  267. 'it will be added to `list_filter` [default: %(default)s]')
  268. @signalcommand
  269. def handle(self, *args, **options):
  270. app_name = options['app_name']
  271. try:
  272. app = apps.get_app_config(app_name)
  273. except LookupError:
  274. self.stderr.write('This command requires an existing app name as argument')
  275. self.stderr.write('Available apps:')
  276. app_labels = [app.label for app in apps.get_app_configs()]
  277. for label in sorted(app_labels):
  278. self.stderr.write(' %s' % label)
  279. return
  280. model_res = []
  281. for arg in options['model_name']:
  282. model_res.append(re.compile(arg, re.IGNORECASE))
  283. self.stdout.write(AdminApp(app, model_res, **options).__str__())