micro-sidebar 1.2.2__py3-none-any.whl → 2.1.0__py3-none-any.whl
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.
- {micro_sidebar-1.2.2.dist-info → micro_sidebar-2.1.0.dist-info}/METADATA +116 -1
- micro_sidebar-2.1.0.dist-info/RECORD +29 -0
- sidebar/apps.py +10 -0
- sidebar/context_processors.py +127 -0
- sidebar/discovery.py +151 -0
- sidebar/static/sidebar/css/theme_picker.css +165 -0
- sidebar/static/sidebar/js/theme_picker.js +58 -0
- sidebar/static/sidebar/sidebar.css +32 -8
- sidebar/static/sidebar/sidebar.js +15 -40
- sidebar/static/themes/blue.css +51 -0
- sidebar/static/themes/dark.css +501 -0
- sidebar/static/themes/gold.css +51 -0
- sidebar/static/themes/green.css +63 -0
- sidebar/static/themes/light.css +51 -0
- sidebar/static/themes/main.css +6 -0
- sidebar/static/themes/main.js +41 -0
- sidebar/static/themes/red.css +51 -0
- sidebar/templates/sidebar/auto.html +13 -0
- sidebar/templates/sidebar/extra_groups.html +34 -0
- sidebar/templates/sidebar/main.html +24 -1
- sidebar/templatetags/__init__.py +1 -0
- sidebar/templatetags/sidebar_tags.py +74 -0
- micro_sidebar-1.2.2.dist-info/RECORD +0 -13
- {micro_sidebar-1.2.2.dist-info → micro_sidebar-2.1.0.dist-info}/LICENSE +0 -0
- {micro_sidebar-1.2.2.dist-info → micro_sidebar-2.1.0.dist-info}/WHEEL +0 -0
- {micro_sidebar-1.2.2.dist-info → micro_sidebar-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: micro-sidebar
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A Reusable RTL Django Sidebar App
|
|
5
5
|
Home-page: https://github.com/debeski/micro-sidebar
|
|
6
6
|
Author: DeBeski
|
|
@@ -66,6 +66,119 @@ Requires-Dist: Django (>=5.1)
|
|
|
66
66
|
]
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Auto-Discovery Mode (New in v2.0.0)
|
|
70
|
+
|
|
71
|
+
The sidebar can **automatically discover** your list views and generate navigation items!
|
|
72
|
+
|
|
73
|
+
### Setup
|
|
74
|
+
|
|
75
|
+
1. **Add context processor** to `settings.py`:
|
|
76
|
+
```python
|
|
77
|
+
TEMPLATES = [{
|
|
78
|
+
'OPTIONS': {
|
|
79
|
+
'context_processors': [
|
|
80
|
+
...
|
|
81
|
+
'sidebar.context_processors.sidebar_context',
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
}]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
2. **Use in your sidebar template:**
|
|
88
|
+
```html
|
|
89
|
+
{% extends "sidebar/main.html" %}
|
|
90
|
+
{% load sidebar_tags %}
|
|
91
|
+
|
|
92
|
+
{% block items %}
|
|
93
|
+
{% auto_sidebar %}
|
|
94
|
+
{% endblock %}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
That's it! The sidebar will automatically find all URLs with `list` in their name (e.g., `decree_list`), match them to models, and display `verbose_name_plural` as labels.
|
|
98
|
+
|
|
99
|
+
### Configuration (Optional)
|
|
100
|
+
|
|
101
|
+
Add to `settings.py`:
|
|
102
|
+
```python
|
|
103
|
+
SIDEBAR_AUTO = {
|
|
104
|
+
'ENABLED': True, # Enable auto-discovery
|
|
105
|
+
'URL_PATTERNS': ['list'], # Keywords to match in URL names
|
|
106
|
+
'EXCLUDE_APPS': ['admin', 'auth'], # Apps to exclude
|
|
107
|
+
'EXCLUDE_MODELS': [], # Specific models to exclude
|
|
108
|
+
'CACHE_TIMEOUT': 3600, # Cache timeout in seconds
|
|
109
|
+
'DEFAULT_ICON': 'bi-list', # Default Bootstrap icon
|
|
110
|
+
'DEFAULT_ITEMS': {}, # See "Default Items" section below
|
|
111
|
+
'EXTRA_ITEMS': {}, # See "Extra Items" section below
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Default Items Configuration
|
|
116
|
+
|
|
117
|
+
Use `DEFAULT_ITEMS` to customize auto-discovered items (labels, icons, ordering) without modifying your models:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
SIDEBAR_AUTO = {
|
|
121
|
+
# ...
|
|
122
|
+
'DEFAULT_ITEMS': {
|
|
123
|
+
'decree_list': { # Key is the URL name
|
|
124
|
+
'label': 'Decisions', # Override display label
|
|
125
|
+
'icon': 'bi-gavel', # Override icon
|
|
126
|
+
'order': 10, # Sort order (lower = first)
|
|
127
|
+
},
|
|
128
|
+
'incoming_list': {
|
|
129
|
+
'order': 20, # Only setting order
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Items not in `DEFAULT_ITEMS` will use defaults:
|
|
136
|
+
- **Label**: Model's `verbose_name_plural`
|
|
137
|
+
- **Icon**: `DEFAULT_ICON`
|
|
138
|
+
- **Order**: 100
|
|
139
|
+
|
|
140
|
+
### Extra Items (Non-Model URLs)
|
|
141
|
+
|
|
142
|
+
For URLs that don't map to a model (e.g., management pages), use `EXTRA_ITEMS`:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
SIDEBAR_AUTO = {
|
|
146
|
+
# ... other config ...
|
|
147
|
+
'EXTRA_ITEMS': {
|
|
148
|
+
'الإدارة': { # Group name (accordion header)
|
|
149
|
+
'icon': 'bi-gear',
|
|
150
|
+
'items': [
|
|
151
|
+
{
|
|
152
|
+
'url_name': 'manage_sections',
|
|
153
|
+
'label': 'إدارة الأقسام',
|
|
154
|
+
'icon': 'bi-diagram-3',
|
|
155
|
+
'permission': 'documents.manage_sections',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
'url_name': 'manage_users',
|
|
159
|
+
'label': 'إدارة المستخدمين',
|
|
160
|
+
'icon': 'bi-people',
|
|
161
|
+
'permission': 'is_staff', # or a special check to Only show to staff users.
|
|
162
|
+
},
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Then in your sidebar template:
|
|
170
|
+
```html
|
|
171
|
+
{% extends "sidebar/main.html" %}
|
|
172
|
+
{% load sidebar_tags %}
|
|
173
|
+
|
|
174
|
+
{% block items %}
|
|
175
|
+
{% auto_sidebar %}
|
|
176
|
+
{% extra_sidebar %}
|
|
177
|
+
{% endblock %}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Extra items appear at the bottom of the sidebar, grouped in Bootstrap accordions.
|
|
181
|
+
|
|
69
182
|
## Customization
|
|
70
183
|
|
|
71
184
|
### Override Default Menu
|
|
@@ -115,3 +228,5 @@ While it may theoretically work in LTR environments if standard Bootstrap files
|
|
|
115
228
|
| **v1.2.0** | **New Theme Implementation:** Redesigned UI with rounded pill-shaped items, tactile micro-animations, and a refined color palette. Improved responsiveness with dynamic top-offset calculations and inline FOUC fixes for small screens. Fixed tooltip stickiness bug. |
|
|
116
229
|
| **v1.2.1** | **Positioning Fix:** Added `align-self: flex-start` to resolve 60px vertical offset in flex containers. Removed legacy `sidebar-top-offset` CSS variable and JS calculations. Added `box-shadow: none` and `outline: none` to accordion buttons to remove focus ring. Fixed page flickering on wider screens by constraining sidebar height with `calc(100vh - header-height)`. |
|
|
117
230
|
| **v1.2.2** | **CSP Compliance:** Added `nonce` attribute support to inline scripts for Content Security Policy compliance. |
|
|
231
|
+
| **v2.0.0** | **Auto-Discovery:** New feature that introspects Django URL patterns and models to automatically generate sidebar navigation items. Adds `{% auto_sidebar %}` template tag, context processor, and configuration options. |
|
|
232
|
+
| **v2.1.0** | **Refactor & Enhancements:** Decoupled customization from models by introducing `DEFAULT_ITEMS` setting for overriding auto-discovered items' labels/icons/order. Added `EXTRA_ITEMS` setting for manual, permission-aware sidebar links grouped in accordions with `{% extra_sidebar %}` tag. Removed deprecated model-level `sidebar_*` attributes. |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
sidebar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sidebar/apps.py,sha256=iF89pf0Vfs32KLibizTjb9crYPOb3sKcpS0MddBYGrM,381
|
|
3
|
+
sidebar/context_processors.py,sha256=g4TmjkAdyJDqGmQ7c1EhRtDUyt7gHQTPsNzbzy0o_sY,4221
|
|
4
|
+
sidebar/discovery.py,sha256=6rBpji-g057Pwg3fS7Vp1JV61wz3ygmhZfosGMNXjAY,5636
|
|
5
|
+
sidebar/urls.py,sha256=UL_9e1RLNMxZXkah65m7GRU1dbViZRGeNPBIiSZpOYg,142
|
|
6
|
+
sidebar/views.py,sha256=MebyJ1ZiylSOPESXFkkQ8QTg-ClrkJn-oYLN6KrcgiM,418
|
|
7
|
+
sidebar/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
sidebar/static/sidebar/sidebar.css,sha256=2mO5IlDZawzFds5KE9NCpM1UwB-e5HHlnRj2gfK28vs,5995
|
|
9
|
+
sidebar/static/sidebar/sidebar.js,sha256=gcurW6nl0Dt9jPTzk749DX7uQ3U_7De4F6Gj8ci6W40,5233
|
|
10
|
+
sidebar/static/sidebar/css/theme_picker.css,sha256=S2u9p_4rXZALf7hoCOsgyAU91NP3Hoy54WYnc4aGyww,4345
|
|
11
|
+
sidebar/static/sidebar/js/theme_picker.js,sha256=Kt4S2fd0ocFjqeuOQXDcvWVLrLDAVxs7zANZqzIqA4g,2292
|
|
12
|
+
sidebar/static/themes/blue.css,sha256=_CMBLoX8xuSkEdiSYP4HhM-_M368q5Zus9n2kGILBQw,1651
|
|
13
|
+
sidebar/static/themes/dark.css,sha256=JmLG6UWSB7Erwogz0tCcpZUL1hgwxqgfyx-WSCXBtLU,14854
|
|
14
|
+
sidebar/static/themes/gold.css,sha256=2Ry4ba5CIRvgYFTiAaeh5K5DUjvbX1uyVIcolVTPiwE,1619
|
|
15
|
+
sidebar/static/themes/green.css,sha256=y-P-ShJ6ISOqT6FnuS2PNKNQX0kr4Whx-uMLozhPkvY,1951
|
|
16
|
+
sidebar/static/themes/light.css,sha256=XqStIa8wTzWswWyy2nqbk4tMPKUuIR4967PBoJoPuOc,1717
|
|
17
|
+
sidebar/static/themes/main.css,sha256=NaPBDAPL-PkTphtcIto7Qjb5CkiR9T_X5YztDT5SwwE,121
|
|
18
|
+
sidebar/static/themes/main.js,sha256=DSz07M8c1KTXSlj7woxPq0Kf9VFb2uNCI13Cz87gghs,1412
|
|
19
|
+
sidebar/static/themes/red.css,sha256=t8OTmvgszQXX4YtYxlIM86zjx1jp-SvfVx2G3X0FjDI,1613
|
|
20
|
+
sidebar/templates/sidebar/auto.html,sha256=nPKOcWMQ9aYNnH6z2EswsBE76zuxL4mUYUv8JyvsyWA,476
|
|
21
|
+
sidebar/templates/sidebar/extra_groups.html,sha256=uMjaXhCuDitIattfChKdvyl6KQj6OVuMG4-bkg8Lb9E,1525
|
|
22
|
+
sidebar/templates/sidebar/main.html,sha256=5-cPxjVAJ_lLvjI6IMwM3Lrov7gBZQAb4fh_vVTM160,3521
|
|
23
|
+
sidebar/templatetags/__init__.py,sha256=RC19QrlHdcgslFc_19Se9UhOe-7_WH9GtNGYU_cZlXg,48
|
|
24
|
+
sidebar/templatetags/sidebar_tags.py,sha256=KoZzjqRpMtiGcITKFUQBcs5RdbvtuzCaNrCWEzIbBlk,2245
|
|
25
|
+
micro_sidebar-2.1.0.dist-info/LICENSE,sha256=Fco89ULLSSxKkC2KKnx57SaT0R7WOkZfuk8IYcGiN50,1063
|
|
26
|
+
micro_sidebar-2.1.0.dist-info/METADATA,sha256=Qu2mhQ1Log9XThokGmFAtUc4haOkaIQeMU7n7MPFh3c,8264
|
|
27
|
+
micro_sidebar-2.1.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
28
|
+
micro_sidebar-2.1.0.dist-info/top_level.txt,sha256=ih69sjMhU1wOB9HzUV90yEY98aiPuGhzPBBBE-YtJ3w,8
|
|
29
|
+
micro_sidebar-2.1.0.dist-info/RECORD,,
|
sidebar/apps.py
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
from django.apps import AppConfig
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
class SidebarConfig(AppConfig):
|
|
4
5
|
default_auto_field = 'django.db.models.BigAutoField'
|
|
5
6
|
name = 'sidebar'
|
|
7
|
+
|
|
8
|
+
def ready(self):
|
|
9
|
+
"""
|
|
10
|
+
Called when Django starts.
|
|
11
|
+
Import discovery module to ensure it's available.
|
|
12
|
+
"""
|
|
13
|
+
# Import to make discovery available
|
|
14
|
+
from . import discovery # noqa: F401
|
|
15
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context processor for auto-discovered sidebar items.
|
|
3
|
+
|
|
4
|
+
Injects sidebar navigation items into every request context,
|
|
5
|
+
filtered by user permissions.
|
|
6
|
+
"""
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from django.core.cache import cache
|
|
10
|
+
from django.urls import reverse, NoReverseMatch
|
|
11
|
+
from .discovery import discover_list_urls, get_sidebar_config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_config_hash(config):
|
|
15
|
+
"""Generate a hash of the config for cache key."""
|
|
16
|
+
# Exclude EXTRA_ITEMS from hash since they're processed separately
|
|
17
|
+
config_copy = {k: v for k, v in config.items() if k != 'EXTRA_ITEMS'}
|
|
18
|
+
config_str = json.dumps(config_copy, sort_keys=True)
|
|
19
|
+
return hashlib.md5(config_str.encode()).hexdigest()[:8]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _process_extra_items(config, request):
|
|
23
|
+
"""
|
|
24
|
+
Process EXTRA_ITEMS config into sidebar-ready format.
|
|
25
|
+
|
|
26
|
+
Returns dict of groups, each with icon and list of items with resolved URLs.
|
|
27
|
+
"""
|
|
28
|
+
extra_items = config.get('EXTRA_ITEMS', {})
|
|
29
|
+
processed_groups = {}
|
|
30
|
+
|
|
31
|
+
for group_name, group_config in extra_items.items():
|
|
32
|
+
group_icon = group_config.get('icon', 'bi-gear')
|
|
33
|
+
items = []
|
|
34
|
+
|
|
35
|
+
for item in group_config.get('items', []):
|
|
36
|
+
url_name = item.get('url_name', '')
|
|
37
|
+
|
|
38
|
+
# Check permission if specified
|
|
39
|
+
permission = item.get('permission')
|
|
40
|
+
if permission:
|
|
41
|
+
if permission == 'is_staff' and not request.user.is_staff:
|
|
42
|
+
continue
|
|
43
|
+
elif permission == 'is_superuser' and not request.user.is_superuser:
|
|
44
|
+
continue
|
|
45
|
+
elif permission not in ['is_staff', 'is_superuser'] and not request.user.has_perm(permission):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
# Resolve URL
|
|
49
|
+
try:
|
|
50
|
+
url = reverse(url_name)
|
|
51
|
+
active = request.path == url or request.path.startswith(url.rstrip('/') + '/')
|
|
52
|
+
except NoReverseMatch:
|
|
53
|
+
url = '#'
|
|
54
|
+
active = False
|
|
55
|
+
|
|
56
|
+
items.append({
|
|
57
|
+
'url_name': url_name,
|
|
58
|
+
'url': url,
|
|
59
|
+
'label': item.get('label', url_name),
|
|
60
|
+
'icon': item.get('icon', 'bi-link'),
|
|
61
|
+
'active': active,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if items: # Only add group if it has visible items
|
|
65
|
+
processed_groups[group_name] = {
|
|
66
|
+
'icon': group_icon,
|
|
67
|
+
'items': items,
|
|
68
|
+
'has_active': any(item['active'] for item in items),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return processed_groups
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sidebar_context(request):
|
|
75
|
+
"""
|
|
76
|
+
Add auto-discovered sidebar items to template context.
|
|
77
|
+
|
|
78
|
+
Items are cached for performance and filtered by user permissions.
|
|
79
|
+
Only authenticated users see sidebar items, and only items they
|
|
80
|
+
have view permission for.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with 'sidebar_auto_items' and 'sidebar_extra_groups' keys.
|
|
84
|
+
"""
|
|
85
|
+
config = get_sidebar_config()
|
|
86
|
+
# Include config hash in cache key so settings changes invalidate cache
|
|
87
|
+
cache_key = f'sidebar_auto_items_{_get_config_hash(config)}'
|
|
88
|
+
items = cache.get(cache_key)
|
|
89
|
+
|
|
90
|
+
if items is None:
|
|
91
|
+
items = discover_list_urls()
|
|
92
|
+
cache.set(cache_key, items, timeout=config['CACHE_TIMEOUT'])
|
|
93
|
+
|
|
94
|
+
# Filter by user permissions
|
|
95
|
+
if request.user.is_authenticated:
|
|
96
|
+
if request.user.is_superuser:
|
|
97
|
+
# Superusers see everything
|
|
98
|
+
visible = items
|
|
99
|
+
else:
|
|
100
|
+
visible = []
|
|
101
|
+
for item in items:
|
|
102
|
+
if not item.get('permissions'):
|
|
103
|
+
visible.append(item)
|
|
104
|
+
elif any(request.user.has_perm(p) for p in item['permissions']):
|
|
105
|
+
visible.append(item)
|
|
106
|
+
items = visible
|
|
107
|
+
|
|
108
|
+
# Process extra items for authenticated users
|
|
109
|
+
extra_groups = _process_extra_items(config, request)
|
|
110
|
+
else:
|
|
111
|
+
items = []
|
|
112
|
+
extra_groups = {}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
'sidebar_auto_items': items,
|
|
116
|
+
'sidebar_extra_groups': extra_groups,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def clear_sidebar_cache():
|
|
121
|
+
"""
|
|
122
|
+
Clear the sidebar items cache.
|
|
123
|
+
|
|
124
|
+
Call this when models or URLs change and sidebar needs refresh.
|
|
125
|
+
"""
|
|
126
|
+
cache.delete('sidebar_auto_items')
|
|
127
|
+
|
sidebar/discovery.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-discovery module for sidebar navigation items.
|
|
3
|
+
|
|
4
|
+
Scans Django URL patterns for list views and matches them to models
|
|
5
|
+
to automatically generate sidebar navigation items.
|
|
6
|
+
"""
|
|
7
|
+
from django.urls import get_resolver, URLPattern, URLResolver
|
|
8
|
+
from django.apps import apps
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from difflib import get_close_matches
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_sidebar_config():
|
|
14
|
+
"""Get sidebar configuration from Django settings with defaults."""
|
|
15
|
+
defaults = {
|
|
16
|
+
'ENABLED': True,
|
|
17
|
+
'URL_PATTERNS': ['list'],
|
|
18
|
+
'EXCLUDE_APPS': ['admin', 'auth', 'contenttypes', 'sessions'],
|
|
19
|
+
'EXCLUDE_MODELS': [],
|
|
20
|
+
'CACHE_TIMEOUT': 3600,
|
|
21
|
+
'DEFAULT_ICON': 'bi-list',
|
|
22
|
+
# Extra items that don't require model matching
|
|
23
|
+
# Format: {'group_name': {'icon': 'bi-gear', 'items': [{'url_name': 'x', 'label': 'X', 'icon': 'bi-x'}]}}
|
|
24
|
+
'EXTRA_ITEMS': {},
|
|
25
|
+
# Default overrides for auto-discovered items
|
|
26
|
+
# Format: {'url_name': {'label': 'X', 'icon': 'bi-x', 'order': 10}}
|
|
27
|
+
'DEFAULT_ITEMS': {},
|
|
28
|
+
}
|
|
29
|
+
user_config = getattr(settings, 'SIDEBAR_AUTO', {})
|
|
30
|
+
return {**defaults, **user_config}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def discover_list_urls():
|
|
35
|
+
"""
|
|
36
|
+
Scan all URL patterns for names containing configured keywords.
|
|
37
|
+
Match them to Django models and extract verbose_name_plural.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of sidebar item dictionaries sorted by order.
|
|
41
|
+
"""
|
|
42
|
+
config = get_sidebar_config()
|
|
43
|
+
|
|
44
|
+
if not config['ENABLED']:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
resolver = get_resolver()
|
|
48
|
+
items = []
|
|
49
|
+
|
|
50
|
+
for pattern in _iterate_patterns(resolver.url_patterns):
|
|
51
|
+
url_name = getattr(pattern, 'name', '') or ''
|
|
52
|
+
|
|
53
|
+
# Check if URL name contains any of the configured patterns
|
|
54
|
+
matched_keyword = None
|
|
55
|
+
for kw in config['URL_PATTERNS']:
|
|
56
|
+
if kw in url_name.lower():
|
|
57
|
+
matched_keyword = kw
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if matched_keyword:
|
|
61
|
+
# Extract model hint (e.g., 'decree' from 'decree_list')
|
|
62
|
+
model_hint = url_name
|
|
63
|
+
for kw in config['URL_PATTERNS']:
|
|
64
|
+
model_hint = model_hint.replace(f'_{kw}', '').replace(f'-{kw}', '').replace(kw, '')
|
|
65
|
+
|
|
66
|
+
model_hint = model_hint.strip('_-')
|
|
67
|
+
|
|
68
|
+
# Try to find model - first with stripped hint, then with keyword itself
|
|
69
|
+
model = None
|
|
70
|
+
if model_hint:
|
|
71
|
+
model = _find_model(model_hint, config)
|
|
72
|
+
|
|
73
|
+
# If no model found and keyword looks like it could be a model name, try that
|
|
74
|
+
if not model and matched_keyword not in ['list', 'index', 'view', 'page']:
|
|
75
|
+
model = _find_model(matched_keyword, config)
|
|
76
|
+
|
|
77
|
+
if model:
|
|
78
|
+
# Check exclusions
|
|
79
|
+
if model._meta.app_label in config['EXCLUDE_APPS']:
|
|
80
|
+
continue
|
|
81
|
+
if f"{model._meta.app_label}.{model.__name__}" in config['EXCLUDE_MODELS']:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
items.append({
|
|
85
|
+
'url_name': url_name,
|
|
86
|
+
'label': getattr(model._meta, 'sidebar_label', None) or str(model._meta.verbose_name_plural),
|
|
87
|
+
'icon': getattr(model._meta, 'sidebar_icon', config['DEFAULT_ICON']),
|
|
88
|
+
'order': getattr(model._meta, 'sidebar_order', 100),
|
|
89
|
+
'app_label': model._meta.app_label,
|
|
90
|
+
'model_name': model._meta.model_name,
|
|
91
|
+
'permissions': [f'{model._meta.app_label}.view_{model._meta.model_name}'],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# Override with DEFAULT_ITEMS if present
|
|
95
|
+
default_items = config.get('DEFAULT_ITEMS', {})
|
|
96
|
+
if url_name in default_items:
|
|
97
|
+
item_config = default_items[url_name]
|
|
98
|
+
# Update last added item
|
|
99
|
+
if items:
|
|
100
|
+
item = items[-1]
|
|
101
|
+
if 'label' in item_config:
|
|
102
|
+
item['label'] = item_config['label']
|
|
103
|
+
if 'icon' in item_config:
|
|
104
|
+
item['icon'] = item_config['icon']
|
|
105
|
+
if 'order' in item_config:
|
|
106
|
+
item['order'] = item_config['order']
|
|
107
|
+
|
|
108
|
+
return sorted(items, key=lambda x: (x['order'], x['label']))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _iterate_patterns(patterns, prefix=''):
|
|
112
|
+
"""Recursively iterate through URL patterns."""
|
|
113
|
+
for pattern in patterns:
|
|
114
|
+
if isinstance(pattern, URLResolver):
|
|
115
|
+
yield from _iterate_patterns(pattern.url_patterns, prefix + str(pattern.pattern))
|
|
116
|
+
elif isinstance(pattern, URLPattern):
|
|
117
|
+
yield pattern
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _find_model(hint, config):
|
|
121
|
+
"""
|
|
122
|
+
Find model by name using exact and fuzzy matching.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
hint: The model name hint extracted from URL
|
|
126
|
+
config: Sidebar configuration dictionary
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Model class or None if not found
|
|
130
|
+
"""
|
|
131
|
+
all_models = apps.get_models()
|
|
132
|
+
|
|
133
|
+
# Build lookup excluding configured apps
|
|
134
|
+
model_names = {}
|
|
135
|
+
for m in all_models:
|
|
136
|
+
if m._meta.app_label not in config['EXCLUDE_APPS']:
|
|
137
|
+
model_names[m.__name__.lower()] = m
|
|
138
|
+
|
|
139
|
+
hint_lower = hint.lower()
|
|
140
|
+
|
|
141
|
+
# Exact match first
|
|
142
|
+
if hint_lower in model_names:
|
|
143
|
+
return model_names[hint_lower]
|
|
144
|
+
|
|
145
|
+
# Handle common pluralization (simple 's' suffix)
|
|
146
|
+
if hint_lower.endswith('s') and hint_lower[:-1] in model_names:
|
|
147
|
+
return model_names[hint_lower[:-1]]
|
|
148
|
+
|
|
149
|
+
# Fuzzy match as fallback
|
|
150
|
+
matches = get_close_matches(hint_lower, model_names.keys(), n=1, cutoff=0.8)
|
|
151
|
+
return model_names[matches[0]] if matches else None
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
|
|
2
|
+
.current-theme-indicator {
|
|
3
|
+
width: 25px;
|
|
4
|
+
height: 25px;
|
|
5
|
+
border-radius: 50%;
|
|
6
|
+
cursor: pointer;
|
|
7
|
+
border: 2px solid white;
|
|
8
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
9
|
+
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
10
|
+
margin: 5px; /* Spacing from bottom-left corner */
|
|
11
|
+
justify-self: end;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
pointer-events: auto; /* Enable clicks since parent toolbar is none */
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.theme-arrow {
|
|
19
|
+
position: absolute;
|
|
20
|
+
bottom: 36px; /* Space above indicator */
|
|
21
|
+
left: 50%;
|
|
22
|
+
transform: translateX(-50%);
|
|
23
|
+
color: var(--primal);
|
|
24
|
+
font-size: 1.3rem; /* Even bigger arrow */
|
|
25
|
+
opacity: 0;
|
|
26
|
+
visibility: hidden;
|
|
27
|
+
transition: all 0.2s ease;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
z-index: 10;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.sidebar.collapsed .theme-arrow.visible {
|
|
33
|
+
opacity: 1;
|
|
34
|
+
visibility: visible;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sidebar.collapsed .theme-arrow {
|
|
38
|
+
bottom: 36px;
|
|
39
|
+
left: 50%;
|
|
40
|
+
transform: translateX(-50%);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.sidebar.collapsed .current-theme-indicator {
|
|
44
|
+
margin: 5px auto; /* Centered in narrow sidebar */
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.current-theme-indicator:hover {
|
|
48
|
+
transform: scale(1.15);
|
|
49
|
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Theme Popup Menu */
|
|
53
|
+
.theme-popup {
|
|
54
|
+
display: none;
|
|
55
|
+
position: absolute;
|
|
56
|
+
bottom: 45px;
|
|
57
|
+
left: 10px; /* Fixed to left in RTL too for specific bottom-left request */
|
|
58
|
+
background: white;
|
|
59
|
+
border-radius: 12px;
|
|
60
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
|
61
|
+
padding: 10px;
|
|
62
|
+
z-index: 1100;
|
|
63
|
+
min-width: 110px;
|
|
64
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
65
|
+
animation: popupIn 0.2s ease-out;
|
|
66
|
+
pointer-events: auto;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.sidebar.collapsed .theme-popup {
|
|
70
|
+
left: 5px;
|
|
71
|
+
right: 5px;
|
|
72
|
+
bottom: 65px; /* More space to accommodate larger arrow and breathing room */
|
|
73
|
+
min-width: auto;
|
|
74
|
+
width: auto;
|
|
75
|
+
background: transparent !important;
|
|
76
|
+
border: none !important;
|
|
77
|
+
box-shadow: none !important;
|
|
78
|
+
padding: 0 !important;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.sidebar.collapsed .theme-popup .small {
|
|
82
|
+
display: none; /* Hide "Select Color" text in narrow view */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sidebar.collapsed .theme-options-grid {
|
|
86
|
+
grid-template-columns: 1fr; /* Vertical column */
|
|
87
|
+
gap: 8px;
|
|
88
|
+
justify-items: center;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Staggered Animations for items */
|
|
92
|
+
.theme-option-circle {
|
|
93
|
+
opacity: 0;
|
|
94
|
+
transform: translateY(10px);
|
|
95
|
+
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.theme-popup.show .theme-option-circle {
|
|
99
|
+
opacity: 1;
|
|
100
|
+
transform: translateY(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Staggered delays */
|
|
104
|
+
.theme-popup.show .theme-option-circle:nth-child(1) { transition-delay: 0.05s; }
|
|
105
|
+
.theme-popup.show .theme-option-circle:nth-child(2) { transition-delay: 0.1s; }
|
|
106
|
+
.theme-popup.show .theme-option-circle:nth-child(3) { transition-delay: 0.15s; }
|
|
107
|
+
.theme-popup.show .theme-option-circle:nth-child(4) { transition-delay: 0.2s; }
|
|
108
|
+
.theme-popup.show .theme-option-circle:nth-child(5) { transition-delay: 0.25s; }
|
|
109
|
+
.theme-popup.show .theme-option-circle:nth-child(6) { transition-delay: 0.3s; }
|
|
110
|
+
|
|
111
|
+
@keyframes popupIn {
|
|
112
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
113
|
+
to { opacity: 1; transform: translateY(0); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.theme-popup.show {
|
|
117
|
+
display: block;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.theme-options-grid {
|
|
121
|
+
display: grid;
|
|
122
|
+
grid-template-columns: repeat(3, 1fr);
|
|
123
|
+
gap: 12px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.theme-option-circle {
|
|
127
|
+
width: 25px;
|
|
128
|
+
height: 25px;
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
border: 2px solid transparent;
|
|
132
|
+
transition: all 0.2s ease;
|
|
133
|
+
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1);
|
|
134
|
+
pointer-events: auto;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.theme-option-circle:hover {
|
|
138
|
+
transform: scale(1.2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.theme-option-circle.active {
|
|
142
|
+
border-color: #3b82f6;
|
|
143
|
+
box-shadow: 0 0 0 2px white, 0 0 0 4px #3b82f6 !important;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Theme Colors for circles */
|
|
147
|
+
.theme-circle-light { background: #f6f7f9; }
|
|
148
|
+
.theme-circle-blue { background: #2363c3; }
|
|
149
|
+
.theme-circle-gold { background: #d97706; }
|
|
150
|
+
.theme-circle-green { background: #166534; }
|
|
151
|
+
.theme-circle-red { background: #7f1d1d; }
|
|
152
|
+
.theme-circle-dark { background: #0f172a; }
|
|
153
|
+
|
|
154
|
+
/* Dark Mode Overrides for the Popup */
|
|
155
|
+
:root.theme-dark .theme-popup {
|
|
156
|
+
background: #1e293b;
|
|
157
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
158
|
+
color: white;
|
|
159
|
+
}
|
|
160
|
+
:root.theme-dark .sidebar-toolbar {
|
|
161
|
+
border-top-color: rgba(255, 255, 255, 0.05);
|
|
162
|
+
}
|
|
163
|
+
:root.theme-dark .theme-option-circle.active {
|
|
164
|
+
box-shadow: 0 0 0 2px #1e293b, 0 0 0 4px #3b82f6 !important;
|
|
165
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
3
|
+
const indicator = document.getElementById('sidebarThemeIndicator');
|
|
4
|
+
const popup = document.getElementById('sidebarThemePopup');
|
|
5
|
+
const options = document.querySelectorAll('.theme-option-circle');
|
|
6
|
+
const arrow = document.getElementById('sidebarThemeArrow');
|
|
7
|
+
|
|
8
|
+
if (!indicator || !popup) return;
|
|
9
|
+
|
|
10
|
+
// Toggle Popup
|
|
11
|
+
indicator.addEventListener('click', (e) => {
|
|
12
|
+
e.stopPropagation();
|
|
13
|
+
const isOpen = popup.classList.toggle('show');
|
|
14
|
+
indicator.classList.toggle('open', isOpen);
|
|
15
|
+
if (arrow) arrow.classList.toggle('visible', isOpen);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Close when clicking outside
|
|
19
|
+
document.addEventListener('click', (e) => {
|
|
20
|
+
if (!popup.contains(e.target) && e.target !== indicator) {
|
|
21
|
+
popup.classList.remove('show');
|
|
22
|
+
indicator.classList.remove('open');
|
|
23
|
+
if (arrow) arrow.classList.remove('visible');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Theme selection
|
|
28
|
+
options.forEach(opt => {
|
|
29
|
+
opt.addEventListener('click', () => {
|
|
30
|
+
const theme = opt.getAttribute('data-theme');
|
|
31
|
+
if (window.setTheme) {
|
|
32
|
+
window.setTheme(theme);
|
|
33
|
+
updateCurrentThemeIndicator(theme);
|
|
34
|
+
popup.classList.remove('show');
|
|
35
|
+
indicator.classList.remove('open');
|
|
36
|
+
if (arrow) arrow.classList.remove('visible');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function updateCurrentThemeIndicator(theme) {
|
|
42
|
+
// Update the main indicator circle's color class
|
|
43
|
+
indicator.className = 'current-theme-indicator theme-circle-' + (theme || 'light');
|
|
44
|
+
|
|
45
|
+
// Highlight active option in popup
|
|
46
|
+
options.forEach(opt => {
|
|
47
|
+
opt.classList.remove('active');
|
|
48
|
+
if (opt.getAttribute('data-theme') === (theme || 'light')) {
|
|
49
|
+
opt.classList.add('active');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Initialize indicator color
|
|
55
|
+
const savedTheme = localStorage.getItem('appTheme') || 'light';
|
|
56
|
+
updateCurrentThemeIndicator(savedTheme);
|
|
57
|
+
});
|
|
58
|
+
})();
|