Repository: django-extensions/django-extensions
Pull request #1848: graph_models style per app
Author: login: ahmad88me
Labels: name: patch need improvement
Created: 2023-11-18T15:49:23Z | Merged: 2025-04-11T01:09:58Z

# Idea
Style the generated model per app. 

# use case
 Imagine you're working on a Django project composed of two distinct applications, namely 'app1' and 'app2'. These applications are interconnected, with models in each app having relationships not only within the same app but also across the two apps. To effectively visualise these intricate relationships, you're aiming to generate a comprehensive graph model. This graph will not only illustrate the internal model structures within each individual app but will also clearly depict the inter-app model relationships. The goal is to have a single, unified graphical representation that vividly differentiates the models of 'app1' and 'app2', while simultaneously showcasing the connections between them. Such a visual tool would be invaluable for understanding the complex architecture of your Django project at a glance.

# How to use it
You can either have a `.app-style.json` or you can specify the style file using the `--app-style` option. It takes a path to the style file. The style file is a json file which has the app name as the key and the style as the value. Here is a sample one:

```
{
  "app1": {"bg": "#341b56"},
  "django.contrib.auth": {"bg": "#561b4c"},
  "app2":{"bg": "#1b3956"}
}
```

Further, I updated the django style to support the background change. So in the future, this can easily be extended to more styles (e.g., color, font, ..., etc.)



Diff:
diff --git a/django_extensions/management/commands/graph_models.py b/django_extensions/management/commands/graph_models.py
index ca3b7807d..81bc230da 100644
--- a/django_extensions/management/commands/graph_models.py
+++ b/django_extensions/management/commands/graph_models.py
@@ -28,6 +28,28 @@
     HAS_PYDOT = False
 
 
+def retheme(graph_data, app_style={}):
+    if isinstance(app_style, str):
+        if os.path.exists(app_style):
+            try:
+                with open(app_style, 'rt') as f:
+                    app_style = json.load(f)
+            except Exception as e:
+                print(f"Invalid app style file {app_style}")
+                raise Exception(e)
+        else:
+            return graph_data
+
+    for gc in graph_data["graphs"]:
+        for g in gc:
+            if "name" in g:
+                for m in g["models"]:
+                    app_name = g['app_name']
+                    if app_name in app_style:
+                        m["style"] = app_style[app_name]
+    return graph_data
+
+
 class Command(BaseCommand):
     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"
@@ -47,6 +69,12 @@ def __init__(self, *args, **kwargs):
         --disable-fields can be set in settings.GRAPH_MODELS['disable_fields'].
         """
         self.arguments = {
+            "--app-style": {
+                "action": "store",
+                "help": "Path to style json to configure the style per app",
+                "dest": "app-style",
+                "default": ".app-style.json"
+            },
             "--pygraphviz": {
                 "action": "store_true",
                 "default": False,
@@ -344,6 +372,7 @@ def handle(self, *args, **options):
         )
         template = loader.get_template(template_name)
 
+        graph_data = retheme(graph_data, app_style=options['app-style'])
         dotdata = generate_dot(graph_data, template=template)
 
         if output == "pygraphviz":
diff --git a/django_extensions/templates/django_extensions/graph_models/django2018style/digraph.dot b/django_extensions/templates/django_extensions/graph_models/django2018style/digraph.dot
new file mode 100644
index 000000000..e2a330611
--- /dev/null
+++ b/django_extensions/templates/django_extensions/graph_models/django2018style/digraph.dot
@@ -0,0 +1,27 @@
+{% block digraph %}digraph model_graph {
+  // Dotfile by Django-Extensions graph_models
+  // Created: {{ created_at }}
+  {% if cli_options %}// Cli Options: {{ cli_options }}{% endif %}
+
+  {% block digraph_options %}fontname = "Roboto"
+  fontsize = 8
+  splines  = true
+  rankdir = "{{ rankdir }}"{% endblock %}
+
+  node [{% block node_options %}
+    fontname = "Roboto"
+    fontsize = 8
+    shape = "plaintext"
+  {% endblock %}]
+
+  edge [{% block edge_options %}
+    fontname = "Roboto"
+    fontsize = 8
+  {% endblock %}]
+
+  // Labels
+{% block labels %}{% for graph in graphs %}{% include "django_extensions/graph_models/django2018style/label.dot" %}{% endfor %}{% endblock %}
+
+  // Relations
+{% block relations %}{% for graph in graphs %}{% include "django_extensions/graph_models/django2018style/relation.dot" %}{% endfor %}{% endblock %}
+}{% endblock %}
diff --git a/django_extensions/templates/django_extensions/graph_models/django2018style/label.dot b/django_extensions/templates/django_extensions/graph_models/django2018style/label.dot
new file mode 100644
index 000000000..6949682e7
--- /dev/null
+++ b/django_extensions/templates/django_extensions/graph_models/django2018style/label.dot
@@ -0,0 +1,39 @@
+{% load indent_text %}{% if use_subgraph %}  subgraph {{ graph.cluster_app_name }} {
+    label=<
+          <TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
+          <TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER">
+          <FONT FACE="Roboto" COLOR="Black" POINT-SIZE="10">
+          <B>{{ graph.app_name }}</B>
+          </FONT>
+          </TD></TR>
+          </TABLE>
+          >
+    color=olivedrab4
+    style="rounded"{% endif %}
+{% indentby 2 if use_subgraph %}{% for model in graph.models %}
+  {{ model.app_name }}_{{ model.name }} [label=<
+    <TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
+    <TR><TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER"
+    {% if model.style.bg%}
+        BGCOLOR="{{model.style.bg}}"
+    {%else%}
+            BGCOLOR="#1b563f"
+    {%endif%}
+    >
+    <FONT FACE="Roboto" COLOR="white" POINT-SIZE="10"><B>
+    {{ model.label }}{% if model.abstracts %}<BR/>&lt;<FONT FACE="Roboto"><I>{{ model.abstracts|join:"," }}</I></FONT>&gt;{% endif %}
+    </B></FONT></TD></TR>
+  {% if not disable_fields %}{% for field in model.fields %}
+  {% if disable_abstract_fields and field.abstract %}
+  {% else %}
+    <TR><TD ALIGN="LEFT" BORDER="0">
+    <FONT {% if not field.primary_key and field.blank %}COLOR="#7B7B7B" {% endif %}FACE="Roboto">{% if field.abstract %}<I>{% endif %}{% if field.relation or field.primary_key %}<B>{% endif %}{{ field.label }}{% if field.relation or field.primary_key %}</B>{% endif %}{% if field.abstract %}</I>{% endif %}</FONT>
+    </TD><TD ALIGN="LEFT">
+    <FONT {% if not field.primary_key and field.blank %}COLOR="#7B7B7B" {% endif %}FACE="Roboto">{% if field.abstract %}<I>{% endif %}{% if field.relation or field.primary_key %}<B>{% endif %}{{ field.type }}{% if field.relation or field.primary_key %}</B>{% endif %}{% if field.abstract %}</I>{% endif %}</FONT>
+    </TD></TR>
+  {% endif %}
+  {% endfor %}{% endif %}
+    </TABLE>
+    >]
+{% endfor %}{% endindentby %}
+{% if use_subgraph %}  }{% endif %}
diff --git a/django_extensions/templates/django_extensions/graph_models/django2018style/relation.dot b/django_extensions/templates/django_extensions/graph_models/django2018style/relation.dot
new file mode 100644
index 000000000..c5ef3d731
--- /dev/null
+++ b/django_extensions/templates/django_extensions/graph_models/django2018style/relation.dot
@@ -0,0 +1,10 @@
+{% for model in graph.models %}{% for relation in model.relations %}{% if relation.needs_node %}  {{ relation.target_app }}_{{ relation.target }} [label=<
+  <TABLE BGCOLOR="white" BORDER="0" CELLBORDER="0" CELLSPACING="0">
+  <TR><TD COLSPAN="2" CELLPADDING="4" ALIGN="CENTER" BGCOLOR="#1b563f">
+  <FONT FACE="Roboto" POINT-SIZE="12" COLOR="white">{{ relation.target }}</FONT>
+  </TD></TR>
+  </TABLE>
+  >]{% endif %}
+  {{ model.app_name }}_{{ model.name }} -> {{ relation.target_app }}_{{ relation.target }}
+  [label=" {{ relation.label }}"] {{ relation.arrows }};
+{% endfor %}{% endfor %}


Reviews:
user: login: trbs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T11:38:32Z
user: login: ahmad88me
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T23:22:17Z

Review comments:
path: django_extensions/__init__.py
user: login: trbs
body: Could you please remove this from the PR ?
created_at: 2025-04-05T11:38:32Z
author_association: MEMBER
path: django_extensions/__init__.py
user: login: ahmad88me
body: Done
created_at: 2025-04-05T23:22:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: trbs
created_at: 2025-04-06T09:16:21Z
body: Thanks for the update. It looks like the PR now removes `django_extensions/__init__.py` entirely ?

And sorry with the move to `pyproject.toml` and `ruff` `graph_models.py` changed..
author_association: MEMBER
user: login: ahmad88me
created_at: 2025-04-06T14:57:19Z
body: @trbs no worries 😉  .. I added the `__init__` and resolved the conflicts.  
author_association: CONTRIBUTOR
user: login: ahmad88me
created_at: 2025-04-10T06:48:14Z
body: @trbs resolved the new conflicts and fix the lint 
author_association: CONTRIBUTOR
user: login: ulgens
created_at: 2025-04-11T11:25:49Z
body: @ahmad88me Is it possible to update https://django-extensions.readthedocs.io/en/latest/graph_models.html#graph-models too? Currently, there is no info in the docs about this new option.
author_association: CONTRIBUTOR
user: login: trbs
created_at: 2025-04-11T12:27:13Z
body: > @ahmad88me Is it possible to update https://django-extensions.readthedocs.io/en/latest/graph_models.html#graph-models too? Currently, there is no info in the docs about this new option.

good catch 😄  yes please submit a new PR for updated docs, thanks !
author_association: MEMBER