Add function to get list of (sandbox approved) attributes from classes
authorMagnus Hagander <magnus@hagander.net>
Wed, 21 Aug 2024 11:35:00 +0000 (13:35 +0200)
committerMagnus Hagander <magnus@hagander.net>
Wed, 21 Aug 2024 11:51:03 +0000 (13:51 +0200)
postgresqleu/confreg/jinjafunc.py

index cd184eaa86fbb4cef3cfc2c5693b57d59ca4a415..7472d84bde697b16c55dc3dfe7c1e97cf07a5bf9 100644 (file)
@@ -7,6 +7,7 @@ from django.utils.text import slugify
 from django.utils.timesince import timesince
 from django.utils import timezone
 from django.conf import settings
+import django.db.models
 
 import os.path
 import random
@@ -176,6 +177,40 @@ class ConfSandbox(jinja2.sandbox.SandboxedEnvironment):
         return super(ConfSandbox, self).is_safe_attribute(obj, attr, value)
 
 
+# Enumerate all available attributes (in the postgresqleu scope), showing their
+# availability.
+def get_all_available_attributes(objclass, depth=0):
+    modname = objclass.__module__
+    if not (modname.startswith('postgresqleu.') and modname.endswith('models')):
+        # Outside of models, we also specifically allow the InvoicePresentationWrapper
+        if modname != 'postgresqleu.invoices.util' or obj.__class__.__name__ != 'InvoicePresentationWrapper':
+            return
+
+    for attname, attref in objclass.__dict__.items():
+        def _is_visible():
+            # Implement the same rules as above, because reusing the sandbox is painful as it
+            # works with objects and not models.
+            if attname in getattr(objclass, '_unsafe_attributes', []):
+                return False
+            if hasattr(objclass, '_safe_attributes'):
+                return attname in getattr(objclass, '_safe_attributes')
+            # If neither safe nor unsafe is specified, we only allow access if the model has
+            # a conference field specified.
+            return hasattr(objclass, 'conference')
+        if issubclass(type(attref), django.db.models.query_utils.DeferredAttribute):
+            if _is_visible():
+                yield attname, attref.field.verbose_name
+        elif issubclass(type(attref), django.db.models.fields.related_descriptors.ForwardManyToOneDescriptor):
+            # Special case, don't recurse into conference model if we're not at the top object (to keep smaller)
+            if attname == 'conference' and depth > 0:
+                continue
+            if _is_visible():
+                yield attname, dict(get_all_available_attributes(type(attref.field.related_model()), depth + 1))
+        elif issubclass(type(attref), django.db.models.fields.related_descriptors.ManyToManyDescriptor) and not attref.reverse:
+            if _is_visible():
+                yield attname, [dict(get_all_available_attributes(type(attref.field.related_model()), depth + 1))]
+
+
 # A couple of useful filters that we publish everywhere:
 
 # Like |groupby, except support grouping by objects and not just by values, and sort by