import re from django.conf import settings from django.utils.encoding import force_str from rest_framework import serializers from rest_framework_json_api.metadata import JSONAPIMetadata from rest_framework.reverse import reverse from rest_framework.utils.field_mapping import ClassLookupDict from rest_framework_json_api.utils import get_related_resource_type from access.models.tenant import Tenant from centurion.serializers.user import User, UserBaseSerializer from core import fields as centurion_field from core.fields.badge import BadgeField from core.fields.icon import IconField class OverRideJSONAPIMetadata(JSONAPIMetadata): type_lookup = ClassLookupDict( { serializers.Field: "GenericField", serializers.RelatedField: "Relationship", serializers.BooleanField: "Boolean", serializers.CharField: "String", serializers.URLField: "URL", serializers.EmailField: "Email", serializers.RegexField: "Regex", serializers.SlugField: "Slug", serializers.IntegerField: "Integer", serializers.FloatField: "Float", serializers.DecimalField: "Decimal", serializers.DateField: "Date", serializers.DateTimeField: "DateTime", serializers.TimeField: "Time", serializers.ChoiceField: "Choice", serializers.MultipleChoiceField: "MultipleChoice", serializers.FileField: "File", serializers.ImageField: "Image", serializers.ListField: "List", serializers.DictField: "Dict", serializers.Serializer: "Serializer", serializers.JSONField: "JSON", # New. Does not exist in base class BadgeField: 'Badge', IconField: 'Icon', User: 'Relationship', UserBaseSerializer: 'Relationship', centurion_field.CharField: 'String', centurion_field.MarkdownField: 'Markdown' } ) class ReactUIMetadata(OverRideJSONAPIMetadata): def determine_metadata(self, request, view): metadata = {} metadata["name"] = view.get_view_name() metadata["description"] = view.get_view_description() if hasattr(view, 'get_model_documentation'): if view.get_model_documentation(): metadata['documentation'] = str(settings.DOCS_ROOT) + str(view.get_model_documentation()) metadata['urls']: dict = {} url_self = None app_namespace = '' base_model = getattr(view, 'base_model', None) if base_model is not None: if( base_model.app_namespace != '' and base_model.app_namespace is not None ): app_namespace = base_model.app_namespace + ':' if getattr(view, 'model', None): if getattr(view.model, 'get_app_namespace', None): app_namespace = view.model().get_app_namespace() if view.kwargs.get('pk', None) is not None: qs = view.get_queryset()[0] if hasattr(qs, 'get_url'): url_self = qs.get_url( request=request ) elif view.kwargs: url_self = reverse('v2:' + app_namespace + view.basename + '-list', request = view.request, kwargs = view.kwargs ) else: url_self = reverse('v2:' + app_namespace + view.basename + '-list', request = view.request ) if url_self: metadata['urls'].update({'self': url_self}) if view.get_back_url(): metadata['urls'].update({'back': view.get_back_url()}) if view.get_return_url(): metadata['urls'].update({'return_url': view.get_return_url()}) metadata["renders"] = [ renderer.media_type for renderer in view.renderer_classes ] metadata["parses"] = [parser.media_type for parser in view.parser_classes] metadata["allowed_methods"] = view.allowed_methods if hasattr(view, 'get_serializer'): serializer = view.get_serializer() metadata['fields'] = self.get_serializer_info(serializer) if view.suffix == 'Instance': metadata['layout'] = view.get_page_layout() elif view.suffix == 'List': if hasattr(view, 'table_fields'): metadata['table_fields'] = view.get_table_fields() if hasattr(view, 'page_layout'): metadata['layout'] = view.get_page_layout() build_repo: str = None if settings.BUILD_REPO: build_repo = settings.BUILD_REPO build_sha: str = None if settings.BUILD_SHA: build_sha = settings.BUILD_SHA build_version: str = 'development' if settings.BUILD_VERSION: build_version = settings.BUILD_VERSION metadata['version']: dict = { 'project_url': build_repo, 'sha': build_sha, 'version': build_version, } metadata['navigation'] = self.get_navigation(request) return metadata def get_field_info(self, field): """ Custom from `rest_framewarok_json_api.metadata.py` Require that read-only fields have their choices added to the metadata. Given an instance of a serializer field, return a dictionary of metadata about it. """ field_info = {} serializer = field.parent if hasattr(field, 'textarea'): if field.textarea: field_info["multi_line"] = True if isinstance(field, serializers.ManyRelatedField): field_info["type"] = self.type_lookup[field.child_relation] else: field_info["type"] = self.type_lookup[field] try: serializer_model = serializer.Meta.model field_info["relationship_type"] = self.relation_type_lookup[ getattr(serializer_model, field.field_name) ] except KeyError: pass except AttributeError: pass else: field_info["relationship_resource"] = get_related_resource_type(field) if hasattr(field, 'autolink'): if field.autolink: field_info['autolink'] = field.autolink field_info["required"] = getattr(field, "required", False) if hasattr(field, 'style_class'): field_info["style"]: dict = { 'class': field.style_class } attrs = [ "read_only", "write_only", "label", "help_text", "min_length", "max_length", "min_value", "max_value", "initial", ] for attr in attrs: value = getattr(field, attr, None) if value is not None and value != "": field_info[attr] = force_str(value, strings_only=True) if getattr(field, "child", None): field_info["child"] = self.get_field_info(field.child) elif getattr(field, "fields", None): field_info["children"] = self.get_serializer_info(field) if ( hasattr(field, "choices") ): field_info["choices"] = [ { "value": choice_value, "display_name": force_str(choice_name, strings_only=True), } for choice_value, choice_name in field.choices.items() ] if ( hasattr(serializer, "included_serializers") and "relationship_resource" in field_info ): field_info["allows_include"] = ( field.field_name in serializer.included_serializers ) if field_info["type"] == 'Markdown': linked_models = [] linked_tickets = [] field_info["render"] = { 'models': {}, 'tickets': {}, } if( field.context['view'].kwargs.get('pk', None) or field.context['view'].metadata_markdown ): queryset = field.context['view'].get_queryset() from core.lib.slash_commands.linked_model import CommandLinkedModel from core.models.ticket.ticket import Ticket for obj in queryset: value = getattr(obj, field.source, None) if field.source == 'display_name': value = str(obj) if value: linked_models = re.findall(r'\s\$(?P[a-z_]+)-(?P\d+)[\s|\n]?', ' ' + str(value) + ' ') linked_tickets = re.findall(r'(?P#(?P\d+))', str(value)) if(getattr(obj, 'from_ticket_id_id', None)): linked_tickets += re.findall(r'(?P#(?P\d+))', '#' + str(obj.to_ticket_id_id)) for ticket, number in linked_tickets: try: item = Ticket.objects.get( pk = number ) field_info["render"]['tickets'].update({ number: { 'status': Ticket.TicketStatus.All(item.status).label, 'ticket_type': Ticket.TicketType(item.ticket_type).label, 'title': str(item), 'url': str(item.get_url()).replace('/api/v2', '') } }) except Ticket.DoesNotExist as e: pass for model_type, model_id in linked_models: try: model, item_type = CommandLinkedModel().get_model( model_type ) if model: item = model.objects.get( pk = model_id ) item_meta = { model_id: { 'title': str(item), 'url': str(item.get_url()).replace('/api/v2', ''), } } if not field_info["render"]['models'].get(model_type, None): field_info["render"]['models'].update({ model_type: item_meta }) else: field_info["render"]['models'][model_type].update( item_meta ) except model.DoesNotExist as e: pass return field_info def get_nav_items(self, request) -> dict: nav = { 'access': { "display_name": "Access", "name": "access", "pages": { 'view_tenant': { "display_name": "Tenancy", "name": "tenant", "icon": "organization", "link": "/access/tenant" }, } }, 'accounting': { "display_name": "Accounting", "name": "accounting", "pages": {} }, '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" } } }, 'human_resources': { "display_name": "Human Resources (HR)", "name": "human_resources", "pages": { # 'view_employees': { # "display_name": "Employees", # "name": "employees", # "icon": "employees", # "link": "/human_resources/employees" # } } }, '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" }, 'view_service': { "display_name": "Services", "name": "service", "link": "/itim/service" }, } }, 'itops': { "display_name": "ITOps", "name": "itops", "icon": "itops", "pages": { 'core.view_ticketcategory': { "display_name": "Ticket Category", "name": "ticketcategory", "icon": 'ticketcategory', "link": "/settings/ticket_category" }, 'core.view_ticketcommentcategory': { "display_name": "Ticket Comment Category", "name": "ticketcommentcategory", "icon": 'ticketcommentcategory', "link": "/settings/ticket_comment_category" }, } }, 'devops': { "display_name": "DevOPs", "name": "devops", "icon": "devops", "pages": { 'view_featureflag': { "display_name": "Feature Flags", "name": "feature_flag", "icon": 'feature_flag', "link": "/devops/feature_flag" } } }, '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": { 'all_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" } } } } if getattr(request, 'feature_flag', None): if request.feature_flag['2025-00001']: nav['devops']['pages'].update({ 'view_gitgroup': { "display_name": "Git Group", "name": "git_group", "icon": 'git_group', "link": "/devops/git_group" }, 'view_gitrepository': { "display_name": "Git Repositories", "name": "git_repository", "icon": 'git', "link": "/devops/git_repository" } }) if request.feature_flag['2025-00002']: if request.feature_flag['2025-00003']: nav['access']['pages'].update({ 'view_role': { "display_name": "Roles", "name": "roles", "icon": 'roles', "link": "/access/role" } }) if request.feature_flag['2025-00008']: nav['access']['pages'].update({ 'view_company': { "display_name": "Companies", "name": "organization", "icon": 'organization', "link": "/access/company" } }) nav['access']['pages'].update({ 'view_contact': { "display_name": "Directory", "name": "directory", "link": "/access/entity/contact" } }) if request.feature_flag['2025-00005']: nav['human_resources']['pages'].update({ 'view_employee': { "display_name": "Employees", "name": "employees", "icon": "employees", "link": "/access/entity/employee" } }) if request.feature_flag['2025-00004']: nav['accounting']['pages'].update({ 'view_assetbase': { "display_name": "Assets", "name": "asset", "link": "/accounting/asset" } }) if request.feature_flag['2025-00007']: nav['itam']['pages'] = { 'view_itamassetbase': { "display_name": "IT Assets", "name": "itasset", "link": "/itam/it_asset" }, **nav['itam']['pages'] } if request.feature_flag['2025-00006']: nav['assistance']['pages'].update({ 'itim.view_requestticket': { "display_name": "Requests New", "name": "request_new", "icon": "ticket_request", "link": "/core/ticket/request" } }) return nav def get_navigation(self, request) -> list(dict()): """Render the navigation menu Check the users permissions against `get_nav_items()`. 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 request.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): '_'}) view_settings: list = [ 'assistance.view_knowledgebasecategory', 'core.view_manufacturer', 'itam.view_devicemodel', 'itam.view_devicetype', 'itam.view_softwarecategory', 'itim.view_clustertype', 'project_management.view_projectstate', 'project_management.view_projecttype', 'settings.view_appsettings', ] # user = view.request.user user_orgainzations = Tenant.objects.filter( manager = request.user ) for app, entry in self.get_nav_items(request).items(): new_menu_entry: dict = {} new_pages: list = [] # if processed_permissions.get(app, None): # doesn't cater for `.` in perm for permission, page in entry['pages'].items(): if permission == 'all_settings': for setting_permission in view_settings: app_permission = str(setting_permission).split('.') if processed_permissions.get(app_permission[0], None): if processed_permissions[app_permission[0]].get(app_permission[1], None): new_pages += [ page ] break elif '.' in permission: app_permission = str(permission).split('.') if processed_permissions.get(app_permission[0], None): if processed_permissions[app_permission[0]].get(app_permission[1], None): new_pages += [ page ] else: if processed_permissions.get(app, None): if processed_permissions[app].get(permission, None): new_pages += [ page ] if( app == 'access' and permission == 'view_organization' and len(user_orgainzations) > 0 ): if page not in new_pages: 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