diff --git a/CHANGELOG.md b/CHANGELOG.md index f26971c4f..d3ad6aec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ Changelog See https://github.com/django-extensions/django-extensions/releases +4.1 +--- + +Changes: + +- Add: show_permissions command (#1920) +- Improvement: graph_models, style per app (#1848) +- Fix: JSONField, bulk_update's (#1924) + 4.0 --- diff --git a/django_extensions/__init__.py b/django_extensions/__init__.py index a9cdeb55d..87cef4971 100644 --- a/django_extensions/__init__.py +++ b/django_extensions/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from django.utils.version import get_version -VERSION = (4, 0, 0, "final", 0) +VERSION = (4, 1, 0, "final", 0) __version__ = get_version(VERSION) diff --git a/django_extensions/db/fields/json.py b/django_extensions/db/fields/json.py index 42c114e67..435a8de8e 100644 --- a/django_extensions/db/fields/json.py +++ b/django_extensions/db/fields/json.py @@ -15,6 +15,7 @@ class LOL(models.Model): from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models import expressions def dumps(value): @@ -94,12 +95,18 @@ def get_db_prep_save(self, value, connection, **kwargs): """Convert our JSON object to a string before we save""" if value is None and self.null: return None + # default values come in as strings; only non-strings should be # run through `dumps` - if not isinstance(value, str): + if ( + not isinstance(value, str) + # https://github.com/django-extensions/django-extensions/issues/1924 + # https://code.djangoproject.com/ticket/35167 + and not isinstance(value, expressions.Expression) + ): value = dumps(value) - return value + return super().get_db_prep_save(value, connection) def deconstruct(self): name, path, args, kwargs = super().deconstruct() diff --git a/django_extensions/management/commands/graph_models.py b/django_extensions/management/commands/graph_models.py index ca3b7807d..66610dde4 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/management/commands/show_permissions.py b/django_extensions/management/commands/show_permissions.py new file mode 100644 index 000000000..d1eed7195 --- /dev/null +++ b/django_extensions/management/commands/show_permissions.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = ( + "List all permissions for models. " + "By default, excludes admin, auth, contenttypes, and sessions apps." + ) + + def add_arguments(self, parser): + parser.add_argument( + "app_label_model", + nargs="*", + help="[app_label.]model(s) to show permissions for.", + ) + parser.add_argument( + "--all", + action="store_true", + help="Include results for admin, auth, contenttypes, and sessions.", + ) + parser.add_argument("--app-label", help="App label to dump permissions for.") + + def handle(self, *args, **options): + app_label_models = options["app_label_model"] + include_all = options["all"] + app_label_filter = options["app_label"] + + if include_all: + content_types = ContentType.objects.order_by("app_label", "model") + elif app_label_filter: + content_types = ContentType.objects.filter( + app_label=app_label_filter.lower() + ).order_by("app_label", "model") + if not content_types: + raise CommandError( + f'No content types found for app label "{app_label_filter}".' + ) + elif not app_label_models: + excluded = ["admin", "auth", "contenttypes", "sessions"] + content_types = ContentType.objects.exclude( + app_label__in=excluded + ).order_by("app_label", "model") + else: + content_types = [] + for value in app_label_models: + if "." in value: + app_label, model = value.split(".") + qs = ContentType.objects.filter(app_label=app_label, model=model) + else: + qs = ContentType.objects.filter(model=value) + + if not qs: + raise CommandError(f"Content type not found for '{value}'.") + content_types.extend(qs) + + for ct in content_types: + self.stdout.write(f"Permissions for {ct}") + for perm in ct.permission_set.all(): + self.stdout.write(f" {ct.app_label}.{perm.codename} | {perm.name}") 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=< + + +
+ + {{ graph.app_name }} + +
+ > + color=olivedrab4 + style="rounded"{% endif %} +{% indentby 2 if use_subgraph %}{% for model in graph.models %} + {{ model.app_name }}_{{ model.name }} [label=< + + + {% if not disable_fields %}{% for field in model.fields %} + {% if disable_abstract_fields and field.abstract %} + {% else %} + + {% endif %} + {% endfor %}{% endif %} +
+ + {{ model.label }}{% if model.abstracts %}
<{{ model.abstracts|join:"," }}>{% endif %} +
+ {% if field.abstract %}{% endif %}{% if field.relation or field.primary_key %}{% endif %}{{ field.label }}{% if field.relation or field.primary_key %}{% endif %}{% if field.abstract %}{% endif %} + + {% if field.abstract %}{% endif %}{% if field.relation or field.primary_key %}{% endif %}{{ field.type }}{% if field.relation or field.primary_key %}{% endif %}{% if field.abstract %}{% endif %} +
+ >] +{% 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=< + + +
+ {{ relation.target }} +
+ >]{% endif %} + {{ model.app_name }}_{{ model.name }} -> {{ relation.target_app }}_{{ relation.target }} + [label=" {{ relation.label }}"] {{ relation.arrows }}; +{% endfor %}{% endfor %} diff --git a/docs/show_permissions.rst b/docs/show_permissions.rst new file mode 100644 index 000000000..7bdfae5d2 --- /dev/null +++ b/docs/show_permissions.rst @@ -0,0 +1,78 @@ +Show Permissions +================ + +:synopsis: Show all permissions for Django models + +Introduction +------------ + +The ``show_permissions`` management command lists all permissions for the models in your Django project. +By default, it excludes built-in Django apps such as ``admin``, ``auth``, ``contenttypes``, and ``sessions``. + +This command is useful to quickly inspect the permissions assigned to models, especially when customizing permission logic or managing role-based access. + +Basic Usage +----------- + +.. code-block:: bash + + python manage.py show_permissions + +This will output the list of permissions for models in all installed apps **excluding** built-in Django apps. + +Examples +-------- + +Show permissions for specific apps and models: + +.. code-block:: bash + + python manage.py show_permissions blog + python manage.py show_permissions blog.Post + +Show permissions including built-in Django apps: + +.. code-block:: bash + + python manage.py show_permissions --all + +Show permissions for only a specific app using the `--app-label` option: + +.. code-block:: bash + + python manage.py show_permissions --app-label blog + +Options +------- + +* ``--all`` + Include permissions for Django’s built-in apps (``admin``, ``auth``, ``contenttypes``, ``sessions``). + +* ``--app-label