| 1 |
#!/usr/bin/env python |
|---|
| 2 |
"""Django model to DOT (Graphviz) converter |
|---|
| 3 |
by Antonio Cavedoni <antonio@cavedoni.org> |
|---|
| 4 |
|
|---|
| 5 |
Make sure your DJANGO_SETTINGS_MODULE is set to your project or |
|---|
| 6 |
place this script in the same directory of the project and call |
|---|
| 7 |
the script like this: |
|---|
| 8 |
|
|---|
| 9 |
$ python modelviz.py [-h] [-a] [-d] [-g] [-i <model_names>] <app_label> ... <app_label> > <filename>.dot |
|---|
| 10 |
$ dot <filename>.dot -Tpng -o <filename>.png |
|---|
| 11 |
|
|---|
| 12 |
options: |
|---|
| 13 |
-h, --help |
|---|
| 14 |
show this help message and exit. |
|---|
| 15 |
|
|---|
| 16 |
-a, --all_applications |
|---|
| 17 |
show models from all applications. |
|---|
| 18 |
|
|---|
| 19 |
-d, --disable_fields |
|---|
| 20 |
don't show the class member fields. |
|---|
| 21 |
|
|---|
| 22 |
-g, --group_models |
|---|
| 23 |
draw an enclosing box around models from the same app. |
|---|
| 24 |
|
|---|
| 25 |
-i, --include_models=User,Person,Car |
|---|
| 26 |
only include selected models in graph. |
|---|
| 27 |
""" |
|---|
| 28 |
__version__ = "0.9" |
|---|
| 29 |
__svnid__ = "$Id$" |
|---|
| 30 |
__license__ = "Python" |
|---|
| 31 |
__author__ = "Antonio Cavedoni <http://cavedoni.com/>" |
|---|
| 32 |
__contributors__ = [ |
|---|
| 33 |
"Stefano J. Attardi <http://attardi.org/>", |
|---|
| 34 |
"limodou <http://www.donews.net/limodou/>", |
|---|
| 35 |
"Carlo C8E Miron", |
|---|
| 36 |
"Andre Campos <cahenan@gmail.com>", |
|---|
| 37 |
"Justin Findlay <jfindlay@gmail.com>", |
|---|
| 38 |
"Alexander Houben <alexander@houben.ch>", |
|---|
| 39 |
"Bas van Oostveen <v.oostveen@gmail.com>", |
|---|
| 40 |
] |
|---|
| 41 |
|
|---|
| 42 |
import getopt, sys |
|---|
| 43 |
|
|---|
| 44 |
from django.core.management import setup_environ |
|---|
| 45 |
|
|---|
| 46 |
try: |
|---|
| 47 |
import settings |
|---|
| 48 |
except ImportError: |
|---|
| 49 |
pass |
|---|
| 50 |
else: |
|---|
| 51 |
setup_environ(settings) |
|---|
| 52 |
|
|---|
| 53 |
from django.template import Template, Context |
|---|
| 54 |
from django.db import models |
|---|
| 55 |
from django.db.models import get_models |
|---|
| 56 |
from django.db.models.fields.related import \ |
|---|
| 57 |
ForeignKey, OneToOneField, ManyToManyField |
|---|
| 58 |
|
|---|
| 59 |
try: |
|---|
| 60 |
from django.db.models.fields.generic import GenericRelation |
|---|
| 61 |
except ImportError: |
|---|
| 62 |
from django.contrib.contenttypes.generic import GenericRelation |
|---|
| 63 |
|
|---|
| 64 |
head_template = """ |
|---|
| 65 |
digraph name { |
|---|
| 66 |
fontname = "Helvetica" |
|---|
| 67 |
fontsize = 8 |
|---|
| 68 |
|
|---|
| 69 |
node [ |
|---|
| 70 |
fontname = "Helvetica" |
|---|
| 71 |
fontsize = 8 |
|---|
| 72 |
shape = "plaintext" |
|---|
| 73 |
] |
|---|
| 74 |
edge [ |
|---|
| 75 |
fontname = "Helvetica" |
|---|
| 76 |
fontsize = 8 |
|---|
| 77 |
] |
|---|
| 78 |
|
|---|
| 79 |
""" |
|---|
| 80 |
|
|---|
| 81 |
body_template = """ |
|---|
| 82 |
{% if use_subgraph %} |
|---|
| 83 |
subgraph {{ cluster_app_name }} { |
|---|
| 84 |
label=< |
|---|
| 85 |
<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0"> |
|---|
| 86 |
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" |
|---|
| 87 |
><FONT FACE="Helvetica Bold" COLOR="Black" POINT-SIZE="12" |
|---|
| 88 |
>{{ app_name }}</FONT></TD></TR> |
|---|
| 89 |
</TABLE> |
|---|
| 90 |
> |
|---|
| 91 |
color=olivedrab4 |
|---|
| 92 |
style="rounded" |
|---|
| 93 |
{% endif %} |
|---|
| 94 |
|
|---|
| 95 |
{% for model in models %} |
|---|
| 96 |
{{ model.name }} [label=< |
|---|
| 97 |
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0"> |
|---|
| 98 |
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4" |
|---|
| 99 |
><FONT FACE="Helvetica Bold" COLOR="white" |
|---|
| 100 |
>{{ model.name }}</FONT></TD></TR> |
|---|
| 101 |
|
|---|
| 102 |
{% if not disable_fields %} |
|---|
| 103 |
{% for field in model.fields %} |
|---|
| 104 |
<TR><TD ALIGN="LEFT" BORDER="0" |
|---|
| 105 |
><FONT {% if field.blank %}COLOR="#7B7B7B" {% endif %}FACE="Helvetica Bold">{{ field.name }}</FONT |
|---|
| 106 |
></TD> |
|---|
| 107 |
<TD ALIGN="LEFT" |
|---|
| 108 |
><FONT {% if field.blank %}COLOR="#7B7B7B" {% endif %}FACE="Helvetica Bold">{{ field.type }}</FONT |
|---|
| 109 |
></TD></TR> |
|---|
| 110 |
{% endfor %} |
|---|
| 111 |
{% endif %} |
|---|
| 112 |
</TABLE> |
|---|
| 113 |
>] |
|---|
| 114 |
{% endfor %} |
|---|
| 115 |
|
|---|
| 116 |
{% if use_subgraph %} |
|---|
| 117 |
} |
|---|
| 118 |
{% endif %} |
|---|
| 119 |
""" |
|---|
| 120 |
|
|---|
| 121 |
rel_template = """ |
|---|
| 122 |
{% for model in models %} |
|---|
| 123 |
{% for relation in model.relations %} |
|---|
| 124 |
{% if relation.needs_node %} |
|---|
| 125 |
{{ relation.target }} [label=< |
|---|
| 126 |
<TABLE BGCOLOR="palegoldenrod" BORDER="0" CELLBORDER="0" CELLSPACING="0"> |
|---|
| 127 |
<TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="olivedrab4" |
|---|
| 128 |
><FONT FACE="Helvetica Bold" COLOR="white" |
|---|
| 129 |
>{{ relation.target }}</FONT></TD></TR> |
|---|
| 130 |
</TABLE> |
|---|
| 131 |
>] |
|---|
| 132 |
{% endif %} |
|---|
| 133 |
{{ model.name }} -> {{ relation.target }} |
|---|
| 134 |
[label="{{ relation.name }}"] {{ relation.arrows }}; |
|---|
| 135 |
{% endfor %} |
|---|
| 136 |
{% endfor %} |
|---|
| 137 |
""" |
|---|
| 138 |
|
|---|
| 139 |
tail_template = """ |
|---|
| 140 |
} |
|---|
| 141 |
""" |
|---|
| 142 |
|
|---|
| 143 |
def generate_dot(app_labels, **kwargs): |
|---|
| 144 |
disable_fields = kwargs.get('disable_fields', False) |
|---|
| 145 |
include_models = kwargs.get('include_models', []) |
|---|
| 146 |
all_applications = kwargs.get('all_applications', False) |
|---|
| 147 |
use_subgraph = kwargs.get('group_models', False) |
|---|
| 148 |
|
|---|
| 149 |
dot = head_template |
|---|
| 150 |
|
|---|
| 151 |
apps = [] |
|---|
| 152 |
if all_applications: |
|---|
| 153 |
apps = models.get_apps() |
|---|
| 154 |
|
|---|
| 155 |
for app_label in app_labels: |
|---|
| 156 |
app = models.get_app(app_label) |
|---|
| 157 |
if not app in apps: |
|---|
| 158 |
apps.append(app) |
|---|
| 159 |
|
|---|
| 160 |
graphs = [] |
|---|
| 161 |
for app in apps: |
|---|
| 162 |
graph = Context({ |
|---|
| 163 |
'name': '"%s"' % app.__name__, |
|---|
| 164 |
'app_name': "%s" % app.__name__.rsplit('.', 1)[0], |
|---|
| 165 |
'cluster_app_name': "cluster_%s" % app.__name__.replace(".", "_"), |
|---|
| 166 |
'disable_fields': disable_fields, |
|---|
| 167 |
'use_subgraph': use_subgraph, |
|---|
| 168 |
'models': [] |
|---|
| 169 |
}) |
|---|
| 170 |
|
|---|
| 171 |
for appmodel in get_models(app): |
|---|
| 172 |
model = { |
|---|
| 173 |
'name': appmodel.__name__, |
|---|
| 174 |
'fields': [], |
|---|
| 175 |
'relations': [] |
|---|
| 176 |
} |
|---|
| 177 |
|
|---|
| 178 |
# consider given model name ? |
|---|
| 179 |
def consider(model_name): |
|---|
| 180 |
return not include_models or model_name in include_models |
|---|
| 181 |
|
|---|
| 182 |
if not consider(appmodel._meta.object_name): |
|---|
| 183 |
continue |
|---|
| 184 |
|
|---|
| 185 |
# model attributes |
|---|
| 186 |
def add_attributes(): |
|---|
| 187 |
model['fields'].append({ |
|---|
| 188 |
'name': field.name, |
|---|
| 189 |
'type': type(field).__name__, |
|---|
| 190 |
'blank': field.blank |
|---|
| 191 |
}) |
|---|
| 192 |
|
|---|
| 193 |
for field in appmodel._meta.fields: |
|---|
| 194 |
add_attributes() |
|---|
| 195 |
|
|---|
| 196 |
if appmodel._meta.many_to_many: |
|---|
| 197 |
for field in appmodel._meta.many_to_many: |
|---|
| 198 |
add_attributes() |
|---|
| 199 |
|
|---|
| 200 |
# relations |
|---|
| 201 |
def add_relation(extras=""): |
|---|
| 202 |
_rel = { |
|---|
| 203 |
'target': field.rel.to.__name__, |
|---|
| 204 |
'type': type(field).__name__, |
|---|
| 205 |
'name': field.name, |
|---|
| 206 |
'arrows': extras, |
|---|
| 207 |
'needs_node': True |
|---|
| 208 |
} |
|---|
| 209 |
if _rel not in model['relations'] and consider(_rel['target']): |
|---|
| 210 |
model['relations'].append(_rel) |
|---|
| 211 |
|
|---|
| 212 |
for field in appmodel._meta.fields: |
|---|
| 213 |
if isinstance(field, ForeignKey): |
|---|
| 214 |
add_relation() |
|---|
| 215 |
elif isinstance(field, OneToOneField): |
|---|
| 216 |
add_relation('[arrowhead=none arrowtail=none]') |
|---|
| 217 |
|
|---|
| 218 |
if appmodel._meta.many_to_many: |
|---|
| 219 |
for field in appmodel._meta.many_to_many: |
|---|
| 220 |
if isinstance(field, ManyToManyField): |
|---|
| 221 |
add_relation('[arrowhead=normal arrowtail=normal]') |
|---|
| 222 |
elif isinstance(field, GenericRelation): |
|---|
| 223 |
add_relation('[style="dotted"] [arrowhead=normal arrowtail=normal]') |
|---|
| 224 |
graph['models'].append(model) |
|---|
| 225 |
graphs.append(graph) |
|---|
| 226 |
|
|---|
| 227 |
nodes = [] |
|---|
| 228 |
for graph in graphs: |
|---|
| 229 |
nodes.extend([e['name'] for e in graph['models']]) |
|---|
| 230 |
|
|---|
| 231 |
for graph in graphs: |
|---|
| 232 |
# don't draw duplication nodes because of relations |
|---|
| 233 |
for model in graph['models']: |
|---|
| 234 |
for relation in model['relations']: |
|---|
| 235 |
if relation['target'] in nodes: |
|---|
| 236 |
relation['needs_node'] = False |
|---|
| 237 |
# render templates |
|---|
| 238 |
t = Template(body_template) |
|---|
| 239 |
dot += '\n' + t.render(graph) |
|---|
| 240 |
|
|---|
| 241 |
for graph in graphs: |
|---|
| 242 |
t = Template(rel_template) |
|---|
| 243 |
dot += '\n' + t.render(graph) |
|---|
| 244 |
|
|---|
| 245 |
dot += '\n' + tail_template |
|---|
| 246 |
|
|---|
| 247 |
return dot |
|---|
| 248 |
|
|---|
| 249 |
def main(): |
|---|
| 250 |
try: |
|---|
| 251 |
opts, args = getopt.getopt(sys.argv[1:], "hadgi:", |
|---|
| 252 |
["help", "all_applications", "disable_fields", "group_models", "include_models="]) |
|---|
| 253 |
except getopt.GetoptError, error: |
|---|
| 254 |
print __doc__ |
|---|
| 255 |
sys.exit(error) |
|---|
| 256 |
|
|---|
| 257 |
kwargs = {} |
|---|
| 258 |
for opt, arg in opts: |
|---|
| 259 |
if opt in ("-h", "--help"): |
|---|
| 260 |
print __doc__ |
|---|
| 261 |
sys.exit() |
|---|
| 262 |
if opt in ("-a", "--all_applications"): |
|---|
| 263 |
kwargs['all_applications'] = True |
|---|
| 264 |
if opt in ("-d", "--disable_fields"): |
|---|
| 265 |
kwargs['disable_fields'] = True |
|---|
| 266 |
if opt in ("-g", "--group_models"): |
|---|
| 267 |
kwargs['group_models'] = True |
|---|
| 268 |
if opt in ("-i", "--include_models"): |
|---|
| 269 |
kwargs['include_models'] = arg.split(',') |
|---|
| 270 |
|
|---|
| 271 |
if not args and not kwargs.get('all_applications', False): |
|---|
| 272 |
print __doc__ |
|---|
| 273 |
sys.exit() |
|---|
| 274 |
|
|---|
| 275 |
print generate_dot(args, **kwargs) |
|---|
| 276 |
|
|---|
| 277 |
if __name__ == "__main__": |
|---|
| 278 |
main() |
|---|