cardo-python-utils 0.5.dev16__tar.gz → 0.5.dev17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {cardo_python_utils-0.5.dev16/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev17}/PKG-INFO +1 -1
  2. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17/cardo_python_utils.egg-info}/PKG-INFO +1 -1
  3. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/pyproject.toml +1 -1
  4. cardo_python_utils-0.5.dev17/python_utils/django/keycloak/admin/user_group.py +159 -0
  5. cardo_python_utils-0.5.dev16/python_utils/django/keycloak/admin/user_group.py +0 -86
  6. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/LICENSE +0 -0
  7. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/MANIFEST.in +0 -0
  8. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/README.rst +0 -0
  9. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/SOURCES.txt +0 -0
  10. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  11. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/requires.txt +0 -0
  12. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/top_level.txt +0 -0
  13. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/__init__.py +0 -0
  14. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/choices.py +0 -0
  15. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/data_structures.py +0 -0
  16. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/db.py +0 -0
  17. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/__init__.py +0 -0
  18. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/admin/__init__.py +0 -0
  19. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/admin/auth.py +0 -0
  20. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/admin/user_groups_changelist.html +0 -0
  21. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/__init__.py +0 -0
  22. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/drf.py +0 -0
  23. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/ninja.py +0 -0
  24. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/utils.py +0 -0
  25. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/models/__init__.py +0 -0
  26. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/models/user_group.py +0 -0
  27. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/service.py +0 -0
  28. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/utils.py +0 -0
  29. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/esma_choices.py +0 -0
  30. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/exceptions.py +0 -0
  31. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/imports.py +0 -0
  32. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/math.py +0 -0
  33. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/text.py +0 -0
  34. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/time.py +0 -0
  35. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/types_hinting.py +0 -0
  36. {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev16
3
+ Version: 0.5.dev17
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
5
  Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev16
3
+ Version: 0.5.dev17
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
5
  Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardo-python-utils"
7
- version = "0.5.dev16"
7
+ version = "0.5.dev17"
8
8
  description = "Python library enhanced with a wide range of functions for different scenarios."
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.8"
@@ -0,0 +1,159 @@
1
+ from django import forms
2
+ from django.contrib import admin, messages
3
+ from django.core.cache import cache
4
+ from django.db.models import Count
5
+ from django.urls import path
6
+ from django.shortcuts import redirect
7
+
8
+ from ..service import KeycloakService
9
+
10
+
11
+ class UserGroupAdminMetaclass(forms.MediaDefiningClass):
12
+ """Metaclass to dynamically construct admin attributes based on app_entities."""
13
+
14
+ def __new__(mcs, name, bases, namespace):
15
+ app_entities = namespace.get("app_entities", ())
16
+
17
+ allowed_entities_attrs = tuple(f"allowed_{entity}" for entity in app_entities)
18
+ allowed_entities_count_attrs = tuple(f"allowed_{entity}_count" for entity in app_entities)
19
+ allow_all_entities_attrs = tuple(f"allow_all_{entity}" for entity in app_entities)
20
+
21
+ namespace["list_display"] = (
22
+ *namespace.get("list_display", ()),
23
+ "path",
24
+ *allow_all_entities_attrs,
25
+ *allowed_entities_count_attrs,
26
+ )
27
+
28
+ namespace["filter_horizontal"] = allowed_entities_attrs
29
+
30
+ namespace["fieldsets"] = (
31
+ (
32
+ None,
33
+ {
34
+ "fields": (
35
+ "id",
36
+ "path",
37
+ *allow_all_entities_attrs,
38
+ *allowed_entities_attrs,
39
+ )
40
+ },
41
+ ),
42
+ )
43
+
44
+ # Dynamically create count methods for each entity
45
+ for count_attr in allowed_entities_count_attrs:
46
+ if count_attr not in namespace:
47
+ namespace[count_attr] = mcs._create_count_method(count_attr)
48
+
49
+ return super().__new__(mcs, name, bases, namespace)
50
+
51
+ def _create_count_method(attr):
52
+ def count_method(self, obj):
53
+ return getattr(obj, attr)
54
+
55
+ # Add the @admin.display decorator
56
+ count_method = admin.display(
57
+ description=attr.replace("_", " ").title(),
58
+ ordering=attr,
59
+ )(count_method)
60
+ return count_method
61
+
62
+
63
+ class UserGroupAdminBase(admin.ModelAdmin, metaclass=UserGroupAdminMetaclass):
64
+ app_entities = () # E.g. app_entities = ('transactions',)
65
+
66
+ search_fields = ("id", "path")
67
+ readonly_fields = ("id", "path")
68
+ change_list_template = "user_groups_changelist.html"
69
+
70
+ def get_queryset(self, request):
71
+ queryset = super().get_queryset(request)
72
+
73
+ for entity in self.app_entities:
74
+ count_attr = f"allowed_{entity}_count"
75
+ queryset = queryset.annotate(**{count_attr: Count(f"allowed_{entity}")})
76
+
77
+ return queryset
78
+
79
+ def construct_change_message(self, request, form, formsets, add=False):
80
+ """
81
+ Enhance the change message to include detailed information about changes
82
+ to allowed and allow_all entity fields.
83
+ """
84
+ change_message = super().construct_change_message(request, form, formsets, add)
85
+
86
+ for entity in self.app_entities:
87
+ # Handle changes to allow_all_entities field
88
+ allow_all_entities_attr = f"allow_all_{entity}"
89
+ if not add and allow_all_entities_attr in form.changed_data:
90
+ old_value = form.initial.get(allow_all_entities_attr, False)
91
+ new_value = form.cleaned_data.get(allow_all_entities_attr, False)
92
+
93
+ if old_value != new_value:
94
+ changed_fields = change_message[0]["changed"]["fields"]
95
+ field = changed_fields.index(allow_all_entities_attr.replace("_", " ").capitalize())
96
+ changed_fields[field] += f": set to {new_value}"
97
+
98
+ # Handle changes to allowed_entities field
99
+ allowed_entities_attr = f"allowed_{entity}"
100
+ if not add and allowed_entities_attr in form.changed_data:
101
+ old_entities = {str(rec) for rec in form.initial.get(allowed_entities_attr, [])}
102
+ new_entities_queryset = form.cleaned_data.get(allowed_entities_attr, [])
103
+ new_entities = {str(rec) for rec in new_entities_queryset}
104
+
105
+ added_entities = new_entities - old_entities
106
+ removed_entities = old_entities - new_entities
107
+
108
+ details = []
109
+ if added_entities:
110
+ details.append(f"added {', '.join(added_entities)}")
111
+ if removed_entities:
112
+ details.append(f"removed {', '.join(removed_entities)}")
113
+
114
+ if details:
115
+ changed_fields = change_message[0]["changed"]["fields"]
116
+ entity_label = allowed_entities_attr.replace("_", " ").capitalize()
117
+ field = changed_fields.index(entity_label)
118
+ changed_fields[field] += ": " + "; ".join(details)
119
+
120
+ return change_message
121
+
122
+ def has_add_permission(self, request):
123
+ # Manual addition of user groups is not allowed
124
+ return False
125
+
126
+ def has_delete_permission(self, request, obj=None):
127
+ # Manual deletion of user groups is not allowed
128
+ return False
129
+
130
+ def get_urls(self):
131
+ return [
132
+ path("sync-with-keycloak/", self.sync_groups_with_keycloak),
133
+ *super().get_urls(),
134
+ ]
135
+
136
+ @staticmethod
137
+ def sync_groups_with_keycloak(request): # noqa
138
+ """Syncs user groups with Keycloak"""
139
+
140
+ try:
141
+ KeycloakService().sync_user_groups(raise_exceptions=True)
142
+ messages.success(request, "User groups synced successfully.")
143
+ except Exception as e:
144
+ messages.error(request, f"Error syncing user groups: {e}")
145
+
146
+ return redirect("..")
147
+
148
+ def changelist_view(self, request, extra_context=None):
149
+ """
150
+ When the list view is accessed, sync the user groups from Keycloak.
151
+ Cache the sync for 10 minutes to avoid excessive requests.
152
+ """
153
+ cache_key = "keycloak_group_sync_lock"
154
+
155
+ if cache.get(cache_key) is None:
156
+ KeycloakService().sync_user_groups()
157
+ cache.set(cache_key, "true", 60 * 10)
158
+
159
+ return super().changelist_view(request, extra_context)
@@ -1,86 +0,0 @@
1
- from django.contrib import admin, messages
2
- from django.core.cache import cache
3
- from django.urls import path
4
- from django.shortcuts import redirect
5
-
6
- from ..service import KeycloakService
7
-
8
-
9
- class UserGroupAdminBase(admin.ModelAdmin):
10
- list_display = ("path",)
11
- search_fields = ("id", "path")
12
- readonly_fields = ("id", "path")
13
-
14
- # To show ManyToMany fields with a horizontal filter widget
15
- # filter_horizontal = ("allowed_entities",)
16
-
17
- change_list_template = "user_groups_changelist.html"
18
-
19
- # To show fields in this order in the detail view
20
- # fieldsets = (
21
- # (
22
- # None,
23
- # {
24
- # "fields": (
25
- # "id",
26
- # "path",
27
- # "allow_all_entities",
28
- # "allowed_entities",
29
- # )
30
- # },
31
- # ),
32
- # )
33
-
34
- def has_add_permission(self, request):
35
- # Manual addition of user groups is not allowed
36
- return False
37
-
38
- def has_delete_permission(self, request, obj=None):
39
- # Manual deletion of user groups is not allowed
40
- return False
41
-
42
- # To show annotated fields in the list view
43
- # def get_queryset(self, request):
44
- # return (
45
- # super()
46
- # .get_queryset(request)
47
- # .annotate(allowed_job_configs_count=Count("allowed_job_configs"))
48
- # )
49
-
50
- def get_urls(self):
51
- return [
52
- path("sync-with-keycloak/", self.sync_groups_with_keycloak),
53
- *super().get_urls(),
54
- ]
55
-
56
- @staticmethod
57
- def sync_groups_with_keycloak(request): # noqa
58
- """Syncs user groups with Keycloak"""
59
-
60
- try:
61
- KeycloakService().sync_user_groups(raise_exceptions=True)
62
- messages.success(request, "User groups synced successfully.")
63
- except Exception as e:
64
- messages.error(request, f"Error syncing user groups: {e}")
65
-
66
- return redirect("..")
67
-
68
- # To allow sorting by annotated fields
69
- # @admin.display(
70
- # description="Allowed Job Configs", ordering="allowed_job_configs_count"
71
- # )
72
- # def allowed_job_configs_count(self, obj):
73
- # return obj.allowed_job_configs_count
74
-
75
- def changelist_view(self, request, extra_context=None):
76
- """
77
- When the list view is accessed, sync the user groups from Keycloak.
78
- Cache the sync for 10 minutes to avoid excessive requests.
79
- """
80
- cache_key = "keycloak_group_sync_lock"
81
-
82
- if cache.get(cache_key) is None:
83
- KeycloakService().sync_user_groups()
84
- cache.set(cache_key, "true", 60 * 10)
85
-
86
- return super().changelist_view(request, extra_context)