Skip to content

Commit

Permalink
Merge pull request django-extensions#91 from filipefigcorreia/umlish-…
Browse files Browse the repository at this point in the history
…graphmodels

UMLish graphmodels
  • Loading branch information
trbs committed Jan 24, 2012
2 parents cccd84c + 8bae88a commit 4da5513
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 30 deletions.
4 changes: 3 additions & 1 deletion django_extensions/management/commands/graph_models.py
Expand Up @@ -12,7 +12,7 @@ class Command(BaseCommand):
make_option('--all-applications', '-a', action='store_true', dest='all_applications',
help='Automatically include all applications from INSTALLED_APPS'),
make_option('--output', '-o', action='store', dest='outputfile',
help='Render output file. Type of output dependent on file extensions. Use png or jpg to render graph to image.'),
help='Render output file. Type of output dependend on file extensions. Use png or jpg to render graph to image.'),
make_option('--layout', '-l', action='store', dest='layout', default='dot',
help='Layout to be used by GraphViz for visualization. Layouts: circo dot fdp neato nop nop1 nop2 twopi'),
make_option('--verbose-names', '-n', action='store_true', dest='verbose_names',
Expand All @@ -23,6 +23,8 @@ class Command(BaseCommand):
help='Exclude specific column(s) from the graph. Can also load exclude list from file.'),
make_option('--exclude-models', '-X', action='store', dest='exclude_models',
help='Exclude specific model(s) from the graph. Can also load exclude list from file.'),
make_option('--inheritance', '-e', action='store_true', dest='inheritance',
help='Include inheritance arrows'),
)

help = ("Creates a GraphViz dot file for the specified app names. You can pass multiple app names and they will all be combined into a single model. Output is usually directed to a dot file.")
Expand Down
112 changes: 83 additions & 29 deletions django_extensions/management/modelviz.py
Expand Up @@ -37,6 +37,9 @@
-X, --exclude_models
exclude specific model(s) from the graph.
-e, --inheritance
show inheritance arrows.
"""
__version__ = "0.9"
__svnid__ = "$Id$"
Expand All @@ -50,6 +53,7 @@
"Justin Findlay <jfindlay@gmail.com>",
"Alexander Houben <alexander@houben.ch>",
"Bas van Oostveen <v.oostveen@gmail.com>",
"Joern Hees <gitdev@joernhees.de>"
]

import os
Expand All @@ -71,14 +75,13 @@
from django.db import models
from django.db.models import get_models
from django.db.models.fields.related import \
ForeignKey, OneToOneField, ManyToManyField
ForeignKey, OneToOneField, ManyToManyField, RelatedField

try:
from django.db.models.fields.generic import GenericRelation
except ImportError:
from django.contrib.contenttypes.generic import GenericRelation


def parse_file_or_list(arg):
if not arg:
return []
Expand All @@ -93,6 +96,7 @@ def generate_dot(app_labels, **kwargs):
all_applications = kwargs.get('all_applications', False)
use_subgraph = kwargs.get('group_models', False)
verbose_names = kwargs.get('verbose_names', False)
inheritance = kwargs.get('inheritance', False)
language = kwargs.get('language', None)
if language is not None:
activate_language(language)
Expand Down Expand Up @@ -135,8 +139,16 @@ def skip_field(field):
'models': []
})

for appmodel in get_models(app):
abstracts = [e.__name__ for e in appmodel.__bases__ if hasattr(e, '_meta') and e._meta.abstract]
appmodels = get_models(app)
abstract_models = []
for appmodel in appmodels:
abstract_models = abstract_models + [abstract_model for abstract_model in appmodel.__bases__ if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract]
abstract_models = list(set(abstract_models)) # remove duplicates
appmodels = abstract_models + appmodels


for appmodel in appmodels:
appmodel_abstracts = [abstract_model.__name__ for abstract_model in appmodel.__bases__ if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract]

# collect all attribs of abstract superclasses
def getBasesAbstractFields(c):
Expand All @@ -151,7 +163,7 @@ def getBasesAbstractFields(c):
model = {
'app_name': appmodel.__module__.replace(".", "_"),
'name': appmodel.__name__,
'abstracts': abstracts,
'abstracts': appmodel_abstracts,
'fields': [],
'relations': []
}
Expand All @@ -177,24 +189,38 @@ def add_attributes(field):
else:
label = field.name

t = type(field).__name__
if isinstance(field, (OneToOneField, ForeignKey)):
t += " ({0})".format(field.rel.field_name)
# TODO: ManyToManyField, GenericRelation

model['fields'].append({
'name': field.name,
'label': label,
'type': type(field).__name__,
'type': t,
'blank': field.blank,
'abstract': field in abstract_fields,
})

for field in appmodel._meta.fields:
# Find all the real attributes. Relations are depicted as graph edges instead of attributes
attributes = [field for field in appmodel._meta.local_fields if not isinstance(field, RelatedField)]

# find primary key and print it first, ignoring implicit id if other pk exists
pk = appmodel._meta.pk
if not appmodel._meta.abstract and pk in attributes:
add_attributes(pk)
for field in attributes:
if skip_field(field):
continue
add_attributes(field)

if appmodel._meta.many_to_many:
for field in appmodel._meta.many_to_many:
if skip_field(field):
continue
if not field.primary_key:
add_attributes(field)

# FIXME: actually many_to_many fields aren't saved in this model's db table, so why should we add an attribute-line for them in the resulting graph?
#if appmodel._meta.many_to_many:
# for field in appmodel._meta.many_to_many:
# if skip_field(field):
# continue
# add_attributes(field)

# relations
def add_relation(field, extras=""):
Expand All @@ -219,24 +245,51 @@ def add_relation(field, extras=""):
if _rel not in model['relations'] and consider(_rel['target']):
model['relations'].append(_rel)

for field in appmodel._meta.fields:
for field in appmodel._meta.local_fields:
if field.attname.endswith('_ptr_id'): # excluding field redundant with inheritance relation
continue
if field in abstract_fields: # excluding fields inherited from abstract classes. they too show as local_fields
continue
if skip_field(field):
continue
if isinstance(field, OneToOneField):
add_relation(field, '[arrowhead=none arrowtail=none]')
add_relation(field, '[arrowhead=none, arrowtail=none]')
elif isinstance(field, ForeignKey):
add_relation(field)

if appmodel._meta.many_to_many:
for field in appmodel._meta.many_to_many:
if skip_field(field):
continue
if isinstance(field, ManyToManyField):
if (getattr(field, 'creates_table', False) or # django 1.1.
(hasattr(field.rel.through, '_meta') and field.rel.through._meta.auto_created)): # django 1.2
add_relation(field, '[arrowhead=normal arrowtail=normal]')
add_relation(field, '[arrowhead=none, arrowtail=dot]')

for field in appmodel._meta.local_many_to_many:
if skip_field(field):
continue
if isinstance(field, ManyToManyField):
if (getattr(field, 'creates_table', False) or # django 1.1.
(hasattr(field.rel.through, '_meta') and field.rel.through._meta.auto_created)): # django 1.2
add_relation(field, '[arrowhead=dot arrowtail=dot, dir=both]')
elif isinstance(field, GenericRelation):
add_relation(field, mark_safe('[style="dotted"] [arrowhead=normal arrowtail=normal]'))
add_relation(field, mark_safe('[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]'))

if inheritance:
# add inheritance arrows
for parent in appmodel.__bases__:
if hasattr(parent, "_meta"): # parent is a model
l = "multi-table"
if parent._meta.abstract:
l = "abstract"
if appmodel._meta.proxy:
l = "proxy"
l += r"\ninheritance"
_rel = {
'target_app': parent.__module__.replace(".", "_"),
'target': parent.__name__,
'type': "inheritance",
'name': "inheritance",
'label': l,
'arrows': '[arrowhead=empty, arrowtail=none]',
'needs_node': True
}
# TODO: seems as if abstract models aren't part of models.getModels, which is why they are printed by this without any attributes.
if _rel not in model['relations'] and consider(_rel['target']):
model['relations'].append(_rel)

graph['models'].append(model)
graphs.append(graph)

Expand Down Expand Up @@ -264,11 +317,10 @@ def add_relation(field, extras=""):
dot += '\n' + t.render(c)
return dot


def main():
try:
opts, args = getopt.getopt(sys.argv[1:], "hadgi:L:x:X:",
["help", "all_applications", "disable_fields", "group_models", "include_models=", "verbose_names", "language=", "exclude_columns=", "exclude_models="])
opts, args = getopt.getopt(sys.argv[1:], "hadgi:L:x:X:en",
["help", "all_applications", "disable_fields", "group_models", "include_models=", "inheritance", "verbose_names", "language=", "exclude_columns=", "exclude_models="])
except getopt.GetoptError, error:
print __doc__
sys.exit(error)
Expand All @@ -286,6 +338,8 @@ def main():
kwargs['group_models'] = True
if opt in ("-i", "--include_models"):
kwargs['include_models'] = arg
if opt in ("-e", "--inheritance"):
kwargs['inheritance'] = True
if opt in ("-n", "--verbose-names"):
kwargs['verbose_names'] = True
if opt in ("-L", "--language"):
Expand Down

0 comments on commit 4da5513

Please sign in to comment.