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.
- {cardo_python_utils-0.5.dev16/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev17}/PKG-INFO +1 -1
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17/cardo_python_utils.egg-info}/PKG-INFO +1 -1
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/pyproject.toml +1 -1
- cardo_python_utils-0.5.dev17/python_utils/django/keycloak/admin/user_group.py +159 -0
- cardo_python_utils-0.5.dev16/python_utils/django/keycloak/admin/user_group.py +0 -86
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/LICENSE +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/MANIFEST.in +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/README.rst +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/SOURCES.txt +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/requires.txt +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/cardo_python_utils.egg-info/top_level.txt +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/__init__.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/choices.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/data_structures.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/db.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/__init__.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/admin/__init__.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/admin/auth.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/admin/user_groups_changelist.html +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/__init__.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/drf.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/ninja.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/api/utils.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/models/__init__.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/models/user_group.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/keycloak/service.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/django/utils.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/esma_choices.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/exceptions.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/imports.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/math.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/text.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/time.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/types_hinting.py +0 -0
- {cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cardo-python-utils"
|
|
7
|
-
version = "0.5.
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cardo_python_utils-0.5.dev16 → cardo_python_utils-0.5.dev17}/python_utils/data_structures.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|