/django_extensions/management/modelviz.py

http://github.com/django-extensions/django-extensions · Python · 432 lines · 349 code · 58 blank · 25 comment · 110 complexity · df6fb5f7143ec0143281f5045514ba16 MD5 · raw file

  1. # -*- coding: utf-8 -*-
  2. """
  3. modelviz.py - DOT file generator for Django Models
  4. Based on:
  5. Django model to DOT (Graphviz) converter
  6. by Antonio Cavedoni <antonio@cavedoni.org>
  7. Adapted to be used with django-extensions
  8. """
  9. import datetime
  10. import os
  11. import re
  12. import six
  13. from django.apps import apps
  14. from django.db.models.fields.related import (
  15. ForeignKey, ManyToManyField, OneToOneField, RelatedField,
  16. )
  17. from django.contrib.contenttypes.fields import GenericRelation
  18. from django.template import Context, Template, loader
  19. from django.utils.encoding import force_str
  20. from django.utils.safestring import mark_safe
  21. from django.utils.translation import activate as activate_language
  22. __version__ = "1.1"
  23. __license__ = "Python"
  24. __author__ = "Bas van Oostveen <v.oostveen@gmail.com>",
  25. __contributors__ = [
  26. "Antonio Cavedoni <http://cavedoni.com/>"
  27. "Stefano J. Attardi <http://attardi.org/>",
  28. "limodou <http://www.donews.net/limodou/>",
  29. "Carlo C8E Miron",
  30. "Andre Campos <cahenan@gmail.com>",
  31. "Justin Findlay <jfindlay@gmail.com>",
  32. "Alexander Houben <alexander@houben.ch>",
  33. "Joern Hees <gitdev@joernhees.de>",
  34. "Kevin Cherepski <cherepski@gmail.com>",
  35. "Jose Tomas Tocino <theom3ga@gmail.com>",
  36. "Adam Dobrawy <naczelnik@jawnosc.tk>",
  37. "Mikkel Munch Mortensen <https://www.detfalskested.dk/>",
  38. "Andrzej Bistram <andrzej.bistram@gmail.com>",
  39. "Daniel Lipsitt <danlipsitt@gmail.com>",
  40. ]
  41. def parse_file_or_list(arg):
  42. if not arg:
  43. return []
  44. if isinstance(arg, (list, tuple, set)):
  45. return arg
  46. if ',' not in arg and os.path.isfile(arg):
  47. return [e.strip() for e in open(arg).readlines()]
  48. return [e.strip() for e in arg.split(',')]
  49. class ModelGraph(object):
  50. def __init__(self, app_labels, **kwargs):
  51. self.graphs = []
  52. self.cli_options = kwargs.get('cli_options', None)
  53. self.disable_fields = kwargs.get('disable_fields', False)
  54. self.disable_abstract_fields = kwargs.get('disable_abstract_fields', False)
  55. self.include_models = parse_file_or_list(
  56. kwargs.get('include_models', "")
  57. )
  58. self.all_applications = kwargs.get('all_applications', False)
  59. self.use_subgraph = kwargs.get('group_models', False)
  60. self.verbose_names = kwargs.get('verbose_names', False)
  61. self.inheritance = kwargs.get('inheritance', True)
  62. self.relations_as_fields = kwargs.get("relations_as_fields", True)
  63. self.sort_fields = kwargs.get("sort_fields", True)
  64. self.language = kwargs.get('language', None)
  65. if self.language is not None:
  66. activate_language(self.language)
  67. self.exclude_columns = parse_file_or_list(
  68. kwargs.get('exclude_columns', "")
  69. )
  70. self.exclude_models = parse_file_or_list(
  71. kwargs.get('exclude_models', "")
  72. )
  73. self.hide_edge_labels = kwargs.get('hide_edge_labels', False)
  74. self.arrow_shape = kwargs.get("arrow_shape")
  75. if self.all_applications:
  76. self.app_labels = [app.label for app in apps.get_app_configs()]
  77. else:
  78. self.app_labels = app_labels
  79. def generate_graph_data(self):
  80. self.process_apps()
  81. nodes = []
  82. for graph in self.graphs:
  83. nodes.extend([e['name'] for e in graph['models']])
  84. for graph in self.graphs:
  85. for model in graph['models']:
  86. for relation in model['relations']:
  87. if relation is not None:
  88. if relation['target'] in nodes:
  89. relation['needs_node'] = False
  90. def get_graph_data(self, as_json=False):
  91. now = datetime.datetime.now()
  92. graph_data = {
  93. 'created_at': now.strftime("%Y-%m-%d %H:%M"),
  94. 'cli_options': self.cli_options,
  95. 'disable_fields': self.disable_fields,
  96. 'disable_abstract_fields': self.disable_abstract_fields,
  97. 'use_subgraph': self.use_subgraph,
  98. }
  99. if as_json:
  100. graph_data['graphs'] = [context.flatten() for context in self.graphs]
  101. else:
  102. graph_data['graphs'] = self.graphs
  103. return graph_data
  104. def add_attributes(self, field, abstract_fields):
  105. if self.verbose_names and field.verbose_name:
  106. label = force_str(field.verbose_name)
  107. if label.islower():
  108. label = label.capitalize()
  109. else:
  110. label = field.name
  111. t = type(field).__name__
  112. if isinstance(field, (OneToOneField, ForeignKey)):
  113. t += " ({0})".format(field.remote_field.field_name)
  114. # TODO: ManyToManyField, GenericRelation
  115. return {
  116. 'name': field.name,
  117. 'label': label,
  118. 'type': t,
  119. 'blank': field.blank,
  120. 'abstract': field in abstract_fields,
  121. 'relation': isinstance(field, RelatedField),
  122. 'primary_key': field.primary_key,
  123. }
  124. def add_relation(self, field, model, extras=""):
  125. if self.verbose_names and field.verbose_name:
  126. label = force_str(field.verbose_name)
  127. if label.islower():
  128. label = label.capitalize()
  129. else:
  130. label = field.name
  131. # show related field name
  132. if hasattr(field, 'related_query_name'):
  133. related_query_name = field.related_query_name()
  134. if self.verbose_names and related_query_name.islower():
  135. related_query_name = related_query_name.replace('_', ' ').capitalize()
  136. label = u'{} ({})'.format(label, force_str(related_query_name))
  137. if self.hide_edge_labels:
  138. label = ''
  139. # handle self-relationships and lazy-relationships
  140. if isinstance(field.remote_field.model, six.string_types):
  141. if field.remote_field.model == 'self':
  142. target_model = field.model
  143. else:
  144. if '.' in field.remote_field.model:
  145. app_label, model_name = field.remote_field.model.split('.', 1)
  146. else:
  147. app_label = field.model._meta.app_label
  148. model_name = field.remote_field.model
  149. target_model = apps.get_model(app_label, model_name)
  150. else:
  151. target_model = field.remote_field.model
  152. _rel = self.get_relation_context(target_model, field, label, extras)
  153. if _rel not in model['relations'] and self.use_model(_rel['target']):
  154. return _rel
  155. def get_abstract_models(self, appmodels):
  156. abstract_models = []
  157. for appmodel in appmodels:
  158. abstract_models += [
  159. abstract_model for abstract_model in appmodel.__bases__
  160. if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract
  161. ]
  162. abstract_models = list(set(abstract_models)) # remove duplicates
  163. return abstract_models
  164. def get_app_context(self, app):
  165. return Context({
  166. 'name': '"%s"' % app.name,
  167. 'app_name': "%s" % app.name,
  168. 'cluster_app_name': "cluster_%s" % app.name.replace(".", "_"),
  169. 'models': []
  170. })
  171. def get_appmodel_attributes(self, appmodel):
  172. if self.relations_as_fields:
  173. attributes = [field for field in appmodel._meta.local_fields]
  174. else:
  175. # Find all the 'real' attributes. Relations are depicted as graph edges instead of attributes
  176. attributes = [field for field in appmodel._meta.local_fields if not
  177. isinstance(field, RelatedField)]
  178. return attributes
  179. def get_appmodel_abstracts(self, appmodel):
  180. return [
  181. abstract_model.__name__ for abstract_model in appmodel.__bases__
  182. if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract
  183. ]
  184. def get_appmodel_context(self, appmodel, appmodel_abstracts):
  185. context = {
  186. 'app_name': appmodel.__module__.replace(".", "_"),
  187. 'name': appmodel.__name__,
  188. 'abstracts': appmodel_abstracts,
  189. 'fields': [],
  190. 'relations': []
  191. }
  192. if self.verbose_names and appmodel._meta.verbose_name:
  193. context['label'] = force_str(appmodel._meta.verbose_name)
  194. else:
  195. context['label'] = context['name']
  196. return context
  197. def get_bases_abstract_fields(self, c):
  198. _abstract_fields = []
  199. for e in c.__bases__:
  200. if hasattr(e, '_meta') and e._meta.abstract:
  201. _abstract_fields.extend(e._meta.fields)
  202. _abstract_fields.extend(self.get_bases_abstract_fields(e))
  203. return _abstract_fields
  204. def get_inheritance_context(self, appmodel, parent):
  205. label = "multi-table"
  206. if parent._meta.abstract:
  207. label = "abstract"
  208. if appmodel._meta.proxy:
  209. label = "proxy"
  210. label += r"\ninheritance"
  211. if self.hide_edge_labels:
  212. label = ''
  213. return {
  214. 'target_app': parent.__module__.replace(".", "_"),
  215. 'target': parent.__name__,
  216. 'type': "inheritance",
  217. 'name': "inheritance",
  218. 'label': label,
  219. 'arrows': '[arrowhead=empty, arrowtail=none, dir=both]',
  220. 'needs_node': True,
  221. }
  222. def get_models(self, app):
  223. appmodels = list(app.get_models())
  224. return appmodels
  225. def get_relation_context(self, target_model, field, label, extras):
  226. return {
  227. 'target_app': target_model.__module__.replace('.', '_'),
  228. 'target': target_model.__name__,
  229. 'type': type(field).__name__,
  230. 'name': field.name,
  231. 'label': label,
  232. 'arrows': extras,
  233. 'needs_node': True
  234. }
  235. def process_attributes(self, field, model, pk, abstract_fields):
  236. newmodel = model.copy()
  237. if self.skip_field(field) or pk and field == pk:
  238. return newmodel
  239. newmodel['fields'].append(self.add_attributes(field, abstract_fields))
  240. return newmodel
  241. def process_apps(self):
  242. for app_label in self.app_labels:
  243. app = apps.get_app_config(app_label)
  244. if not app:
  245. continue
  246. app_graph = self.get_app_context(app)
  247. app_models = self.get_models(app)
  248. abstract_models = self.get_abstract_models(app_models)
  249. app_models = abstract_models + app_models
  250. for appmodel in app_models:
  251. if not self.use_model(appmodel._meta.object_name):
  252. continue
  253. appmodel_abstracts = self.get_appmodel_abstracts(appmodel)
  254. abstract_fields = self.get_bases_abstract_fields(appmodel)
  255. model = self.get_appmodel_context(appmodel, appmodel_abstracts)
  256. attributes = self.get_appmodel_attributes(appmodel)
  257. # find primary key and print it first, ignoring implicit id if other pk exists
  258. pk = appmodel._meta.pk
  259. if pk and not appmodel._meta.abstract and pk in attributes:
  260. model['fields'].append(self.add_attributes(pk, abstract_fields))
  261. for field in attributes:
  262. model = self.process_attributes(field, model, pk, abstract_fields)
  263. if self.sort_fields:
  264. model = self.sort_model_fields(model)
  265. for field in appmodel._meta.local_fields:
  266. model = self.process_local_fields(field, model, abstract_fields)
  267. for field in appmodel._meta.local_many_to_many:
  268. model = self.process_local_many_to_many(field, model)
  269. if self.inheritance:
  270. # add inheritance arrows
  271. for parent in appmodel.__bases__:
  272. model = self.process_parent(parent, appmodel, model)
  273. app_graph['models'].append(model)
  274. if app_graph['models']:
  275. self.graphs.append(app_graph)
  276. def process_local_fields(self, field, model, abstract_fields):
  277. newmodel = model.copy()
  278. if field.attname.endswith('_ptr_id') or field in abstract_fields or self.skip_field(field):
  279. # excluding field redundant with inheritance relation
  280. # excluding fields inherited from abstract classes. they too show as local_fields
  281. return newmodel
  282. if isinstance(field, OneToOneField):
  283. relation = self.add_relation(
  284. field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]'
  285. )
  286. elif isinstance(field, ForeignKey):
  287. relation = self.add_relation(
  288. field,
  289. newmodel,
  290. '[arrowhead=none, arrowtail={}, dir=both]'.format(
  291. self.arrow_shape
  292. ),
  293. )
  294. else:
  295. relation = None
  296. if relation is not None:
  297. newmodel['relations'].append(relation)
  298. return newmodel
  299. def process_local_many_to_many(self, field, model):
  300. newmodel = model.copy()
  301. if self.skip_field(field):
  302. return newmodel
  303. relation = None
  304. if isinstance(field, ManyToManyField):
  305. if hasattr(field.remote_field.through, '_meta') and field.remote_field.through._meta.auto_created:
  306. relation = self.add_relation(
  307. field,
  308. newmodel,
  309. '[arrowhead={} arrowtail={}, dir=both]'.format(
  310. self.arrow_shape, self.arrow_shape
  311. ),
  312. )
  313. elif isinstance(field, GenericRelation):
  314. relation = self.add_relation(field, newmodel, mark_safe('[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]'))
  315. if relation is not None:
  316. newmodel['relations'].append(relation)
  317. return newmodel
  318. def process_parent(self, parent, appmodel, model):
  319. newmodel = model.copy()
  320. if hasattr(parent, "_meta"): # parent is a model
  321. _rel = self.get_inheritance_context(appmodel, parent)
  322. # TODO: seems as if abstract models aren't part of models.getModels, which is why they are printed by this without any attributes.
  323. if _rel not in newmodel['relations'] and self.use_model(_rel['target']):
  324. newmodel['relations'].append(_rel)
  325. return newmodel
  326. def sort_model_fields(self, model):
  327. newmodel = model.copy()
  328. newmodel['fields'] = sorted(newmodel['fields'], key=lambda field: (not field['primary_key'], not field['relation'], field['label']))
  329. return newmodel
  330. def use_model(self, model_name):
  331. """
  332. Decide whether to use a model, based on the model name and the lists of
  333. models to exclude and include.
  334. """
  335. # Check against include list.
  336. if self.include_models:
  337. for model_pattern in self.include_models:
  338. model_pattern = '^%s$' % model_pattern.replace('*', '.*')
  339. if re.search(model_pattern, model_name):
  340. return True
  341. # Check against exclude list.
  342. if self.exclude_models:
  343. for model_pattern in self.exclude_models:
  344. model_pattern = '^%s$' % model_pattern.replace('*', '.*')
  345. if re.search(model_pattern, model_name):
  346. return False
  347. # Return `True` if `include_models` is falsey, otherwise return `False`.
  348. return not self.include_models
  349. def skip_field(self, field):
  350. if self.exclude_columns:
  351. if self.verbose_names and field.verbose_name:
  352. if field.verbose_name in self.exclude_columns:
  353. return True
  354. if field.name in self.exclude_columns:
  355. return True
  356. return False
  357. def generate_dot(graph_data, template='django_extensions/graph_models/digraph.dot'):
  358. if isinstance(template, six.string_types):
  359. template = loader.get_template(template)
  360. if not isinstance(template, Template) and not (hasattr(template, 'template') and isinstance(template.template, Template)):
  361. raise Exception("Default Django template loader isn't used. "
  362. "This can lead to the incorrect template rendering. "
  363. "Please, check the settings.")
  364. c = Context(graph_data).flatten()
  365. dot = template.render(c)
  366. return dot
  367. def generate_graph_data(*args, **kwargs):
  368. generator = ModelGraph(*args, **kwargs)
  369. generator.generate_graph_data()
  370. return generator.get_graph_data()
  371. def use_model(model, include_models, exclude_models):
  372. generator = ModelGraph([], include_models=include_models, exclude_models=exclude_models)
  373. return generator.use_model(model)