modelviz.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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:
  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. # We need to remove the model and field class because it is not JSON serializable
  101. graphs = [context.flatten() for context in self.graphs]
  102. for context in graphs:
  103. for model_data in context['models']:
  104. model_data.pop('model')
  105. for field_data in model_data['fields']:
  106. field_data.pop('field')
  107. graph_data['graphs'] = graphs
  108. else:
  109. graph_data['graphs'] = self.graphs
  110. return graph_data
  111. def add_attributes(self, field, abstract_fields):
  112. if self.verbose_names and field.verbose_name:
  113. label = force_str(field.verbose_name)
  114. if label.islower():
  115. label = label.capitalize()
  116. else:
  117. label = field.name
  118. t = type(field).__name__
  119. if isinstance(field, (OneToOneField, ForeignKey)):
  120. t += " ({0})".format(field.remote_field.field_name)
  121. # TODO: ManyToManyField, GenericRelation
  122. return {
  123. 'field': field,
  124. 'name': field.name,
  125. 'label': label,
  126. 'type': t,
  127. 'blank': field.blank,
  128. 'abstract': field in abstract_fields,
  129. 'relation': isinstance(field, RelatedField),
  130. 'primary_key': field.primary_key,
  131. }
  132. def add_relation(self, field, model, extras=""):
  133. if self.verbose_names and field.verbose_name:
  134. label = force_str(field.verbose_name)
  135. if label.islower():
  136. label = label.capitalize()
  137. else:
  138. label = field.name
  139. # show related field name
  140. if hasattr(field, 'related_query_name'):
  141. related_query_name = field.related_query_name()
  142. if self.verbose_names and related_query_name.islower():
  143. related_query_name = related_query_name.replace('_', ' ').capitalize()
  144. label = u'{} ({})'.format(label, force_str(related_query_name))
  145. if self.hide_edge_labels:
  146. label = ''
  147. # handle self-relationships and lazy-relationships
  148. if isinstance(field.remote_field.model, six.string_types):
  149. if field.remote_field.model == 'self':
  150. target_model = field.model
  151. else:
  152. if '.' in field.remote_field.model:
  153. app_label, model_name = field.remote_field.model.split('.', 1)
  154. else:
  155. app_label = field.model._meta.app_label
  156. model_name = field.remote_field.model
  157. target_model = apps.get_model(app_label, model_name)
  158. else:
  159. target_model = field.remote_field.model
  160. _rel = self.get_relation_context(target_model, field, label, extras)
  161. if _rel not in model['relations'] and self.use_model(_rel['target']):
  162. return _rel
  163. def get_abstract_models(self, appmodels):
  164. abstract_models = []
  165. for appmodel in appmodels:
  166. abstract_models += [
  167. abstract_model for abstract_model in appmodel.__bases__
  168. if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract
  169. ]
  170. abstract_models = list(set(abstract_models)) # remove duplicates
  171. return abstract_models
  172. def get_app_context(self, app):
  173. return Context({
  174. 'name': '"%s"' % app.name,
  175. 'app_name': "%s" % app.name,
  176. 'cluster_app_name': "cluster_%s" % app.name.replace(".", "_"),
  177. 'models': []
  178. })
  179. def get_appmodel_attributes(self, appmodel):
  180. if self.relations_as_fields:
  181. attributes = [field for field in appmodel._meta.local_fields]
  182. else:
  183. # Find all the 'real' attributes. Relations are depicted as graph edges instead of attributes
  184. attributes = [field for field in appmodel._meta.local_fields if not
  185. isinstance(field, RelatedField)]
  186. return attributes
  187. def get_appmodel_abstracts(self, appmodel):
  188. return [
  189. abstract_model.__name__ for abstract_model in appmodel.__bases__
  190. if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract
  191. ]
  192. def get_appmodel_context(self, appmodel, appmodel_abstracts):
  193. context = {
  194. 'model': appmodel,
  195. 'app_name': appmodel.__module__.replace(".", "_"),
  196. 'name': appmodel.__name__,
  197. 'abstracts': appmodel_abstracts,
  198. 'fields': [],
  199. 'relations': []
  200. }
  201. if self.verbose_names and appmodel._meta.verbose_name:
  202. context['label'] = force_str(appmodel._meta.verbose_name)
  203. else:
  204. context['label'] = context['name']
  205. return context
  206. def get_bases_abstract_fields(self, c):
  207. _abstract_fields = []
  208. for e in c.__bases__:
  209. if hasattr(e, '_meta') and e._meta.abstract:
  210. _abstract_fields.extend(e._meta.fields)
  211. _abstract_fields.extend(self.get_bases_abstract_fields(e))
  212. return _abstract_fields
  213. def get_inheritance_context(self, appmodel, parent):
  214. label = "multi-table"
  215. if parent._meta.abstract:
  216. label = "abstract"
  217. if appmodel._meta.proxy:
  218. label = "proxy"
  219. label += r"\ninheritance"
  220. if self.hide_edge_labels:
  221. label = ''
  222. return {
  223. 'target_app': parent.__module__.replace(".", "_"),
  224. 'target': parent.__name__,
  225. 'type': "inheritance",
  226. 'name': "inheritance",
  227. 'label': label,
  228. 'arrows': '[arrowhead=empty, arrowtail=none, dir=both]',
  229. 'needs_node': True,
  230. }
  231. def get_models(self, app):
  232. appmodels = list(app.get_models())
  233. return appmodels
  234. def get_relation_context(self, target_model, field, label, extras):
  235. return {
  236. 'target_app': target_model.__module__.replace('.', '_'),
  237. 'target': target_model.__name__,
  238. 'type': type(field).__name__,
  239. 'name': field.name,
  240. 'label': label,
  241. 'arrows': extras,
  242. 'needs_node': True
  243. }
  244. def process_attributes(self, field, model, pk, abstract_fields):
  245. newmodel = model.copy()
  246. if self.skip_field(field) or pk and field == pk:
  247. return newmodel
  248. newmodel['fields'].append(self.add_attributes(field, abstract_fields))
  249. return newmodel
  250. def process_apps(self):
  251. for app_label in self.app_labels:
  252. app = apps.get_app_config(app_label)
  253. if not app:
  254. continue
  255. app_graph = self.get_app_context(app)
  256. app_models = self.get_models(app)
  257. abstract_models = self.get_abstract_models(app_models)
  258. app_models = abstract_models + app_models
  259. for appmodel in app_models:
  260. if not self.use_model(appmodel._meta.object_name):
  261. continue
  262. appmodel_abstracts = self.get_appmodel_abstracts(appmodel)
  263. abstract_fields = self.get_bases_abstract_fields(appmodel)
  264. model = self.get_appmodel_context(appmodel, appmodel_abstracts)
  265. attributes = self.get_appmodel_attributes(appmodel)
  266. # find primary key and print it first, ignoring implicit id if other pk exists
  267. pk = appmodel._meta.pk
  268. if pk and not appmodel._meta.abstract and pk in attributes:
  269. model['fields'].append(self.add_attributes(pk, abstract_fields))
  270. for field in attributes:
  271. model = self.process_attributes(field, model, pk, abstract_fields)
  272. if self.sort_fields:
  273. model = self.sort_model_fields(model)
  274. for field in appmodel._meta.local_fields:
  275. model = self.process_local_fields(field, model, abstract_fields)
  276. for field in appmodel._meta.local_many_to_many:
  277. model = self.process_local_many_to_many(field, model)
  278. if self.inheritance:
  279. # add inheritance arrows
  280. for parent in appmodel.__bases__:
  281. model = self.process_parent(parent, appmodel, model)
  282. app_graph['models'].append(model)
  283. if app_graph['models']:
  284. self.graphs.append(app_graph)
  285. def process_local_fields(self, field, model, abstract_fields):
  286. newmodel = model.copy()
  287. if field.attname.endswith('_ptr_id') or field in abstract_fields or self.skip_field(field):
  288. # excluding field redundant with inheritance relation
  289. # excluding fields inherited from abstract classes. they too show as local_fields
  290. return newmodel
  291. if isinstance(field, OneToOneField):
  292. relation = self.add_relation(
  293. field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]'
  294. )
  295. elif isinstance(field, ForeignKey):
  296. relation = self.add_relation(
  297. field,
  298. newmodel,
  299. '[arrowhead=none, arrowtail={}, dir=both]'.format(
  300. self.arrow_shape
  301. ),
  302. )
  303. else:
  304. relation = None
  305. if relation is not None:
  306. newmodel['relations'].append(relation)
  307. return newmodel
  308. def process_local_many_to_many(self, field, model):
  309. newmodel = model.copy()
  310. if self.skip_field(field):
  311. return newmodel
  312. relation = None
  313. if isinstance(field, ManyToManyField):
  314. if hasattr(field.remote_field.through, '_meta') and field.remote_field.through._meta.auto_created:
  315. relation = self.add_relation(
  316. field,
  317. newmodel,
  318. '[arrowhead={} arrowtail={}, dir=both]'.format(
  319. self.arrow_shape, self.arrow_shape
  320. ),
  321. )
  322. elif isinstance(field, GenericRelation):
  323. relation = self.add_relation(field, newmodel, mark_safe('[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]'))
  324. if relation is not None:
  325. newmodel['relations'].append(relation)
  326. return newmodel
  327. def process_parent(self, parent, appmodel, model):
  328. newmodel = model.copy()
  329. if hasattr(parent, "_meta"): # parent is a model
  330. _rel = self.get_inheritance_context(appmodel, parent)
  331. # TODO: seems as if abstract models aren't part of models.getModels, which is why they are printed by this without any attributes.
  332. if _rel not in newmodel['relations'] and self.use_model(_rel['target']):
  333. newmodel['relations'].append(_rel)
  334. return newmodel
  335. def sort_model_fields(self, model):
  336. newmodel = model.copy()
  337. newmodel['fields'] = sorted(newmodel['fields'], key=lambda field: (not field['primary_key'], not field['relation'], field['label']))
  338. return newmodel
  339. def use_model(self, model_name):
  340. """
  341. Decide whether to use a model, based on the model name and the lists of
  342. models to exclude and include.
  343. """
  344. # Check against include list.
  345. if self.include_models:
  346. for model_pattern in self.include_models:
  347. model_pattern = '^%s$' % model_pattern.replace('*', '.*')
  348. if re.search(model_pattern, model_name):
  349. return True
  350. # Check against exclude list.
  351. if self.exclude_models:
  352. for model_pattern in self.exclude_models:
  353. model_pattern = '^%s$' % model_pattern.replace('*', '.*')
  354. if re.search(model_pattern, model_name):
  355. return False
  356. # Return `True` if `include_models` is falsey, otherwise return `False`.
  357. return not self.include_models
  358. def skip_field(self, field):
  359. if self.exclude_columns:
  360. if self.verbose_names and field.verbose_name:
  361. if field.verbose_name in self.exclude_columns:
  362. return True
  363. if field.name in self.exclude_columns:
  364. return True
  365. return False
  366. def generate_dot(graph_data, template='django_extensions/graph_models/digraph.dot'):
  367. if isinstance(template, six.string_types):
  368. template = loader.get_template(template)
  369. if not isinstance(template, Template) and not (hasattr(template, 'template') and isinstance(template.template, Template)):
  370. raise Exception("Default Django template loader isn't used. "
  371. "This can lead to the incorrect template rendering. "
  372. "Please, check the settings.")
  373. c = Context(graph_data).flatten()
  374. dot = template.render(c)
  375. return dot
  376. def generate_graph_data(*args, **kwargs):
  377. generator = ModelGraph(*args, **kwargs)
  378. generator.generate_graph_data()
  379. return generator.get_graph_data()
  380. def use_model(model, include_models, exclude_models):
  381. generator = ModelGraph([], include_models=include_models, exclude_models=exclude_models)
  382. return generator.use_model(model)