feat(api): Filter navigation menu by user permissions

if the user has the permission they will have the nav menu.

ref: #409 #415
This commit is contained in:
2024-12-03 17:13:59 +09:30
parent 827fe14369
commit d0118e1f6f
2 changed files with 274 additions and 132 deletions

View File

@ -1,6 +1,8 @@
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.contrib.auth.models import ContentType, Permission
from rest_framework import serializers from rest_framework import serializers
from rest_framework_json_api.metadata import JSONAPIMetadata from rest_framework_json_api.metadata import JSONAPIMetadata
from rest_framework.request import clone_request from rest_framework.request import clone_request
@ -166,136 +168,7 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
} }
metadata['navigation'] = [ metadata['navigation'] = self.get_navigation(request.user)
{
"display_name": "Access",
"name": "access",
"pages": [
{
"display_name": "Organization",
"name": "organization",
"link": "/access/organization"
}
]
},
{
"display_name": "Assistance",
"name": "assistance",
"pages": [
{
"display_name": "Requests",
"name": "request",
"icon": "ticket_request",
"link": "/assistance/ticket/request"
},
{
"display_name": "Knowledge Base",
"name": "knowledge_base",
"icon": "information",
"link": "/assistance/knowledge_base"
}
]
},
{
"display_name": "ITAM",
"name": "itam",
"pages": [
{
"display_name": "Devices",
"name": "device",
"icon": "device",
"link": "/itam/device"
},
{
"display_name": "Operating System",
"name": "operating_system",
"link": "/itam/operating_system"
},
{
"display_name": "Software",
"name": "software",
"link": "/itam/software"
}
]
},
{
"display_name": "ITIM",
"name": "itim",
"pages": [
{
"display_name": "Changes",
"name": "ticket_change",
"link": "/itim/ticket/change"
},
{
"display_name": "Clusters",
"name": "cluster",
"link": "/itim/cluster"
},
{
"display_name": "Incidents",
"name": "ticket_incident",
"link": "/itim/ticket/incident"
},
{
"display_name": "Problems",
"name": "ticket_problem",
"link": "/itim/ticket/problem"
},
{
"display_name": "Services",
"name": "service",
"link": "/itim/service"
},
]
},
{
"display_name": "Config Management",
"name": "config_management",
"icon": "ansible",
"pages": [
{
"display_name": "Groups",
"name": "group",
"icon": 'config_management',
"link": "/config_management/group"
}
]
},
{
"display_name": "Project Management",
"name": "project_management",
"icon": 'project',
"pages": [
{
"display_name": "Projects",
"name": "project",
"icon": 'kanban',
"link": "/project_management/project"
}
]
},
{
"display_name": "Settings",
"name": "settings",
"pages": [
{
"display_name": "System",
"name": "setting",
"icon": "system",
"link": "/settings"
},
{
"display_name": "Task Log",
"name": "celery_log",
# "icon": "settings",
"link": "/settings/celery_log"
}
]
}
]
return metadata return metadata
@ -377,7 +250,6 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
field_info["children"] = self.get_serializer_info(field) field_info["children"] = self.get_serializer_info(field)
if ( if (
# not field_info.get("read_only")
hasattr(field, "choices") hasattr(field, "choices")
): ):
field_info["choices"] = [ field_info["choices"] = [
@ -396,4 +268,206 @@ class ReactUIMetadata(OverRideJSONAPIMetadata):
field.field_name in serializer.included_serializers field.field_name in serializer.included_serializers
) )
return field_info return field_info
_nav = {
'access': {
"display_name": "Access",
"name": "access",
"pages": {
'view_organization': {
"display_name": "Organization",
"name": "organization",
"link": "/access/organization"
}
}
},
'assistance': {
"display_name": "Assistance",
"name": "assistance",
"pages": {
'core.view_ticket_request': {
"display_name": "Requests",
"name": "request",
"icon": "ticket_request",
"link": "/assistance/ticket/request"
},
'view_knowledgebase': {
"display_name": "Knowledge Base",
"name": "knowledge_base",
"icon": "information",
"link": "/assistance/knowledge_base"
}
}
},
'itam': {
"display_name": "ITAM",
"name": "itam",
"pages": {
'view_device': {
"display_name": "Devices",
"name": "device",
"icon": "device",
"link": "/itam/device"
},
'view_operatingsystem': {
"display_name": "Operating System",
"name": "operating_system",
"link": "/itam/operating_system"
},
'view_software': {
"display_name": "Software",
"name": "software",
"link": "/itam/software"
}
}
},
'itim': {
"display_name": "ITIM",
"name": "itim",
"pages": {
'core.view_ticket_change': {
"display_name": "Changes",
"name": "ticket_change",
"link": "/itim/ticket/change"
},
'view_cluster': {
"display_name": "Clusters",
"name": "cluster",
"link": "/itim/cluster"
},
'core.view_ticket_incident': {
"display_name": "Incidents",
"name": "ticket_incident",
"link": "/itim/ticket/incident"
},
'core.view_ticket_problem': {
"display_name": "Problems",
"name": "ticket_problem",
"link": "/itim/ticket/problem"
},
'core.view_service': {
"display_name": "Services",
"name": "service",
"link": "/itim/service"
},
}
},
'config_management': {
"display_name": "Config Management",
"name": "config_management",
"icon": "ansible",
"pages": {
'view_configgroups': {
"display_name": "Groups",
"name": "group",
"icon": 'config_management',
"link": "/config_management/group"
}
}
},
'project_management': {
"display_name": "Project Management",
"name": "project_management",
"icon": 'project',
"pages": {
'view_project': {
"display_name": "Projects",
"name": "project",
"icon": 'kanban',
"link": "/project_management/project"
}
}
},
'settings': {
"display_name": "Settings",
"name": "settings",
"pages": {
'view_settings': {
"display_name": "System",
"name": "setting",
"icon": "system",
"link": "/settings"
},
'django_celery_results.view_taskresult': {
"display_name": "Task Log",
"name": "celery_log",
# "icon": "settings",
"link": "/settings/celery_log"
}
}
}
}
def get_navigation(self, user) -> list(dict()):
"""Render the navigation menu
Check the users permissions agains `_nav`. if they have the permission, add the
menu entry to the navigation to be rendered,
**No** Menu is to be rendered that contains no menu entries.
Args:
user (User): User object from the request.
Returns:
list(dict()): Rendered navigation menu in the format the UI requires it to be.
"""
nav: list = []
processed_permissions: dict = {}
for group in user.groups.all():
for permission in group.permissions.all():
if str(permission.codename).startswith('view_'):
if not processed_permissions.get(permission.content_type.app_label, None):
processed_permissions.update({permission.content_type.app_label: {}})
if permission.codename not in processed_permissions[permission.content_type.app_label]:
processed_permissions[permission.content_type.app_label].update({str(permission.codename): '_'})
for app, entry in self._nav.items():
new_menu_entry: dict = {}
new_pages: list = []
if processed_permissions.get(app, None):
for permission, page in entry['pages'].items():
if '.' in permission:
app_permission = str(permission).split('.')
if processed_permissions[app_permission[0]].get(app_permission[1], None):
new_pages += [ page ]
else:
if processed_permissions[app].get(permission, None):
new_pages += [ page ]
if len(new_pages) > 0:
new_menu_entry = entry.copy()
new_menu_entry.update({ 'pages': new_pages })
nav += [ new_menu_entry ]
return nav

View File

@ -40,6 +40,8 @@ Views are used with Centurion ERP to Fetch the data for rendering.
- _Functional test cases_ `from api.tests.abstract.api_permissions_viewset import APIPermission` - _Functional test cases_ `from api.tests.abstract.api_permissions_viewset import APIPermission`
- View Added to Navigation
## Permissions ## Permissions
@ -66,6 +68,72 @@ def get_dynamic_permissions(self):
``` ```
## Navigation
Although Centurion ERP is a Rest API application, there is a UI. The UI uses data from Centurion's API to render the view that the end user sees. One of those items is the navigation structure.
Location of the navigation is in `app/api/react_ui_metadata.py` under the attribute `_nav`.
### Menu Entry
When adding a view, that is also meant to be seen by the end user, a navigation entry must be added to the correct navgation menu. The entry is a python dictionary and has the following format.
``` pyhton
{
'<app name>.<permission name>': {
"display_name": "<menu entry name>",
"name": "<html id>",
"icon": "<menu entry icon>",
"link": "<relative url.>"
}
}
```
- `app name` _Optional_ is the centurion application name the model belongs to. This entry should only be supplied if the application name for the entry does not match the application for the [navigation menu](#menu).
- `permission name` is the centurion permission required for this menu entry to be rendered for the end user.
- `display_name` Menu entry name that the end user will see
- `name` This is used as part of the html rendering of the page. **must be unique** across ALL menu entries
- `icon` _Optional_ if specified, this is the name of the icon that the UI will place next to the menu entry. If this is not specified, the name key is used as the icon name.
- `link` the relative URL for the entry. this will be the relative URL of the API after the API's version number. _i.e. `/api/v2/assistance/ticket/request` would become `/assistance/ticket/request`_
### Menu
The navigation menu is obtained by the UI as part of the metadata. The structure of the menu is a python dictionary in the following format:
``` python
{
'<app name>': {
"display_name": "<Menu entry>",
"name": "<menu id>",
"pages": {
'<menu entries>'
}
}
}
```
- `app name` the centurion application name the menu belongs to.
- `display_name` Menu name that the end user will see
- `name` This is used as part of the html rendering of the page. **must be unique** across ALL menu entries
- `pages` [Menu entry](#menu-entry) dictionaries.
Upon the UI requesting the navigation menu, the users permission are obtained, and if they have the permission for the menu entry within **any** organization, they will be presented with the menu that has a menu entries.
## Pre v1.3 Docs ## Pre v1.3 Docs
!!! warning !!! warning