From 68c787fab4938bebd9089365ba980bfe441864ef Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 00:58:10 +0930 Subject: [PATCH 001/617] docs(release_notes): add api v1 depreciaation note ref: #344 --- Release-Notes.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Release-Notes.md b/Release-Notes.md index 419b958d9..83f84df3a 100644 --- a/Release-Notes.md +++ b/Release-Notes.md @@ -1,3 +1,10 @@ +## Version 1.4.0 + +- Depreciation of **ALL** API urls. will be [removed in v2.0.0](https://github.com/nofusscomputing/centurion_erp/issues/343) release of Centurion. + +- New API will be at path `api/v2` and will remain until v2.0.0 release of Centurion on which the `api/v2` path will be moved to `api` + + # Version 1.3.0 !!! danger "Security" @@ -14,10 +21,12 @@ This release updates the docker container to be a production setup for deploymen - To setup container as "Worker", set `IS_WORKER='True'` environmental variable within container. _**Note:** You can still use command `celery -A app worker -l INFO`, although **not** recommended as the container health check will not be functioning_ -# Version 1.0.0 +## Version 1.0.0 + Initial Release of Centurion ERP. -## Breaking changes + +### Breaking changes - Nil From cbe01e6b77d530df044db3c829198fc97d007cf4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 01:35:48 +0930 Subject: [PATCH 002/617] feat: Add dependency django-cors-headers ref: #344 --- app/app/settings.py | 9 +++++---- requirements.txt | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/app/settings.py b/app/app/settings.py index d0ae5905d..ba4e530fa 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -99,9 +99,11 @@ # SESSION_COOKIE_SECURE = True # USE_X_FORWARDED_HOST = True # ToDo: https://docs.djangoproject.com/en/dev/ref/settings/#use-x-forwarded-host + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', + 'corsheaders', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', @@ -124,10 +126,11 @@ ] MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', + 'django.middleware.common.CommonMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -263,10 +266,8 @@ 'rest_framework.filters.SearchFilter', 'rest_framework_json_api.django_filters.DjangoFilterBackend', 'rest_framework_json_api.filters.OrderingFilter', - 'rest_framework_json_api.django_filters.DjangoFilterBackend', - 'rest_framework.filters.SearchFilter', ), - 'SEARCH_PARAM': 'filter[search]', + # 'SEARCH_PARAM': 'filter[search]', # 'TEST_REQUEST_RENDERER_CLASSES': ( # 'rest_framework_json_api.renderers.JSONRenderer', # ), diff --git a/requirements.txt b/requirements.txt index 1c90bf1cb..cb46e7a18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ + Django==5.1.2 +django-cors-headers==4.4.0 + django-debug-toolbar==4.3.0 social-auth-app-django==5.4.1 From 7b7b12d08e4a94fa86ddfe07226ea3d0add7a733 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 16:31:41 +0930 Subject: [PATCH 003/617] feat(api): Add common ViewSet class for inheritence ref: #345 #346 --- app/api/viewsets/common.py | 154 ++++++++++++++++++ .../centurion_erp/development/views.md | 35 +++- 2 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 app/api/viewsets/common.py diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py new file mode 100644 index 000000000..3da07fc27 --- /dev/null +++ b/app/api/viewsets/common.py @@ -0,0 +1,154 @@ +from rest_framework import viewsets + +from access.mixin import OrganizationMixin + +from api.react_ui_metadata import ReactUIMetadata +from api.views.mixin import OrganizationPermissionAPI + + + +class CommonViewSet( + OrganizationMixin, + viewsets.ModelViewSet +): + """Common ViewSet class + + This class is to be inherited by ALL viewsets. + + Args: + OrganizationMixin (class): Contains the Authorization checks. + viewsets (class): Django Rest Framework base class. + """ + + @property + def allowed_methods(self) -> list: + """Allowed HTTP Methods + + _Optional_, HTTP Methods allowed for the `viewSet`. + + Returns: + list: Allowed HTTP Methods + """ + + return super().allowed_methods + + + documentation: str = None + """ Viewset Documentation URL + + _Optional_, if specified will be add to list view metadata + """ + + filterset_fields: list = [] + """Fields to use for filtering the query + + _Optional_, if specified, these fields can be used to filter the API response + """ + + metadata_class = ReactUIMetadata + """ Metadata Class + + _Mandatory_, required so that the HTTP/Options method is populated with the data + required to generate the UI. + """ + + model: object = None + """Django Model + _Mandatory_, Django model used for this view. + """ + + model_documentation: str = None + """Model Documentation URL + + _Optional_, if specified will be add to detail view metadata""" + + page_layout: list = [] + """ Page layout class + + _Optional_, used by metadata to add the page layout to the HTTP/Options method + for detail view, Enables the UI can setup the page layout. + """ + + permission_classes = [ OrganizationPermissionAPI ] + """Permission Class + + _Mandatory_, Permission check class + """ + + queryset: object = None + """View Queryset + + _Optional_, View model Query + """ + + search_fields:list = [] + """ Search Fields + + _Optional_, Used by API text search as the fields to search. + """ + + + + def get_model_documentation(self): + + if not self.model_documentation: + + if hasattr(self.model, 'documentataion'): + + self.model_documentation = self.model.documentation + + else: + + self.model_documentation = '' + + return self.model_documentation + + + + def get_page_layout(self): + + if len(self.page_layout) < 1: + + if hasattr(self.model, 'page_layout'): + + self.page_layout = self.model.page_layout + + else: + + self.page_layout = [] + + return self.page_layout + + + + def get_queryset(self): + + if not self.queryset: + + self.queryset = self.model.objects.all() + + return self.queryset + + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] + + + + def get_view_name(self): + + if self.detail: + + return self.model._meta.verbose_name + + return self.model._meta.verbose_name_plural diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 09bb5b781..3238d4a67 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -1,11 +1,35 @@ --- title: Views -description: Centurion ERP Common Views development documentation +description: Views development Documentation for Centurion ERP date: 2024-07-12 template: project.html about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp --- +Views are used with Centurion ERP to Fetch the data for rendering. + +!!! info + Centurion release v1.3.0 added a feature lock to **ALL** Views and the current API. From this release, there is a new API at endpoint `api/v2`. As such we will only be using DRF `ViewSets`. This is required as the UI is being separated from the Centurion Codebase to its own repository. This means that Centurion will become an API only codebase. Release 2.0.0 will remove the current UI and api from Centurion. [See #](https://github.com/nofusscomputing/centurion_erp/issues/343) for details. + + +## Requirements + +- Views are class based + +- Inherits from base class `api.viewsets.common.CommonViewSet` + +- **ALL** views are `ViewSets` + +- Views are saved within the module the model is from under path `viewsets/` + +- views are documented at the class level for the swagger UI. + + +## Pre v1.3 Docs + +!!! warning + These docs are depreciated + Views are used with Centurion ERP to Fetch the data for rendering as html. We have templated our views to aid in quick development. We have done this by adding to our views the required pieces of logic so as to ensure the right information is available. The available API can be found within the [API Views](./api/common_views.md) docs. The views that we use are: @@ -33,7 +57,7 @@ The views that we use are: Common test cases are available for views. These test cases can be found within the API docs under [model view test cases](./api/tests/model_views.md). -## Requirements +### Requirements All views are to meet the following requirements: @@ -50,7 +74,7 @@ All views are to meet the following requirements: - Add and change views to use a form class -## Tests +### Tests The following unit test cases exist for views: @@ -70,13 +94,13 @@ The following unit test cases exist for views: The `AllViews` test class is an aggregation of all views. This class is the recommended test class to include if the model uses all available views. -## Docs to clean up +### Docs to clean up !!! note The below documentation is still to be developed. As such what is written below may be incorrect. -### Templates +#### Templates The base template includes blocks that are designed to assist in rendering your content. The following blocks are available: @@ -100,4 +124,3 @@ your content here {% endblock %} ``` - From d9783477cebfc7e1067f6d01b4da44c88e799c62 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 16:38:33 +0930 Subject: [PATCH 004/617] test: Ensure Models have meta attribute `verbose_name` ref: #345 #346 --- app/app/tests/abstract/models.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 283b711ac..5c8b625a5 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -75,6 +75,39 @@ def test_field_type_verbose_name_plural(self): assert type(self.model._meta.original_attrs['verbose_name_plural']) is str + def test_field_exists_verbose_name(self): + """Test for existance of field in `.Meta` + + Field is required for `templates/detail.html.js` + + Attribute `verbose_name` must be defined in `Meta` class. + """ + + assert 'verbose_name' in self.model._meta.original_attrs + + + def test_field_not_empty_verbose_name(self): + """Test field `.Meta` is not empty + + Field is required for `templates/detail.html.js` + + Attribute `verbose_name` must be defined in `Meta` class. + """ + + assert self.model._meta.original_attrs['verbose_name'] is not None + + + def test_field_type_verbose_name(self): + """Test field `.Meta` is not empty + + Field is required for `templates/detail.html.js` + + Attribute `verbose_name` must be of type str. + """ + + assert type(self.model._meta.original_attrs['verbose_name']) is str + + class ModelAdd( From 5a88b23ae7b407f0e8ea42b14b4197745a7381cd Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 16:41:36 +0930 Subject: [PATCH 005/617] test: Ensure Models have attribute `table_fields` ref: #345 #346 --- app/app/tests/abstract/models.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 5c8b625a5..5f0fedf7e 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -109,6 +109,35 @@ def test_field_type_verbose_name(self): + def test_attribute_exists_table_fields(self): + """Attrribute Test, Exists + + Ensure attribute `table_fields` exists + """ + + assert hasattr(self.model, 'table_fields') + + + def test_attribute_type_table_fields(self): + """Attrribute Test, Type + + Ensure attribute `table_fields` is of type `list` + """ + + assert type(self.model.table_fields) is list + + + def test_attribute_not_callable_table_fields(self): + """Attrribute Test, Not Callable + + Attribute must be a property + + Ensure attribute `table_fields` is not callable. + """ + + assert not callable(self.model.table_fields) + + class ModelAdd( AddView From 07ceae64715832801d3ee08fd3b78b53d1f2457f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 16:42:10 +0930 Subject: [PATCH 006/617] test: Ensure Models have attribute `page_layout` ref: #345 #346 --- app/app/tests/abstract/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 5f0fedf7e..d72cbfe4b 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -139,6 +139,36 @@ def test_attribute_not_callable_table_fields(self): + def test_attribute_exists_page_layout(self): + """Attrribute Test, Exists + + Ensure attribute `page_layout` exists + """ + + assert hasattr(self.model, 'page_layout') + + + def test_attribute_type_page_layout(self): + """Attrribute Test, Type + + Ensure attribute `page_layout` is of type `list` + """ + + assert type(self.model.page_layout) is list + + + def test_attribute_not_callable_page_layout(self): + """Attrribute Test, Not Callable + + Attribute must be a property + + Ensure attribute `page_layout` is not callable. + """ + + assert not callable(self.model.page_layout) + + + class ModelAdd( AddView ): From c0cf657bea276834b3a214d799f27a3ad4767b49 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:45:47 +0930 Subject: [PATCH 007/617] feat(core): Add a badge serializer field. ref: #345 #346 --- app/core/classes/badge.py | 44 +++++++++++++++++++++++++++++++++++++++ app/core/fields/badge.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 app/core/classes/badge.py create mode 100644 app/core/fields/badge.py diff --git a/app/core/classes/badge.py b/app/core/classes/badge.py new file mode 100644 index 000000000..4b7aa579b --- /dev/null +++ b/app/core/classes/badge.py @@ -0,0 +1,44 @@ +from core.classes.icon import Icon + + + +class Badge: + + icon: Icon + + text: str + + text_style:str + + url:str + + + def __init__(self, + icon_name: str = None, + icon_style: str = None, + text: str = None, + text_style: str = None, + url: str = None + ): + + self.icon = Icon( + name=icon_name, + style = icon_style + ) + + self.text = text + + self.text_style = text_style + + self.url = url + + + @property + def to_json(self): + + return { + 'icon': self.icon.to_json, + 'text': self.text, + 'text_style': self.text_style, + 'url': self.url, + } diff --git a/app/core/fields/badge.py b/app/core/fields/badge.py new file mode 100644 index 000000000..6b8c5872c --- /dev/null +++ b/app/core/fields/badge.py @@ -0,0 +1,34 @@ +from rest_framework import serializers +from rest_framework.fields import empty + +from core.classes.badge import Badge + +from core.fields.icon import Icon, IconField + + + +class BadgeField(serializers.Field): + + source = '' + + label = '' + + + def __init__(self, *, read_only=True, write_only=False, + required=None, default=empty, initial=empty, source=None, + label=None, help_text=None, style=None, + error_messages=None, validators=None, allow_null=False): + + super().__init__(read_only=read_only, write_only=write_only, + required=required, default=default, initial=initial, source=source, + label=label, help_text=help_text, style=style, + error_messages=error_messages, validators=validators, allow_null=allow_null) + + + def to_representation(self, badge: Badge): + + return badge.to_json + + + def to_internal_value(self, data): + return Badge(data.icon,data.colour, data.url) From 991fb3432c3bb936acb510314576075dd748bddd Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:46:08 +0930 Subject: [PATCH 008/617] feat(core): Add a icon serializer field. ref: #345 #346 --- app/core/classes/icon.py | 29 +++++++++++++++++++++++++++++ app/core/fields/icon.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 app/core/classes/icon.py create mode 100644 app/core/fields/icon.py diff --git a/app/core/classes/icon.py b/app/core/classes/icon.py new file mode 100644 index 000000000..eac23a8c3 --- /dev/null +++ b/app/core/classes/icon.py @@ -0,0 +1,29 @@ + + + +class Icon: + + name: str + + style:str + + + def __init__(self, + name: str = None, + style: str = None, + url: str = None + ): + + self.name = name + + self.style = style + + self.url = url + + @property + def to_json(self): + + return { + 'name': self.name, + 'style': self.style + } diff --git a/app/core/fields/icon.py b/app/core/fields/icon.py new file mode 100644 index 000000000..1e561714e --- /dev/null +++ b/app/core/fields/icon.py @@ -0,0 +1,36 @@ +from rest_framework import serializers +from rest_framework.fields import empty + +from core.classes.icon import Icon + + + +class IconField(serializers.Field): + + source = '' + + label = '' + + def __init__(self, *, read_only=True, write_only=False, + required=None, default=empty, initial=empty, source=None, + label=None, help_text=None, style=None, + error_messages=None, validators=None, allow_null=False): + + super().__init__(read_only=read_only, write_only=write_only, + required=required, default=default, initial=initial, source=source, + label=label, help_text=help_text, style=style, + error_messages=error_messages, validators=validators, allow_null=allow_null) + + def to_representation(self, icons: list([Icon])): + + a_icons: list = [] + + for icon in icons: + + a_icons += [ icon.to_json ] + + return a_icons + + + def to_internal_value(self, data): + return Icon(data.icon,data.icon_style, data.url) From 849b8da7eb4ed39a3e61f4705a2b1c096bf775e2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:49:16 +0930 Subject: [PATCH 009/617] refactor(itam): remove requirement to specify the pk when fetching config ref: #345 #346 --- app/itam/models/device.py | 5 +++-- app/itam/views/device.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 3740efdfc..7b30ac1ab 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -243,9 +243,10 @@ def status(self) -> str: return 'UNK' - def get_configuration(self, id): + @property + def get_configuration(self): - softwares = DeviceSoftware.objects.filter(device=id) + softwares = DeviceSoftware.objects.filter(device=self.id) config = { "software": [] diff --git a/app/itam/views/device.py b/app/itam/views/device.py index db64cf898..d2f8fb3ea 100644 --- a/app/itam/views/device.py +++ b/app/itam/views/device.py @@ -140,7 +140,7 @@ def get_context_data(self, **kwargs): context['notes'] = Notes.objects.filter(device=self.kwargs['pk']) - config = self.object.get_configuration(self.kwargs['pk']) + config = self.object.get_configuration context['config'] = json.dumps(config, indent=4, sort_keys=True) context['config_groups'] = ConfigGroupHosts.objects.filter(host = self.object.id) From 935dfb7faa21a3f1e57a27a01d400c8bce9dbe0d Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:51:13 +0930 Subject: [PATCH 010/617] feat(itam): Add status badge property to device model ref: #345 #346 --- app/itam/models/device.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 7b30ac1ab..ee4cbf194 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -11,6 +11,7 @@ from app.helpers.merge_software import merge_software +from core.classes.icon import Icon from core.mixin.history_save import SaveHistory from itam.models.device_common import DeviceCommonFields, DeviceCommonFieldsName @@ -208,6 +209,23 @@ def __str__(self): return self.name + + + @property + def status_icon(self) -> list([Icon]): + + icons: list(Icon) = [] + + icons += [ + Icon( + name = f'device_status_{self.status.lower()}', + style = f'icon-device-status-{self.status.lower()}' + ) + ] + + return icons + + @property def status(self) -> str: """ Fetch Device status @@ -226,21 +244,21 @@ def status(self) -> str: one = (now() - check_date).days + status: str = 'UNK' + if (now() - check_date).days >= 0 and (now() - check_date).days <= 1: - return 'OK' + status = 'OK' elif (now() - check_date).days >= 2 and (now() - check_date).days < 3: - return 'WARN' + status = 'WARN' elif (now() - check_date).days >= 3: - return 'BAD' - - else: + status = 'BAD' - return 'UNK' + return status @property From 50e6a24a4dfba1b4613bb3c532cd6bd19e10dcd6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:52:31 +0930 Subject: [PATCH 011/617] feat(itam): Add action badge property to device software model ref: #345 #346 --- app/itam/models/device.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index ee4cbf194..e4c18cef2 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -406,6 +406,25 @@ class Actions(models.TextChoices): blank = True ) + @property + def action_badge(self): + + from core.classes.badge import Badge + + text:str = 'Add' + + if self.action: + + text = self.get_action_display() + + return Badge( + icon_name = f'action_{text.lower()}', + icon_style = f'badge-icon-action-{text.lower()}', + text = text, + text_style = f'badge-text-action-{text.lower()}', + url = '_self', + ) + @property def parent_object(self): From b471718b6a9f88076099909208fa9d7cda1f8174 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:52:58 +0930 Subject: [PATCH 012/617] feat(itam): Add category property to device software model ref: #345 #346 --- app/itam/models/device.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index e4c18cef2..4e86087dd 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -426,6 +426,18 @@ def action_badge(self): ) + @property + def category(self): + + category = None + + if self.software: + + category = self.software.category.id + + return category + + @property def parent_object(self): """ Fetch the parent object """ From 65a47db81ddaa4e5f8d53fbda2570fa3294f9b54 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:53:45 +0930 Subject: [PATCH 013/617] feat(api): Update API template to use name Centurion ref: #345 #346 --- app/templates/rest_framework/api.html | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/templates/rest_framework/api.html diff --git a/app/templates/rest_framework/api.html b/app/templates/rest_framework/api.html new file mode 100644 index 000000000..4e4b7d199 --- /dev/null +++ b/app/templates/rest_framework/api.html @@ -0,0 +1,7 @@ +{% extends "rest_framework/base.html" %} + +{% block branding %} + + Centurion ERP + +{% endblock %} \ No newline at end of file From 3bfd2d4ef6452fa2a7113db236de6dc808047a1a Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 17:54:47 +0930 Subject: [PATCH 014/617] feat(api): add API login template to use current login form ref: #345 #346 --- app/app/urls.py | 6 ++++++ app/templates/rest_framework/login.html | 9 +++++++++ makefile | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 app/templates/rest_framework/login.html diff --git a/app/app/urls.py b/app/app/urls.py index 2ceac18ad..46bc92a9a 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -74,6 +74,12 @@ ] + urlpatterns += [ + path('api/v2/auth/', include('rest_framework.urls')), + ] + + + if settings.DEBUG: urlpatterns += [ diff --git a/app/templates/rest_framework/login.html b/app/templates/rest_framework/login.html new file mode 100644 index 000000000..9b49191d7 --- /dev/null +++ b/app/templates/rest_framework/login.html @@ -0,0 +1,9 @@ + + +{% extends 'registration/login.html' %} + + +{% block branding %} +

Centurion ERP

+{% endblock %} + diff --git a/makefile b/makefile index 461d7ef08..2d420a022 100644 --- a/makefile +++ b/makefile @@ -40,7 +40,7 @@ lint: markdown-mkdocs-lint test: cd app - pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml **/tests/unit + pytest --cov --cov-report term --cov-report xml:../artifacts/coverage.xml --cov-report html:../artifacts/coverage/ --junit-xml=../artifacts/unit.JUnit.xml **/tests/unit **/tests/functional clean: From 9fe671e43c99b23232396ff3078948c59c19068c Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 21:38:57 +0930 Subject: [PATCH 015/617] test(api): ViewSet checks ref: #345 #346 --- app/api/tests/abstract/viewsets.py | 560 ++++++++++++++++++ .../centurion_erp/development/views.md | 6 +- 2 files changed, 565 insertions(+), 1 deletion(-) create mode 100644 app/api/tests/abstract/viewsets.py diff --git a/app/api/tests/abstract/viewsets.py b/app/api/tests/abstract/viewsets.py new file mode 100644 index 000000000..17fdbb635 --- /dev/null +++ b/app/api/tests/abstract/viewsets.py @@ -0,0 +1,560 @@ +from api.react_ui_metadata import ReactUIMetadata +from api.views.mixin import OrganizationPermissionAPI + + + +class AllViewSet: + """Tests specific to the Viewset + + **Dont include these tests directly, see below for correct class** + + Tests are for ALL viewsets. + """ + + viewset = None + """ViewSet to Test""" + + + + def test_view_attr_allowed_methods_exists(self): + """Attribute Test + + Attribute `allowed_methods` must exist + """ + + assert hasattr(self.viewset, 'allowed_methods') + + + def test_view_attr_allowed_methods_not_empty(self): + """Attribute Test + + Attribute `allowed_methods` must return a value + """ + + view_set = self.viewset() + + assert view_set.allowed_methods is not None + + + def test_view_attr_allowed_methods_type(self): + """Attribute Test + + Attribute `allowed_methods` must be of type list + """ + + view_set = self.viewset() + + assert type(view_set.allowed_methods) is list + + + def test_view_attr_allowed_methods_values(self): + """Attribute Test + + Attribute `allowed_methods` only contains valid values + """ + + # Values valid for index views + valid_values: list = [ + 'GET', + 'HEAD', + 'OPTIONS', + ] + + all_valid: bool = True + + view_set = self.viewset() + + for method in list(view_set.allowed_methods): + + if method not in valid_values: + + all_valid = False + + assert all_valid + + + + def test_view_attr_view_description_exists(self): + """Attribute Test + + Attribute `view_description` must exist + """ + + assert hasattr(self.viewset, 'view_description') + + + def test_view_attr_view_description_not_empty(self): + """Attribute Test + + Attribute `view_description` must return a value + """ + + assert self.viewset.view_description is not None + + + def test_view_attr_view_description_type(self): + """Attribute Test + + Attribute `view_description` must be of type str + """ + + assert type(self.viewset.view_description) is str + + + + def test_view_attr_metadata_class_exists(self): + """Attribute Test + + Attribute `metadata_class` must exist + """ + + assert hasattr(self.viewset, 'metadata_class') + + + def test_view_attr_metadata_class_not_empty(self): + """Attribute Test + + Attribute `metadata_class` must return a value + """ + + view_set = self.viewset() + + assert view_set.metadata_class is not None + + + def test_view_attr_metadata_class_type(self): + """Attribute Test + + Attribute `metadata_class` must be metadata class `ReactUIMetadata` + """ + + view_set = self.viewset() + + assert view_set.metadata_class is ReactUIMetadata + + + + def test_view_attr_permission_classes_exists(self): + """Attribute Test + + Attribute `permission_classes` must exist + """ + + assert hasattr(self.viewset, 'permission_classes') + + + def test_view_attr_permission_classes_not_empty(self): + """Attribute Test + + Attribute `permission_classes` must return a value + """ + + view_set = self.viewset() + + assert view_set.permission_classes is not None + + + def test_view_attr_permission_classes_type(self): + """Attribute Test + + Attribute `permission_classes` must be list + """ + + view_set = self.viewset() + + assert type(view_set.permission_classes) is list + + + def test_view_attr_permission_classes_value(self): + """Attribute Test + + Attribute `permission_classes` must be metadata class `ReactUIMetadata` + """ + + view_set = self.viewset() + + assert view_set.permission_classes[0] is OrganizationPermissionAPI + + assert len(view_set.permission_classes) == 1 + + + + def test_view_attr_view_name_exists(self): + """Attribute Test + + Attribute `view_name` must exist + """ + + assert hasattr(self.viewset, 'view_name') + + + def test_view_attr_view_name_not_empty(self): + """Attribute Test + + Attribute `view_name` must return a value + """ + + assert self.viewset.view_name is not None + + + def test_view_attr_view_name_type(self): + """Attribute Test + + Attribute `view_name` must be of type str + """ + + view_set = self.viewset() + + assert ( + type(view_set.view_name) is str + ) + + + +class APIRenderViewSet: + + """Function ViewSet test + + **Dont include these tests directly, see below for correct class** + + These tests ensure that the data from the ViewSet is present for a + HTTP Request + """ + + http_options_response_list: dict = None + """The HTTP/Options Response for the ViewSet""" + + + + def test_api_render_field_allowed_methods_exists(self): + """Attribute Test + + Attribute `allowed_methods` must exist + """ + + assert 'allowed_methods' in self.http_options_response_list.data + + + def test_api_render_field_allowed_methods_not_empty(self): + """Attribute Test + + Attribute `allowed_methods` must return a value + """ + + assert len(self.http_options_response_list.data['allowed_methods']) > 0 + + + def test_api_render_field_allowed_methods_type(self): + """Attribute Test + + Attribute `allowed_methods` must be of type list + """ + + assert type(self.http_options_response_list.data['allowed_methods']) is list + + + def test_api_render_field_allowed_methods_values(self): + """Attribute Test + + Attribute `allowed_methods` only contains valid values + """ + + # Values valid for index views + valid_values: list = [ + 'GET', + 'HEAD', + 'OPTIONS', + ] + + all_valid: bool = True + + for method in list(self.http_options_response_list.data['allowed_methods']): + + if method not in valid_values: + + all_valid = False + + assert all_valid + + + + def test_api_render_field_view_description_exists(self): + """Attribute Test + + Attribute `description` must exist + """ + + assert 'description' in self.http_options_response_list.data + + + def test_api_render_field_view_description_not_empty(self): + """Attribute Test + + Attribute `view_description` must return a value + """ + + assert self.http_options_response_list.data['description'] is not None + + + def test_api_render_field_view_description_type(self): + """Attribute Test + + Attribute `view_description` must be of type str + """ + + assert type(self.http_options_response_list.data['description']) is str + + + + def test_api_render_field_view_name_exists(self): + """Attribute Test + + Attribute `view_name` must exist + """ + + assert 'name' in self.http_options_response_list.data + + + def test_api_render_field_view_name_not_empty(self): + """Attribute Test + + Attribute `view_name` must return a value + """ + + assert self.http_options_response_list.data['name'] is not None + + + def test_api_render_field_view_name_type(self): + """Attribute Test + + Attribute `view_name` must be of type str + """ + + assert type(self.http_options_response_list.data['name']) is str + + + +class ModelViewSet(AllViewSet): + """Tests for Model Viewsets + + **Dont include these tests directly, see below for correct class** + """ + + viewset = None + """ViewSet to Test""" + + + + def test_view_attr_filterset_fields_exists(self): + """Attribute Test + + Attribute `filterset_fields` must exist + """ + + assert hasattr(self.viewset, 'filterset_fields') + + + def test_view_attr_filterset_fields_not_empty(self): + """Attribute Test + + Attribute `filterset_fields` must return a value + """ + + assert self.viewset.filterset_fields is not None + + + def test_view_attr_filterset_fields_type(self): + """Attribute Test + + Attribute `filterset_fields` must be of type list + """ + + view_set = self.viewset() + + assert ( + type(view_set.filterset_fields) is list + ) + + + + def test_view_attr_allowed_methods_values(self): + """Attribute Test + + Attribute `allowed_methods` only contains valid values + """ + + # Values valid for model views + valid_values: list = [ + 'DELETE', + 'GET', + 'HEAD', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + ] + + all_valid: bool = True + + view_set = self.viewset() + + for method in list(view_set.allowed_methods): + + if method not in valid_values: + + all_valid = False + + assert all_valid + + + + def test_view_attr_model_exists(self): + """Attribute Test + + Attribute `model` must exist + """ + + assert hasattr(self.viewset, 'model') + + + def test_view_attr_model_not_empty(self): + """Attribute Test + + Attribute `model` must return a value + """ + + view_set = self.viewset() + + assert view_set.model is not None + + + + def test_view_attr_search_fields_exists(self): + """Attribute Test + + Attribute `search_fields` must exist + """ + + assert hasattr(self.viewset, 'search_fields') + + + def test_view_attr_search_fields_not_empty(self): + """Attribute Test + + Attribute `search_fields` must return a value + """ + + assert self.viewset.search_fields is not None + + + def test_view_attr_search_fields_type(self): + """Attribute Test + + Attribute `search_fields` must be of type list + """ + + view_set = self.viewset() + + assert ( + type(view_set.search_fields) is list + ) + + + + def test_view_attr_view_name_not_empty(self): + """Attribute Test + + Attribute `view_name` must return a value + """ + + view_set = self.viewset() + + assert ( + view_set.view_name is not None + or view_set.get_view_name() is not None + ) + + + def test_view_attr_view_name_type(self): + """Attribute Test + + Attribute `view_name` must be of type str + """ + + view_set = self.viewset() + + assert ( + type(view_set.view_name) is str + or type(view_set.get_view_name()) is str + ) + + + +class APIRenderModelViewSet(APIRenderViewSet): + """Tests for Model Viewsets + + **Dont include these tests directly, see below for correct class** + """ + + viewset = None + """ViewSet to Test""" + + + def test_api_render_field_allowed_methods_values(self): + """Attribute Test + + Attribute `allowed_methods` only contains valid values + """ + + # Values valid for model views + valid_values: list = [ + 'DELETE', + 'GET', + 'HEAD', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT', + ] + + all_valid: bool = True + + for method in list(self.http_options_response_list.data['allowed_methods']): + + if method not in valid_values: + + all_valid = False + + assert all_valid + + + +class ViewSetCommon( + AllViewSet, + APIRenderViewSet +): + """ Tests for Non-Model Viewsets + + **Include this class directly into Non-Model ViewSets** + + Args: + AllViewSet (class): Tests for all Viewsets. + APIRenderViewSet (class): Tests to check API Rendering to ensure data present. + """ + pass + + +class ViewSetModel( + ModelViewSet, + APIRenderModelViewSet +): + """Tests for model ViewSets + + **Include this class directly into Model ViewSets** + + Args: + ModelViewSet (class): Tests for Model Viewsets, includes `AllViewSet` tests. + APIRenderModelViewSet (class): Tests to check API rendering to ensure data is present, includes `APIRenderViewSet` tests. + """ + + pass diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 3238d4a67..334a78a4a 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -24,6 +24,10 @@ Views are used with Centurion ERP to Fetch the data for rendering. - views are documented at the class level for the swagger UI. +- Index Viewsets must be tested against tests `from api.tests.abstract.viewsets import ViewSetCommon` + +- Model VieSets must be tested against tests `from api.tests.abstract.viewsets import ViewSetModel` + ## Pre v1.3 Docs @@ -57,7 +61,7 @@ The views that we use are: Common test cases are available for views. These test cases can be found within the API docs under [model view test cases](./api/tests/model_views.md). -### Requirements +### Requirements. All views are to meet the following requirements: From 447c46eb47187a760c57a618c61c03c8dbdc1ff3 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 21:43:16 +0930 Subject: [PATCH 016/617] refactor(api): Split common ViewSet class into index/model classes ref: #345 #346 --- app/api/viewsets/common.py | 87 ++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index 3da07fc27..d48c8ac61 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -1,3 +1,5 @@ +from django.utils.safestring import mark_safe + from rest_framework import viewsets from access.mixin import OrganizationMixin @@ -9,7 +11,7 @@ class CommonViewSet( OrganizationMixin, - viewsets.ModelViewSet + viewsets.ViewSet ): """Common ViewSet class @@ -21,7 +23,7 @@ class CommonViewSet( """ @property - def allowed_methods(self) -> list: + def allowed_methods(self): """Allowed HTTP Methods _Optional_, HTTP Methods allowed for the `viewSet`. @@ -39,11 +41,6 @@ def allowed_methods(self) -> list: _Optional_, if specified will be add to list view metadata """ - filterset_fields: list = [] - """Fields to use for filtering the query - - _Optional_, if specified, these fields can be used to filter the API response - """ metadata_class = ReactUIMetadata """ Metadata Class @@ -52,6 +49,63 @@ def allowed_methods(self) -> list: required to generate the UI. """ + permission_classes = [ OrganizationPermissionAPI ] + """Permission Class + + _Mandatory_, Permission check class + """ + + view_description: str = None + + view_name: str = None + + + def get_view_description(self, html=False) -> str: + + if not self.view_description: + + self.view_description = "" + + if html: + + return mark_safe(f"

{self.view_description}

") + + else: + + return self.view_description + + + def get_view_name(self): + + if hasattr(self, 'model'): + + if self.detail: + + return self.model._meta.verbose_name + + return self.model._meta.verbose_name_plural + + if not self.view_name: + + return 'Error' + + return self.view_name + + + + +class ModelViewSet( + viewsets.ModelViewSet, + CommonViewSet +): + + + filterset_fields: list = [] + """Fields to use for filtering the query + + _Optional_, if specified, these fields can be used to filter the API response + """ + model: object = None """Django Model _Mandatory_, Django model used for this view. @@ -69,12 +123,6 @@ def allowed_methods(self) -> list: for detail view, Enables the UI can setup the page layout. """ - permission_classes = [ OrganizationPermissionAPI ] - """Permission Class - - _Mandatory_, Permission check class - """ - queryset: object = None """View Queryset @@ -88,7 +136,6 @@ def allowed_methods(self) -> list: """ - def get_model_documentation(self): if not self.model_documentation: @@ -104,7 +151,6 @@ def get_model_documentation(self): return self.model_documentation - def get_page_layout(self): if len(self.page_layout) < 1: @@ -120,7 +166,6 @@ def get_page_layout(self): return self.page_layout - def get_queryset(self): if not self.queryset: @@ -142,13 +187,3 @@ def get_serializer_class(self): return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] - - - - def get_view_name(self): - - if self.detail: - - return self.model._meta.verbose_name - - return self.model._meta.verbose_name_plural From b8c127d144d0bae8305f8bd1894fa0ade8a179a0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 21:51:32 +0930 Subject: [PATCH 017/617] feat(api): Add API v2 Endpoint ref: #248 #345 #346 --- app/api/urls.py | 5 +++++ app/api/viewsets/index.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 app/api/viewsets/index.py diff --git a/app/api/urls.py b/app/api/urls.py index efb92b515..5c3a7f791 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -29,6 +29,9 @@ from .views.itam import inventory +from api.viewsets import index as v2 + + app_name = "API" @@ -64,6 +67,8 @@ router.register('software', software.SoftwareViewSet, basename='software') +# API V2 +router.register('v2', v2.Index, basename='_api_v2_home') urlpatterns = [ diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py new file mode 100644 index 000000000..9bae208f2 --- /dev/null +++ b/app/api/viewsets/index.py @@ -0,0 +1,37 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = """Centurion ERP API V2. + + This endpoint will move to path `/api/` on release of + v2.0.0 of Centurion ERP. + """ + + view_name = "API v2" + + + def list(self, request, *args, **kwargs): + + return Response( + { + "access": "to do", + "assistance": reverse('API:_api_v2_assistance_home-list', request=request), + "itam": reverse('API:_api_v2_itam_home-list', request=request), + "settings": reverse('API:_api_v2_settings_home-list', request=request) + } + ) From 8a70ec1452b3df38aee78f2b38d075b9e2902746 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 21:51:54 +0930 Subject: [PATCH 018/617] test(api): Add API v2 Endpoint ref: #248 #345 #346 --- app/api/tests/unit/test_index_viewset.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/api/tests/unit/test_index_viewset.py diff --git a/app/api/tests/unit/test_index_viewset.py b/app/api/tests/unit/test_index_viewset.py new file mode 100644 index 000000000..f23a69db6 --- /dev/null +++ b/app/api/tests/unit/test_index_viewset.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from itam.viewset.index import Index + + +class HomeViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From d545efc0d3d00ae7de4bfa926b556f4f2689c5e5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 21:53:29 +0930 Subject: [PATCH 019/617] feat(api): Add React UI metadata class adds required items to HTTP/Options request ref: #345 #346 --- app/api/react_ui_metadata.py | 148 +++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 app/api/react_ui_metadata.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py new file mode 100644 index 000000000..b5bd4960f --- /dev/null +++ b/app/api/react_ui_metadata.py @@ -0,0 +1,148 @@ +from rest_framework import serializers +from rest_framework_json_api.metadata import JSONAPIMetadata +from rest_framework.request import clone_request +from rest_framework.utils.field_mapping import ClassLookupDict + +from api.v2.serializers.base.user import User, UserBaseSerializer + +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' + } + ) + + + +class ReactUIMetadata(OverRideJSONAPIMetadata): + + + def determine_metadata(self, request, view): + + metadata = {} + + metadata["name"] = view.get_view_name() + + metadata["description"] = view.get_view_description() + + 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() + + + if hasattr(view, 'get_model_documentation'): + + if view.get_model_documentation(): + + metadata['documentation'] = view.get_model_documentation() + + + elif view.suffix == 'List': + + if hasattr(view, 'model'): + + metadata['table_fields'] = view.model.table_fields + + if view.documentation: + + metadata['documentation'] = view.documentation + + + metadata['navigation'] = [ + { + "display_name": "Access", + "name": "access", + "pages": [ + { + "display_name": "Organization", + "name": "organization", + "icon": "device", + "link": "/access/organization" + } + ] + }, + { + "display_name": "Assistance", + "name": "assistance", + "pages": [ + { + "display_name": "Requests", + "name": "request", + "icon": "ticket", + "link": "/assistance/ticket/request" + } + ] + }, + { + "display_name": "ITAM", + "name": "itam", + "pages": [ + { + "display_name": "Devices", + "name": "device", + "icon": "device", + "link": "/itam/device" + } + ] + }, + + { + "display_name": "Settings", + "name": "settings", + "pages": [ + { + "display_name": "System", + "name": "system", + "icon": "settings", + "link": "/settings" + } + ] + } + ] + + + return metadata From 20b77b4b1eabff7da35ef7b2447fc5284691a002 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:10:36 +0930 Subject: [PATCH 020/617] feat(access): Add `table_fields` and `page_layout` to Organization ref: #345 #346 --- app/access/models.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/access/models.py b/app/access/models.py index 1b4bb5dfb..8118d1436 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -62,6 +62,44 @@ def get_organization(self): def __str__(self): return self.name + table_fields: list = [ + 'nbsp', + 'name', + 'created', + 'modified', + 'nbsp' + ] + + page_layout: list = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'name', + 'manager', + 'created', + 'modified', + ], + "right": [ + 'model_notes', + ] + } + ] + }, + { + "name": "Teams", + "slug": "teams", + "sections": [] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + } + ] class TenancyManager(models.Manager): From 3488d200b6493a2be83ba5b2f09755a592ca73a0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:12:23 +0930 Subject: [PATCH 021/617] feat(core): Add attribute `staatus_badge` to ticket model ref: #345 #346 --- app/core/models/ticket/ticket.py | 37 ++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index a3e532695..314aa0925 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -690,15 +690,17 @@ def linked_items(self) -> list(dict()): linked_items: list = [] - from core.models.ticket.ticket_linked_items import TicketLinkedItem + if self.pk: - items = TicketLinkedItem.objects.filter( - ticket = self - ) + from core.models.ticket.ticket_linked_items import TicketLinkedItem + + items = TicketLinkedItem.objects.filter( + ticket = self + ) - if len(items) > 0: + if len(items) > 0: - linked_items = items + linked_items = items return linked_items @@ -871,6 +873,29 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields signals.m2m_changed.connect(self.action_comment_ticket_teams, Ticket.subscribed_teams.through) + + @property + def status_badge(self): + + from core.classes.badge import Badge + + text:str = 'Add' + + if self.status: + + text:str = str(self.get_status_display()) + style:str = text.replace('(', '') + style = style.replace(')', '') + style = style.replace(' ', '_') + + return Badge( + icon_name = f'ticket_status_{style.lower()}', + icon_style = f'ticket-status-icon ticket-status-icon-{style.lower()}', + text = text, + text_style = f'ticket-status-text badge-text-ticket_status-{style.lower()}', + ) + + def ticketassigned(self, instance) -> bool: """ Check if the ticket has any assigned user(s)/team(s)""" From 95675da022bf1fc6dd2e152f273ea9ebf8ba51c2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:13:55 +0930 Subject: [PATCH 022/617] feat(core): Add `table_fields` to Ticket ref: #345 #346 --- app/core/models/ticket/ticket.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index 314aa0925..3dbdb37bf 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -1137,6 +1137,19 @@ class Related(models.IntegerChoices): verbose_name = 'Related Ticket', ) + table_fields: list = [ + 'id', + 'title', + 'status_badge', + 'opened_by', + 'organization', + 'created' + ] + + + # def __str__(self): + + # return '' @property def parent_object(self): From 8f2726aafb301dc8b5730abd580d0aec17497e43 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:14:15 +0930 Subject: [PATCH 023/617] feat(core): Add `table_fields` to Ticket Comment ref: #345 #346 --- app/core/models/ticket/ticket_comment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/models/ticket/ticket_comment.py b/app/core/models/ticket/ticket_comment.py index 6c517b54b..9bb07dd42 100644 --- a/app/core/models/ticket/ticket_comment.py +++ b/app/core/models/ticket/ticket_comment.py @@ -17,6 +17,7 @@ class TicketComment( TenancyObject, ): + table_fields: list = [] save_model_history: bool = False From a776fcc760dade3a6f26bd4ed9971970c072beb3 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:14:30 +0930 Subject: [PATCH 024/617] feat(core): Add `table_fields` to Ticket Linked Item ref: #345 #346 --- app/core/models/ticket/ticket_linked_items.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/core/models/ticket/ticket_linked_items.py b/app/core/models/ticket/ticket_linked_items.py index 759deda29..a8e508cce 100644 --- a/app/core/models/ticket/ticket_linked_items.py +++ b/app/core/models/ticket/ticket_linked_items.py @@ -66,6 +66,12 @@ class Modules(models.IntegerChoices): verbose_name = 'Item ID', ) + table_fields: list = [ + 'display_name', + 'status_badge', + 'created' + ] + def __str__(self) -> str: item_type: str = None From f8a1087af30d00063b6a98efa9b40c16652bc874 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:15:17 +0930 Subject: [PATCH 025/617] feat(itam): Add `table_fields` and `page_layout` to Device Model ref: #345 #346 --- app/itam/models/device_models.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/itam/models/device_models.py b/app/itam/models/device_models.py index f57771b95..b560648e8 100644 --- a/app/itam/models/device_models.py +++ b/app/itam/models/device_models.py @@ -31,6 +31,37 @@ class Meta: blank= True ) + table_fields: list = [ + 'manufacturer', + 'name', + 'organization', + 'created', + 'modified' + ] + + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'manufacturer', + 'name', + 'created', + 'modified', + ], + "right": [ + 'model_notes', + 'is_global', + ] + } + ] + } + ] + def clean(self): From b5c1e85258284b17bc3543757ac99cf4df7193bc Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:16:52 +0930 Subject: [PATCH 026/617] feat(itam): Add `table_fields` and `page_layout` to Device Model ref: #345 #346 --- app/itam/models/device.py | 88 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 4e86087dd..b7232a8ca 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -161,6 +161,94 @@ def validate_hostname_format(self): verbose_name = 'Is Virtual', ) + table_fields: list = [ + 'status_icon', + "name", + "device_model", + "device_type", + "organization", + "created", + "modified", + "model", + "nbsp" + ] + + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'device_type', + 'device_model', + 'name', + 'serial_number', + 'uuid', + 'inventorydate', + 'created', + 'modified', + ], + "right": [ + 'model_notes', + 'is_virtual', + 'is_global', + ] + }, + { + "layout": "table", + "name": "Dependent Services", + "field": "service", + }, + { + "layout": "single", + "fields": [ + 'config', + ] + } + ] + }, + { + "name": "Software", + "slug": "software", + "sections": [ + { + "layout": "table", + "field": "software", + } + ] + }, + { + "name": "Tickets", + "slug": "tickets", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ], + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + { + "name": "Config Management", + "slug": "config_management", + "sections": [ + { + "layout": "single", + "fields": [ + "rendered_config", + ] + } + ] + } + ] + def save( self, force_insert=False, force_update=False, using=None, update_fields=None From b827dfac61f7186f26adbad84e701f41f1735c62 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:17:49 +0930 Subject: [PATCH 027/617] feat(core): Add `table_fields` to History Model ref: #345 #346 --- app/core/models/history.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/core/models/history.py b/app/core/models/history.py index 9acd98174..ec2eeb273 100644 --- a/app/core/models/history.py +++ b/app/core/models/history.py @@ -93,3 +93,16 @@ class Actions(models.TextChoices): max_length = 50, unique = False, ) + + + table_fields: list = [ + 'created', + 'action', + 'item_class', + 'user', + 'nbsp', + [ + 'before', + 'after' + ] + ] From 36d044dc4347b32732a7637de523c3b1b4cf3f4b Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:18:32 +0930 Subject: [PATCH 028/617] refactor(core): Change history fields after and before to be JSON fields ref: #345 #346 --- ...lter_history_after_alter_history_before.py | 23 +++++++++++++++++++ app/core/models/history.py | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 app/core/migrations/0007_alter_history_after_alter_history_before.py diff --git a/app/core/migrations/0007_alter_history_after_alter_history_before.py b/app/core/migrations/0007_alter_history_after_alter_history_before.py new file mode 100644 index 000000000..348d614ba --- /dev/null +++ b/app/core/migrations/0007_alter_history_after_alter_history_before.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2024-10-11 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_ticket_milestone_ticket_opened_by_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='history', + name='after', + field=models.JSONField(blank=True, default=None, help_text='JSON Object After Change', null=True), + ), + migrations.AlterField( + model_name='history', + name='before', + field=models.JSONField(blank=True, default=None, help_text='JSON Object before Change', null=True), + ), + ] diff --git a/app/core/models/history.py b/app/core/models/history.py index ec2eeb273..c986c33ae 100644 --- a/app/core/models/history.py +++ b/app/core/models/history.py @@ -35,7 +35,7 @@ class Actions(models.TextChoices): DELETE = '3', 'Delete' - before = models.TextField( + before = models.JSONField( help_text = 'JSON Object before Change', blank = True, default = None, @@ -43,7 +43,7 @@ class Actions(models.TextChoices): ) - after = models.TextField( + after = models.JSONField( help_text = 'JSON Object After Change', blank = True, default = None, From 766706268bbc0d5a3d1bd0bdd68a010cf98da2c0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:19:06 +0930 Subject: [PATCH 029/617] feat(core): Add `table_fields` to Notes Model ref: #345 #346 --- app/core/models/notes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/models/notes.py b/app/core/models/notes.py index c6ad8d489..71a5782d5 100644 --- a/app/core/models/notes.py +++ b/app/core/models/notes.py @@ -118,6 +118,7 @@ class Meta: blank= True ) + table_fields: list = [] def __str__(self): From fa9a36dffabfea9130540d278b1127f5f7665a5f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:20:34 +0930 Subject: [PATCH 030/617] refactor(itam): Cleanup Device Software model field names. ref: #345 #346 --- ...05_alter_devicesoftware_action_and_more.py | 34 +++++++++++++++++++ app/itam/models/device.py | 30 +++++++++------- 2 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 app/itam/migrations/0005_alter_devicesoftware_action_and_more.py diff --git a/app/itam/migrations/0005_alter_devicesoftware_action_and_more.py b/app/itam/migrations/0005_alter_devicesoftware_action_and_more.py new file mode 100644 index 000000000..5db66b7c6 --- /dev/null +++ b/app/itam/migrations/0005_alter_devicesoftware_action_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.2 on 2024-10-11 16:03 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0004_alter_deviceoperatingsystem_device_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='devicesoftware', + name='action', + field=models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, help_text='Action to perform', max_length=1, null=True, verbose_name='Action'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='installed', + field=models.DateTimeField(blank=True, help_text='Date detected as installed', null=True, verbose_name='Date Installed'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='installedversion', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='installedversion', to='itam.softwareversion', verbose_name='Installed Version'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='version', + field=models.ForeignKey(blank=True, default=None, help_text='Version to install', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion', verbose_name='Desired Version'), + ), + ] diff --git a/app/itam/models/device.py b/app/itam/models/device.py index b7232a8ca..5c726bde8 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -450,48 +450,54 @@ class Actions(models.TextChoices): device = models.ForeignKey( Device, + blank= False, on_delete=models.CASCADE, null = False, - blank= False ) software = models.ForeignKey( Software, - on_delete=models.CASCADE, + blank= False, null = False, - blank= False + on_delete=models.CASCADE, ) action = models.CharField( - max_length=1, + blank = True, choices=Actions, default=None, + help_text = 'Action to perform', + max_length=1, null=True, - blank = True, + verbose_name = 'Action', ) version = models.ForeignKey( SoftwareVersion, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Version to install', + on_delete=models.CASCADE, null = True, - blank= True + verbose_name = 'Desired Version' ) installedversion = models.ForeignKey( SoftwareVersion, - related_name = 'installedversion', - on_delete=models.CASCADE, + blank= True, default = None, null = True, - blank= True + on_delete=models.CASCADE, + related_name = 'installedversion', + verbose_name = 'Installed Version' ) installed = models.DateTimeField( - verbose_name = 'Install Date', + blank = True, + help_text = 'Date detected as installed', null = True, - blank = True + verbose_name = 'Date Installed' ) @property From a93e5625be0f6bb4e4bd17c4d19ab38eeaece1db Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:20:57 +0930 Subject: [PATCH 031/617] feat(core): Add `table_fields` to Device Software Model ref: #345 #346 --- app/itam/models/device.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 5c726bde8..1d5389725 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -500,6 +500,19 @@ class Actions(models.TextChoices): verbose_name = 'Date Installed' ) + + table_fields: list = [ + "nbsp", + "software", + "category", + "action_badge", + "version", + "installedversion", + "installed", + "nbsp" + ] + + @property def action_badge(self): From 4a27360b7c03d1bea19059652714b71dfa07966e Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:21:46 +0930 Subject: [PATCH 032/617] fix(itam): Add missing model.Meta attributes ordering and verbose_name ref: #345 #346 --- app/itam/models/device.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 1d5389725..ceff58d57 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -52,6 +52,13 @@ class Device(DeviceCommonFieldsName, SaveHistory): class Meta: + ordering = [ + 'name', + 'organization' + ] + + verbose_name = 'Device' + verbose_name_plural = 'Devices' From 8b0bcfa886f8d8a871532a58062b6d6d5f71ee82 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:22:38 +0930 Subject: [PATCH 033/617] feat(itim): Add `table_fields` to Service Model ref: #345 #346 --- app/itim/models/services.py | 9 +++++++++ docs/projects/centurion_erp/development/views.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index b6bbf25b9..72d5cd7fb 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -199,6 +199,15 @@ def validate_config_key_variable(value): modified = AutoLastModifiedField() + + table_fields: list = [ + "nbsp", + "name", + "port", + "nbsp" + ] + + @property def config_variables(self): diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 334a78a4a..471dd04f5 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -61,7 +61,7 @@ The views that we use are: Common test cases are available for views. These test cases can be found within the API docs under [model view test cases](./api/tests/model_views.md). -### Requirements. +### Requirements - Depreciated All views are to meet the following requirements: From 73d56692d11548dff0b87a350b890e7da3d7912e Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:34:48 +0930 Subject: [PATCH 034/617] feat(access): Add v2 endpoint Access ref: #345 #346 --- app/access/viewset/index.py | 30 ++++++++++++++++++++++++++++++ app/api/urls.py | 4 ++++ app/api/viewsets/index.py | 4 ++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 app/access/viewset/index.py diff --git a/app/access/viewset/index.py b/app/access/viewset/index.py new file mode 100644 index 000000000..2a1824b8d --- /dev/null +++ b/app/access/viewset/index.py @@ -0,0 +1,30 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = "Access Module" + + view_name = "Access" + + + def list(self, request, pk=None): + + return Response( + { + "organization": "ToDo" + } + ) diff --git a/app/api/urls.py b/app/api/urls.py index 5c3a7f791..9e72bae97 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -31,6 +31,8 @@ from api.viewsets import index as v2 +from access.viewset import index as access_v2 + app_name = "API" @@ -70,6 +72,8 @@ # API V2 router.register('v2', v2.Index, basename='_api_v2_home') +router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') + urlpatterns = [ path("assistance", assistance.index.Index.as_view(), name="_api_assistance"), diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py index 9bae208f2..8586ef42c 100644 --- a/app/api/viewsets/index.py +++ b/app/api/viewsets/index.py @@ -21,7 +21,7 @@ class Index(CommonViewSet): This endpoint will move to path `/api/` on release of v2.0.0 of Centurion ERP. """ - + view_name = "API v2" @@ -29,7 +29,7 @@ def list(self, request, *args, **kwargs): return Response( { - "access": "to do", + "access": reverse('API:_api_v2_access_home-list', request=request), "assistance": reverse('API:_api_v2_assistance_home-list', request=request), "itam": reverse('API:_api_v2_itam_home-list', request=request), "settings": reverse('API:_api_v2_settings_home-list', request=request) From 374c18d997deb7160d861b48d0baac77f641b421 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:35:59 +0930 Subject: [PATCH 035/617] test(access): Add index viewset checks ref: #345 #346 --- app/access/tests/unit/test_access_viewset.py | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/access/tests/unit/test_access_viewset.py diff --git a/app/access/tests/unit/test_access_viewset.py b/app/access/tests/unit/test_access_viewset.py new file mode 100644 index 000000000..6fa10e1aa --- /dev/null +++ b/app/access/tests/unit/test_access_viewset.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from access.viewset.index import Index + + +class AccessViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_access_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From aa57005013decd64cde8427aa2d5f7d0931a9d93 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:41:52 +0930 Subject: [PATCH 036/617] feat(assistance): Add v2 endpoint Assistance ref: #345 #346 --- app/api/urls.py | 4 ++++ app/assistance/viewset/index.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 app/assistance/viewset/index.py diff --git a/app/api/urls.py b/app/api/urls.py index 9e72bae97..c941fca3e 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -33,6 +33,8 @@ from access.viewset import index as access_v2 +from assistance.viewset import index as assistance_index_v2 + app_name = "API" @@ -74,6 +76,8 @@ router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') +router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') + urlpatterns = [ path("assistance", assistance.index.Index.as_view(), name="_api_assistance"), diff --git a/app/assistance/viewset/index.py b/app/assistance/viewset/index.py new file mode 100644 index 000000000..1195a6f7b --- /dev/null +++ b/app/assistance/viewset/index.py @@ -0,0 +1,31 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = "Assistance Module" + + view_name = "Assistance" + + + def list(self, request, pk=None): + + return Response( + { + "knowledge_base": "ToDo", + "request": "ToDo" + } + ) From af9d99774ec670b19b95ebf5ccf9dddaefccb5ee Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:42:08 +0930 Subject: [PATCH 037/617] test(assistance): Add index viewset checks ref: #345 #346 --- .../tests/unit/test_assistance_viewset.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/assistance/tests/unit/test_assistance_viewset.py diff --git a/app/assistance/tests/unit/test_assistance_viewset.py b/app/assistance/tests/unit/test_assistance_viewset.py new file mode 100644 index 000000000..1884586c9 --- /dev/null +++ b/app/assistance/tests/unit/test_assistance_viewset.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from assistance.viewset.index import Index + + +class AccessViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_access_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From 976f5da4460d3551ac740dacfaebcc86ce54c8d2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:52:15 +0930 Subject: [PATCH 038/617] feat(config_management): Add v2 endpoint Config Management ref: #345 #346 --- app/api/urls.py | 5 ++++ app/api/viewsets/index.py | 1 + .../tests/unit/test_assistance_viewset.py | 2 +- app/config_management/viewset/index.py | 30 +++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 app/config_management/viewset/index.py diff --git a/app/api/urls.py b/app/api/urls.py index c941fca3e..cd4ea7545 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -35,6 +35,8 @@ from assistance.viewset import index as assistance_index_v2 +from config_management.viewset import index as config_management_v2 + app_name = "API" @@ -78,6 +80,9 @@ router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') +router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') + + urlpatterns = [ path("assistance", assistance.index.Index.as_view(), name="_api_assistance"), diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py index 8586ef42c..be4689638 100644 --- a/app/api/viewsets/index.py +++ b/app/api/viewsets/index.py @@ -32,6 +32,7 @@ def list(self, request, *args, **kwargs): "access": reverse('API:_api_v2_access_home-list', request=request), "assistance": reverse('API:_api_v2_assistance_home-list', request=request), "itam": reverse('API:_api_v2_itam_home-list', request=request), + "config_management": reverse('API:_api_v2_config_management_home-list', request=request), "settings": reverse('API:_api_v2_settings_home-list', request=request) } ) diff --git a/app/assistance/tests/unit/test_assistance_viewset.py b/app/assistance/tests/unit/test_assistance_viewset.py index 1884586c9..ef7aebf34 100644 --- a/app/assistance/tests/unit/test_assistance_viewset.py +++ b/app/assistance/tests/unit/test_assistance_viewset.py @@ -9,7 +9,7 @@ from assistance.viewset.index import Index -class AccessViewset( +class AssistanceViewset( TestCase, ViewSetCommon ): diff --git a/app/config_management/viewset/index.py b/app/config_management/viewset/index.py new file mode 100644 index 000000000..8ceb7051b --- /dev/null +++ b/app/config_management/viewset/index.py @@ -0,0 +1,30 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = "Configuration Management Module" + + view_name = "Configuration Management" + + + def list(self, request, pk=None): + + return Response( + { + "group": "ToDo", + } + ) From 039ae89814ea28ed2bfece1601c5a5c534f85c05 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 22:54:45 +0930 Subject: [PATCH 039/617] test(config_management): Add index viewset checks ref: #345 #346 --- .../tests/unit/test_assistance_viewset.py | 2 +- .../unit/test_config_management_viewset.py | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 app/config_management/tests/unit/test_config_management_viewset.py diff --git a/app/assistance/tests/unit/test_assistance_viewset.py b/app/assistance/tests/unit/test_assistance_viewset.py index ef7aebf34..061c01c2b 100644 --- a/app/assistance/tests/unit/test_assistance_viewset.py +++ b/app/assistance/tests/unit/test_assistance_viewset.py @@ -16,7 +16,7 @@ class AssistanceViewset( viewset = Index - route_name = 'API:_api_v2_access_home' + route_name = 'API:_api_v2_assistance_home' @classmethod diff --git a/app/config_management/tests/unit/test_config_management_viewset.py b/app/config_management/tests/unit/test_config_management_viewset.py new file mode 100644 index 000000000..0cca53167 --- /dev/null +++ b/app/config_management/tests/unit/test_config_management_viewset.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from config_management.viewset.index import Index + + +class ConfigManagementViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_config_management_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From 1df9589d559b9e6c9c056fadbc577470bd1c47f7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:06:31 +0930 Subject: [PATCH 040/617] feat(itim): Add v2 endpoint ITIM ref: #345 #346 --- app/api/urls.py | 22 ++++++++++++++++++---- app/api/viewsets/index.py | 1 + app/itim/viewsets/index.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 app/itim/viewsets/index.py diff --git a/app/api/urls.py b/app/api/urls.py index cd4ea7545..c5a65c3ce 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -29,13 +29,25 @@ from .views.itam import inventory -from api.viewsets import index as v2 +from api.viewsets import ( + index as v2 +) -from access.viewset import index as access_v2 +from access.viewset import ( + index as access_v2 +) -from assistance.viewset import index as assistance_index_v2 +from assistance.viewset import ( + index as assistance_index_v2 +) -from config_management.viewset import index as config_management_v2 +from config_management.viewset import ( + index as config_management_v2 +) + +from itim.viewsets import ( + index as itim_v2 +) app_name = "API" @@ -80,6 +92,8 @@ router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') +router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') + router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py index be4689638..610619f6c 100644 --- a/app/api/viewsets/index.py +++ b/app/api/viewsets/index.py @@ -32,6 +32,7 @@ def list(self, request, *args, **kwargs): "access": reverse('API:_api_v2_access_home-list', request=request), "assistance": reverse('API:_api_v2_assistance_home-list', request=request), "itam": reverse('API:_api_v2_itam_home-list', request=request), + "itim": reverse('API:_api_v2_itim_home-list', request=request), "config_management": reverse('API:_api_v2_config_management_home-list', request=request), "settings": reverse('API:_api_v2_settings_home-list', request=request) } diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py new file mode 100644 index 000000000..0e6ff9024 --- /dev/null +++ b/app/itim/viewsets/index.py @@ -0,0 +1,34 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = "Information Technology Infrastructure Management (ITIM) Module" + + view_name = "ITIM" + + + def list(self, request, pk=None): + + return Response( + { + "change": "ToDo", + "cluster": "ToDo", + "incident": "ToDo", + "problem": "ToDo", + "service": "ToDo", + } + ) From 3a3ec331d79422be3798aaffe4bd86b677095cbe Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:06:44 +0930 Subject: [PATCH 041/617] test(itim): Add index viewset checks ref: #345 #346 --- app/itim/tests/unit/test_itim_viewset.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/itim/tests/unit/test_itim_viewset.py diff --git a/app/itim/tests/unit/test_itim_viewset.py b/app/itim/tests/unit/test_itim_viewset.py new file mode 100644 index 000000000..012f6cc48 --- /dev/null +++ b/app/itim/tests/unit/test_itim_viewset.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from itim.viewsets.index import Index + + +class ITIMViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_itim_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From 11cf3a11fc2ead31570d4c81e8b5b03b37e4208b Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:14:22 +0930 Subject: [PATCH 042/617] feat(project_management): Add v2 endpoint Project Management ref: #345 #346 --- app/api/urls.py | 5 ++++ app/api/viewsets/index.py | 1 + app/project_management/viewsets/index.py | 30 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 app/project_management/viewsets/index.py diff --git a/app/api/urls.py b/app/api/urls.py index c5a65c3ce..f97f898f8 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -49,6 +49,10 @@ index as itim_v2 ) +from project_management.viewsets import ( + index as project_management_v2 +) + app_name = "API" @@ -96,6 +100,7 @@ router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') +router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') urlpatterns = [ diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py index 610619f6c..a5456003e 100644 --- a/app/api/viewsets/index.py +++ b/app/api/viewsets/index.py @@ -34,6 +34,7 @@ def list(self, request, *args, **kwargs): "itam": reverse('API:_api_v2_itam_home-list', request=request), "itim": reverse('API:_api_v2_itim_home-list', request=request), "config_management": reverse('API:_api_v2_config_management_home-list', request=request), + "project_management": reverse('API:_api_v2_project_management_home-list', request=request), "settings": reverse('API:_api_v2_settings_home-list', request=request) } ) diff --git a/app/project_management/viewsets/index.py b/app/project_management/viewsets/index.py new file mode 100644 index 000000000..ba979e831 --- /dev/null +++ b/app/project_management/viewsets/index.py @@ -0,0 +1,30 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = "Project Management Module" + + view_name = "Project Management" + + + def list(self, request, pk=None): + + return Response( + { + "project": "ToDo", + } + ) From 1b5411136dee8f1b06a4854e1fab8db66309b74a Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:15:51 +0930 Subject: [PATCH 043/617] test(project_management): Add index viewset checks ref: #345 #346 --- .../unit/test_project_management_viewset.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/project_management/tests/unit/test_project_management_viewset.py diff --git a/app/project_management/tests/unit/test_project_management_viewset.py b/app/project_management/tests/unit/test_project_management_viewset.py new file mode 100644 index 000000000..121a7b50d --- /dev/null +++ b/app/project_management/tests/unit/test_project_management_viewset.py @@ -0,0 +1,43 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from itim.viewsets.index import Index + + + +class ProjectManagementViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_project_management_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From 244ae6c3f9b0af1ff2e1e4d30224d0f442e59447 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:22:36 +0930 Subject: [PATCH 044/617] feat(settings): Add v2 endpoint Settings ref: #345 #346 --- app/api/urls.py | 6 ++++ app/settings/viewsets/index.py | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 app/settings/viewsets/index.py diff --git a/app/api/urls.py b/app/api/urls.py index f97f898f8..c76b90ee6 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -53,6 +53,10 @@ index as project_management_v2 ) +from settings.viewsets import ( + index as settings_index_v2, +) + app_name = "API" @@ -102,6 +106,8 @@ router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') +router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') + urlpatterns = [ path("assistance", assistance.index.Index.as_view(), name="_api_assistance"), diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py new file mode 100644 index 000000000..0801ed34c --- /dev/null +++ b/app/settings/viewsets/index.py @@ -0,0 +1,50 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + page_layout: list = [ + { + "name": "Core", + "links": [ + { + "name": "External Links", + "model": "external_link" + } + ] + }, + { + "name": "ITAM", + "links": [ + { + "name": "Device Model", + "model": "device_model" + } + ] + } + ] + + view_description = "Centurion ERP Settings" + + view_name = "Settings" + + + def list(self, request, pk=None): + + return Response( + { + } + ) From 8f9682b0c4b76c0050ecd03ac6694777c52c5737 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:22:51 +0930 Subject: [PATCH 045/617] test(Settings): Add index viewset checks ref: #345 #346 --- .../tests/unit/test_settings_viewset.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/settings/tests/unit/test_settings_viewset.py diff --git a/app/settings/tests/unit/test_settings_viewset.py b/app/settings/tests/unit/test_settings_viewset.py new file mode 100644 index 000000000..3d3c69e29 --- /dev/null +++ b/app/settings/tests/unit/test_settings_viewset.py @@ -0,0 +1,43 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from itim.viewsets.index import Index + + + +class SettingsViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_settings_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From d23c2907cf51f2b0dcc5343e7310db2ec2b4981d Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:32:35 +0930 Subject: [PATCH 046/617] feat(base): Add User Serializer ref: #345 #346 --- app/api/react_ui_metadata.py | 2 +- app/app/serializers/user.py | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/app/serializers/user.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index b5bd4960f..9eff7bb75 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -3,7 +3,7 @@ from rest_framework.request import clone_request from rest_framework.utils.field_mapping import ClassLookupDict -from api.v2.serializers.base.user import User, UserBaseSerializer +from app.serializers.user import User, UserBaseSerializer from core.fields.badge import BadgeField from core.fields.icon import IconField diff --git a/app/app/serializers/user.py b/app/app/serializers/user.py new file mode 100644 index 000000000..721a5cbda --- /dev/null +++ b/app/app/serializers/user.py @@ -0,0 +1,39 @@ +from django.contrib.auth.models import User + +from rest_framework import serializers + + + +class UserBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + + class Meta: + + model = User + + fields = '__all__' + + fields = [ + 'id', + 'display_name', + 'first_name', + 'last_name', + 'username', + 'is_active', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'first_name', + 'last_name', + 'username', + 'is_active', + ] From 97afeeb45d1205fd2df4668320ec033b0063929b Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:34:43 +0930 Subject: [PATCH 047/617] feat(itam): Add v2 endpoint ITAM ref: #345 #346 --- app/api/urls.py | 6 ++++++ app/itam/viewset/index.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 app/itam/viewset/index.py diff --git a/app/api/urls.py b/app/api/urls.py index c76b90ee6..1cbc80aca 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -45,6 +45,10 @@ index as config_management_v2 ) +from itam.viewset import ( + index as itam_index_v2, +) + from itim.viewsets import ( index as itim_v2 ) @@ -100,6 +104,8 @@ router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') +router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') + router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') diff --git a/app/itam/viewset/index.py b/app/itam/viewset/index.py new file mode 100644 index 000000000..5667b44b8 --- /dev/null +++ b/app/itam/viewset/index.py @@ -0,0 +1,30 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'HEAD', + 'OPTIONS' + ] + + view_description = "Information Technology Asset Management (ITAM)" + + view_name = "ITAM" + + + def list(self, request, pk=None): + + return Response( + { + "device": reverse('API:_api_v2_device-list', request=request) + } + ) From fe6a405a413503fdc4dcfff6839b242512d4fb38 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:36:50 +0930 Subject: [PATCH 048/617] test(itam): Add index viewset checks ref: #345 #346 --- app/itam/tests/unit/test_itam_viewset.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/itam/tests/unit/test_itam_viewset.py diff --git a/app/itam/tests/unit/test_itam_viewset.py b/app/itam/tests/unit/test_itam_viewset.py new file mode 100644 index 000000000..1387f33c2 --- /dev/null +++ b/app/itam/tests/unit/test_itam_viewset.py @@ -0,0 +1,42 @@ +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization + +from api.tests.abstract.viewsets import ViewSetCommon + +from itam.viewset.index import Index + + +class ItamViewset( + TestCase, + ViewSetCommon +): + + viewset = Index + + route_name = 'API:_api_v2_itam_home' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user + 3. create super user + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.view_user = User.objects.create_user(username="test_user_add", password="password", is_superuser=True) + + + client = Client() + url = reverse(self.route_name + '-list') + + + client.force_login(self.view_user) + self.http_options_response_list = client.options(url) \ No newline at end of file From 030bb13396658094ea9cde9b312c276246abbdf4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 12 Oct 2024 23:40:24 +0930 Subject: [PATCH 049/617] test(api): fix index import to correct viewset ref: #345 #346 --- app/api/tests/unit/test_index_viewset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/tests/unit/test_index_viewset.py b/app/api/tests/unit/test_index_viewset.py index f23a69db6..0721f964c 100644 --- a/app/api/tests/unit/test_index_viewset.py +++ b/app/api/tests/unit/test_index_viewset.py @@ -6,7 +6,7 @@ from api.tests.abstract.viewsets import ViewSetCommon -from itam.viewset.index import Index +from api.viewsets.index import Index class HomeViewset( From cf8014a26b9db104b784e354341244dfc2c2d25d Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 00:03:32 +0930 Subject: [PATCH 050/617] fix(api): during permission checking dont attempt to access view obj if it doesn't exist ref: #345 #346 --- app/api/views/mixin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index f93686423..2e0feb479 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -78,9 +78,11 @@ def permission_check(self, request, view, obj=None) -> bool: action = 'view' - permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name + if hasattr(self, 'obj'): - self.permission_required = [ permission ] + permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name + + self.permission_required = [ permission ] if hasattr(view, 'get_dynamic_permissions'): From ad1b35dfc7a0b9d29f4a68dccc8cef8ddbce224e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 00:07:14 +0930 Subject: [PATCH 051/617] fix(api): during permission checking if request is HTTP/Options and user is authenticated, allow access ref: #345 #346 --- app/api/views/mixin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index 2e0feb479..d0dec1990 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -35,6 +35,10 @@ def permission_check(self, request, view, obj=None) -> bool: view.http_method_not_allowed(request._request) + if request.user.is_authenticated and method == 'options': + + return True + if hasattr(view, 'get_queryset'): queryset = view.get_queryset() From 37176458ac764fdcffe033c156f247c9d2693db0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 00:39:51 +0930 Subject: [PATCH 052/617] feat(core): Add `table_fields` to Ticket Model ref: #345 #346 --- app/core/models/ticket/ticket.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index 3dbdb37bf..55a408008 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -559,6 +559,15 @@ def validation_title(field): # ?? date_edit date of last edit + table_fields: list = [ + 'id', + 'title', + 'status_badge', + 'opened_by', + 'organization', + 'created' + ] + def __str__(self): return self.title From 67b8648a69847402de08c334e9442ce188c093c6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 14:43:56 +0930 Subject: [PATCH 053/617] test(api): viewset documentation attr check ref: #345 #346 --- app/api/tests/abstract/viewsets.py | 82 ++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/app/api/tests/abstract/viewsets.py b/app/api/tests/abstract/viewsets.py index 17fdbb635..48d4bf9ef 100644 --- a/app/api/tests/abstract/viewsets.py +++ b/app/api/tests/abstract/viewsets.py @@ -74,34 +74,6 @@ def test_view_attr_allowed_methods_values(self): - def test_view_attr_view_description_exists(self): - """Attribute Test - - Attribute `view_description` must exist - """ - - assert hasattr(self.viewset, 'view_description') - - - def test_view_attr_view_description_not_empty(self): - """Attribute Test - - Attribute `view_description` must return a value - """ - - assert self.viewset.view_description is not None - - - def test_view_attr_view_description_type(self): - """Attribute Test - - Attribute `view_description` must be of type str - """ - - assert type(self.viewset.view_description) is str - - - def test_view_attr_metadata_class_exists(self): """Attribute Test @@ -179,6 +151,34 @@ def test_view_attr_permission_classes_value(self): + def test_view_attr_view_description_exists(self): + """Attribute Test + + Attribute `view_description` must exist + """ + + assert hasattr(self.viewset, 'view_description') + + + def test_view_attr_view_description_not_empty(self): + """Attribute Test + + Attribute `view_description` must return a value + """ + + assert self.viewset.view_description is not None + + + def test_view_attr_view_description_type(self): + """Attribute Test + + Attribute `view_description` must be of type str + """ + + assert type(self.viewset.view_description) is str + + + def test_view_attr_view_name_exists(self): """Attribute Test @@ -345,6 +345,32 @@ class ModelViewSet(AllViewSet): + def test_view_attr_documentation_exists(self): + """Attribute Test + + Attribute `documentation` must exist + """ + + assert hasattr(self.viewset, 'documentation') + + + def test_view_attr_documentation_type(self): + """Attribute Test + + Attribute `documentation` must be of type str or None. + + this attribute is optional. + """ + + view_set = self.viewset() + + assert ( + type(view_set.documentation) is str + or type(view_set.documentation) is None + ) + + + def test_view_attr_filterset_fields_exists(self): """Attribute Test From 72c42f07cb40b1b6338129b0af23b3aeffec5cb9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 14:56:17 +0930 Subject: [PATCH 054/617] test(api): Ensure models have `Meta.ordering` set and not empty ref: #345 #346 --- app/app/tests/abstract/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index d72cbfe4b..4cb1ba379 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -30,6 +30,36 @@ def test_class_inherits_save_history(self): assert issubclass(self.model, TenancyObject) + def test_attribute_exists_ordering(self): + """Test for existance of field in `.Meta` + + Attribute `ordering` must be defined in `Meta` class. + """ + + assert 'ordering' in self.model._meta.original_attrs + + + def test_attribute_not_empty_ordering(self): + """Test field `.Meta` is not empty + + Attribute `ordering` must contain values + """ + + assert ( + self.model._meta.original_attrs['ordering'] is not None + and len(list(self.model._meta.original_attrs['ordering'])) > 0 + ) + + + def test_attribute_type_ordering(self): + """Test field `.Meta` is not empty + + Attribute `ordering` must be of type list. + """ + + assert type(self.model._meta.original_attrs['ordering']) is list + + class TenancyModel( BaseModel, From 617bbcc7244704a1cbe60fa2844effa2e0f43b7c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 15:20:15 +0930 Subject: [PATCH 055/617] fix(access): Add missing meta field verbose_name to Team model ref: #248 #345 #346 --- app/access/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/access/models.py b/app/access/models.py index 8118d1436..9fdda79a9 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -249,10 +249,14 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields class Team(Group, TenancyObject): + class Meta: - # proxy = True + + ordering = [ 'team_name' ] + + verbose_name = 'Team' + verbose_name_plural = "Teams" - ordering = ['team_name'] def save(self, force_insert=False, force_update=False, using=None, update_fields=None): From af27b55ad735a49b2ccafa88f8dd546ccf3dd946 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 15:21:09 +0930 Subject: [PATCH 056/617] feat(access): Add attribute page_layout to Team model ref: #248 #345 #346 --- app/access/models.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/access/models.py b/app/access/models.py index 9fdda79a9..4785dcd34 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -278,6 +278,36 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'team_name', + 'created', + 'modified', + ], + "right": [ + 'model_notes', + ] + }, + { + "layout": "table", + "name": "Users", + "field": "user", + }, + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] @property def parent_object(self): From ebf51da951249861efb32e994fdd5555f2736ab5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 15:21:26 +0930 Subject: [PATCH 057/617] feat(access): Add attribute table_fields to Team model ref: #248 #345 #346 --- app/access/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/access/models.py b/app/access/models.py index 4785dcd34..afcfe3413 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -309,6 +309,12 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields }, ] + table_fields: list = [ + 'team_name', + 'modified', + 'created', + ] + @property def parent_object(self): """ Fetch the parent object """ From e52a9c0003c52cb10005a5270fbe21d42183c5fa Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 15:53:26 +0930 Subject: [PATCH 058/617] feat(access): Add attribute page_layout to KB Category model ref: #248 #345 #346 --- app/assistance/models/knowledge_base.py | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index 624fc56dd..00f73dc85 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -70,6 +70,45 @@ class Meta: modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'title', + 'parent_category', + 'target_user', + 'target_team', + 'created', + 'modified', + ], + "right": [ + 'model_notes', + 'organization', + ] + } + ] + }, + { + "name": "Articles", + "slug": "article", + "sections": [ + { + "layout": "table", + "field": "articles", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.name From 8824fcebdfaeb65d2bf6611ba4c2067db6539964 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 15:53:44 +0930 Subject: [PATCH 059/617] feat(access): Add attribute table_fields to KB Category model ref: #248 #345 #346 --- app/assistance/models/knowledge_base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index 00f73dc85..5eeca1876 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -109,6 +109,13 @@ class Meta: }, ] + table_fields: list = [ + 'title', + 'parent', + 'organization', + ] + + def __str__(self): return self.name From d5a4a570e3785d29a309500563de615ee2df262b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:01:40 +0930 Subject: [PATCH 060/617] feat(access): Add attribute page_layout to KB model ref: #248 #345 #346 --- app/assistance/models/knowledge_base.py | 56 +++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index 5eeca1876..bee9fa5b0 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -260,6 +260,62 @@ class Meta: modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'title', + 'category', + 'responsible_user', + 'responsible_teams', + 'is_global', + 'created', + 'modified', + ], + "right": [ + 'model_notes', + 'release_date', + 'expiry_date', + 'target_user', + 'target_team', + ] + }, + { + "layout": "single", + "fields": [ + 'summary', + ] + }, + { + "layout": "single", + "fields": [ + 'content', + ] + } + ] + }, + { + "name": "Articles", + "slug": "article", + "sections": [ + { + "layout": "table", + "field": "articles", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.title From 8e0af707cf5f5ac76d3322315d4057a38b8ee7f6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:02:02 +0930 Subject: [PATCH 061/617] feat(access): Add attribute table_fields to KB model ref: #248 #345 #346 --- app/assistance/models/knowledge_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index bee9fa5b0..9dd7c6593 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -316,6 +316,15 @@ class Meta: }, ] + table_fields: list = [ + 'title', + 'category', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return self.title From 3080b1c1b72f5323991ca31b44c1863ccf910ce9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:15:03 +0930 Subject: [PATCH 062/617] fix(access): Add missing attribute Meta.ordering Config Groups model ref: #248 #345 #346 --- .../migrations/0002_alter_team_options.py | 17 +++++++++++++++++ app/config_management/models/groups.py | 4 ++++ .../migrations/0006_alter_device_options.py | 17 +++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 app/access/migrations/0002_alter_team_options.py create mode 100644 app/itam/migrations/0006_alter_device_options.py diff --git a/app/access/migrations/0002_alter_team_options.py b/app/access/migrations/0002_alter_team_options.py new file mode 100644 index 000000000..7dcb95296 --- /dev/null +++ b/app/access/migrations/0002_alter_team_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 06:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='team', + options={'ordering': ['team_name'], 'verbose_name': 'Team', 'verbose_name_plural': 'Teams'}, + ), + ] diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 88e63c8c7..c235fed87 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -38,6 +38,10 @@ class ConfigGroups(GroupsCommonFields, SaveHistory): class Meta: + ordering = [ + 'name' + ] + verbose_name_plural = 'Config Groups' diff --git a/app/itam/migrations/0006_alter_device_options.py b/app/itam/migrations/0006_alter_device_options.py new file mode 100644 index 000000000..37b1c4b90 --- /dev/null +++ b/app/itam/migrations/0006_alter_device_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 06:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0005_alter_devicesoftware_action_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='device', + options={'ordering': ['name', 'organization'], 'verbose_name': 'Device', 'verbose_name_plural': 'Devices'}, + ), + ] From 9e3a61a890fe88f7265d1ea5332eea2744ee9074 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:15:18 +0930 Subject: [PATCH 063/617] fix(access): Add missing attribute Meta.verbos_name to Config Groups model ref: #248 #345 #346 --- app/config_management/models/groups.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index c235fed87..b16b6c00a 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -42,6 +42,8 @@ class Meta: 'name' ] + verbose_name = 'Config Group' + verbose_name_plural = 'Config Groups' From 7dcde1926ed03d358404be58ce851cd83a6eef39 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:15:52 +0930 Subject: [PATCH 064/617] feat(access): Add attribute page_layout to Config Groups model ref: #248 #345 #346 --- app/config_management/models/groups.py | 84 ++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index b16b6c00a..ae9f532f7 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -86,6 +86,90 @@ def validate_config_keys_not_reserved(self): ) + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'parent', + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + { + "layout": "single", + "fields": [ + 'config', + ] + } + ] + }, + { + "name": "Child Groups", + "slug": "child_groups", + "sections": [ + { + "layout": "table", + "field": "child_groups", + } + ] + }, + { + "name": "Hosts", + "slug": "hosts", + "sections": [ + { + "layout": "table", + "field": "hosts", + } + ] + }, + { + "name": "Software", + "slug": "software", + "sections": [ + { + "layout": "table", + "field": "hosts", + } + ] + }, + { + "name": "Configuration", + "slug": "configuration", + "sections": [ + { + "layout": "table", + "field": "rendered_configuration", + } + ] + }, + { + "name": "Tickets", + "slug": "tickets", + "sections": [ + { + "layout": "table", + "field": "ticket", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def config_keys_ansible_variable(self, value: dict): clean_value = {} From 3b358599ad216bd04ef861d1e61cfc247ab6c8d9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:16:11 +0930 Subject: [PATCH 065/617] feat(access): Add attribute table_fields to Config Groups model ref: #248 #345 #346 --- app/config_management/models/groups.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index ae9f532f7..b7e525377 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -170,6 +170,14 @@ def validate_config_keys_not_reserved(self): }, ] + + table_fields: list = [ + 'name', + 'count_children', + 'organization' + ] + + def config_keys_ansible_variable(self, value: dict): clean_value = {} From 11fd7f8c6c6d2b534253be460462837e4762f71b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:22:27 +0930 Subject: [PATCH 066/617] fix(access): Add missing attribute Meta.verbos_name to Config Group Software model ref: #248 #345 #346 --- .../0004_alter_configgroups_options.py | 17 +++++++++++++++++ .../0005_alter_configgroupsoftware_options.py | 17 +++++++++++++++++ app/config_management/models/groups.py | 3 +++ 3 files changed, 37 insertions(+) create mode 100644 app/config_management/migrations/0004_alter_configgroups_options.py create mode 100644 app/config_management/migrations/0005_alter_configgroupsoftware_options.py diff --git a/app/config_management/migrations/0004_alter_configgroups_options.py b/app/config_management/migrations/0004_alter_configgroups_options.py new file mode 100644 index 000000000..e860267dc --- /dev/null +++ b/app/config_management/migrations/0004_alter_configgroups_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 06:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('config_management', '0003_alter_configgroups_options_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='configgroups', + options={'ordering': ['name'], 'verbose_name': 'Config Group', 'verbose_name_plural': 'Config Groups'}, + ), + ] diff --git a/app/config_management/migrations/0005_alter_configgroupsoftware_options.py b/app/config_management/migrations/0005_alter_configgroupsoftware_options.py new file mode 100644 index 000000000..620b0ab88 --- /dev/null +++ b/app/config_management/migrations/0005_alter_configgroupsoftware_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 06:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('config_management', '0004_alter_configgroups_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='configgroupsoftware', + options={'ordering': ['-action', 'software'], 'verbose_name': 'Config Group Software', 'verbose_name_plural': 'Config Group Softwares'}, + ), + ] diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index b7e525377..c01c002b4 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -363,11 +363,14 @@ class ConfigGroupSoftware(GroupsCommonFields, SaveHistory): """ A way to configure software to install/remove per config group """ class Meta: + ordering = [ '-action', 'software' ] + verbose_name = 'Config Group Software' + verbose_name_plural = 'Config Group Softwares' From 066d8b903a5e70d2d13ae86340b33125bb0d4ac1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:22:56 +0930 Subject: [PATCH 067/617] feat(access): Add attribute page_layout to Config Group Software model ref: #248 #345 #346 --- app/config_management/models/groups.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index c01c002b4..4c4e161a7 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -407,6 +407,9 @@ class Meta: blank= True ) + # This model is not intended to be viewable on it's own page + # as it's a sub model for config groups + page_layout: dict = [] @property def parent_object(self): From 8121879165198aa208de0a7a14664b4f5d4fac18 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:23:07 +0930 Subject: [PATCH 068/617] feat(access): Add attribute table_fields to Config Group Software model ref: #248 #345 #346 --- app/config_management/models/groups.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 4c4e161a7..0807fdd20 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -411,6 +411,15 @@ class Meta: # as it's a sub model for config groups page_layout: dict = [] + + table_fields: list = [ + 'software', + 'category', + 'action', + 'version' + ] + + @property def parent_object(self): """ Fetch the parent object """ From 18f8e515d8d2ce51ea3961b0aead7fe4fe169a68 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:29:53 +0930 Subject: [PATCH 069/617] fix(core): Add missing attribute Meta.verbose_name to Manufacturer model ref: #248 #345 #346 --- .../0008_alter_manufacturer_options.py | 17 +++++++++++++++++ app/core/models/manufacturer.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/core/migrations/0008_alter_manufacturer_options.py diff --git a/app/core/migrations/0008_alter_manufacturer_options.py b/app/core/migrations/0008_alter_manufacturer_options.py new file mode 100644 index 000000000..5f068e8e0 --- /dev/null +++ b/app/core/migrations/0008_alter_manufacturer_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 06:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_alter_history_after_alter_history_before'), + ] + + operations = [ + migrations.AlterModelOptions( + name='manufacturer', + options={'ordering': ['name'], 'verbose_name': 'Manufacturer', 'verbose_name_plural': 'Manufacturers'}, + ), + ] diff --git a/app/core/models/manufacturer.py b/app/core/models/manufacturer.py index 96c2ad56e..e6f7765d4 100644 --- a/app/core/models/manufacturer.py +++ b/app/core/models/manufacturer.py @@ -34,6 +34,8 @@ class Meta: 'name' ] + verbose_name = 'Manufacturer' + verbose_name_plural = 'Manufacturers' From 98ded4c748afdae21921d72deae771a789af8c91 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:30:20 +0930 Subject: [PATCH 070/617] feat(core): Add attribute page_layout to Manufacturer model ref: #248 #345 #346 --- app/core/models/manufacturer.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/core/models/manufacturer.py b/app/core/models/manufacturer.py index e6f7765d4..0b4f3de23 100644 --- a/app/core/models/manufacturer.py +++ b/app/core/models/manufacturer.py @@ -48,6 +48,32 @@ class Meta: slug = AutoSlugField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] def clean(self): From 65759827c827c48159e91bbd7cdaf9aa7b1bff67 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:30:43 +0930 Subject: [PATCH 071/617] feat(core): Add attribute table_fields to Manufacturer model ref: #248 #345 #346 --- app/core/models/manufacturer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/core/models/manufacturer.py b/app/core/models/manufacturer.py index 0b4f3de23..04fa39ad3 100644 --- a/app/core/models/manufacturer.py +++ b/app/core/models/manufacturer.py @@ -75,6 +75,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def clean(self): app_settings = AppSettings.objects.get(owner_organization=None) From 8fa468f735cf1da3e3d4b7f3d8ef901dc032dfc2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:34:05 +0930 Subject: [PATCH 072/617] fix(core): Add missing attribute Meta.verbose_name to Notes model ref: #248 #345 #346 --- app/core/migrations/0009_alter_notes_options.py | 17 +++++++++++++++++ app/core/models/notes.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/core/migrations/0009_alter_notes_options.py diff --git a/app/core/migrations/0009_alter_notes_options.py b/app/core/migrations/0009_alter_notes_options.py new file mode 100644 index 000000000..ff3f11e3a --- /dev/null +++ b/app/core/migrations/0009_alter_notes_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_alter_manufacturer_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='notes', + options={'ordering': ['-created'], 'verbose_name': 'Note', 'verbose_name_plural': 'Notes'}, + ), + ] diff --git a/app/core/models/notes.py b/app/core/models/notes.py index 71a5782d5..d24701b20 100644 --- a/app/core/models/notes.py +++ b/app/core/models/notes.py @@ -46,6 +46,8 @@ class Meta: '-created' ] + verbose_name = 'Note' + verbose_name_plural = 'Notes' From a78f7660e24c372a32738208982eb6cec1f2bc13 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:35:38 +0930 Subject: [PATCH 073/617] feat(core): Add attribute page_layout to Notes model ref: #248 #345 #346 --- app/core/models/notes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/core/models/notes.py b/app/core/models/notes.py index d24701b20..826def8ad 100644 --- a/app/core/models/notes.py +++ b/app/core/models/notes.py @@ -120,6 +120,12 @@ class Meta: blank= True ) + # this model is not intended to have its own viewable page as + # it's a sub model + page_layout: dict = [] + + # This model is not expected to be viewable in a table + # as it's a sub-model table_fields: list = [] def __str__(self): From 1d1e295af714daf2c6f05a8bb5d91b8a84daee8f Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:38:45 +0930 Subject: [PATCH 074/617] feat(core): Add attribute page_layout to Ticket model this model does not require this to be filled out as it uses a custom view ref: #248 #345 #346 --- app/core/models/ticket/ticket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index 55a408008..322f3cc5e 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -559,6 +559,9 @@ def validation_title(field): # ?? date_edit date of last edit + # this model uses a custom page layout + page_layout: list = [] + table_fields: list = [ 'id', 'title', From f814151d3308dc53868f7024b4a954f5f7ca5181 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:43:39 +0930 Subject: [PATCH 075/617] feat(core): Add attribute page_layout to Ticket Category model ref: #248 #345 #346 --- app/core/models/ticket/ticket_category.py | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/app/core/models/ticket/ticket_category.py b/app/core/models/ticket/ticket_category.py index c33375dcf..0306ddac0 100644 --- a/app/core/models/ticket/ticket_category.py +++ b/app/core/models/ticket/ticket_category.py @@ -107,6 +107,47 @@ class Meta: ) + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'parent' + 'name' + 'runbook', + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + { + "layout": "double", + "left": [ + 'change', + 'problem' + 'request' + ], + "right": [ + 'incident', + 'project_task', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + @property def recusive_name(self): From a1bd6a81b38a21c74fdf48dfdf72be9efcb690f0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:43:54 +0930 Subject: [PATCH 076/617] feat(core): Add attribute table_fields to Ticket Category model ref: #248 #345 #346 --- app/core/models/ticket/ticket_category.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/core/models/ticket/ticket_category.py b/app/core/models/ticket/ticket_category.py index 0306ddac0..6f2c632e3 100644 --- a/app/core/models/ticket/ticket_category.py +++ b/app/core/models/ticket/ticket_category.py @@ -148,6 +148,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + @property def recusive_name(self): From 5776a61ff4a213b69d3868b0197b70d746f3fb97 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:47:48 +0930 Subject: [PATCH 077/617] feat(core): Add attribute page_layout to Ticket comment model ref: #248 #345 #346 --- app/core/models/ticket/ticket_comment.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/core/models/ticket/ticket_comment.py b/app/core/models/ticket/ticket_comment.py index 9bb07dd42..c2f95fe76 100644 --- a/app/core/models/ticket/ticket_comment.py +++ b/app/core/models/ticket/ticket_comment.py @@ -17,8 +17,6 @@ class TicketComment( TenancyObject, ): - table_fields: list = [] - save_model_history: bool = False class Meta: @@ -304,6 +302,15 @@ def validation_ticket_id(field): verbose_name = 'Real Finish Date', ) + # this model is not intended to be viewable on its + # own page due to being a sub model + page_layout: list = [] + + + # this model is not intended to be viewable via + # a table as it's a sub-model + table_fields: list = [] + common_fields: list(str()) = [ 'body', From f134b6828f0580b78dbbef6df33854461a975e07 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:52:24 +0930 Subject: [PATCH 078/617] feat(core): Add attribute page_layout to Ticket Comment Category model ref: #248 #345 #346 --- .../models/ticket/ticket_comment_category.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/app/core/models/ticket/ticket_comment_category.py b/app/core/models/ticket/ticket_comment_category.py index 78e52ec97..930918580 100644 --- a/app/core/models/ticket/ticket_comment_category.py +++ b/app/core/models/ticket/ticket_comment_category.py @@ -98,6 +98,46 @@ class Meta: ) + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'parent' + 'name' + 'runbook', + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + { + "layout": "double", + "left": [ + 'comment', + 'solution' + ], + "right": [ + 'notification', + 'task', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.name From 6ecaa08782fcead6a7fea0d81d9412c7e4a0e713 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:52:37 +0930 Subject: [PATCH 079/617] feat(core): Add attribute table_fields to Ticket Comment Category model ref: #248 #345 #346 --- app/core/models/ticket/ticket_comment_category.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/core/models/ticket/ticket_comment_category.py b/app/core/models/ticket/ticket_comment_category.py index 930918580..cc87a7987 100644 --- a/app/core/models/ticket/ticket_comment_category.py +++ b/app/core/models/ticket/ticket_comment_category.py @@ -138,6 +138,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return self.name From b57df0d5bc068538f3eee585959dc9d1d52a305c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 16:57:19 +0930 Subject: [PATCH 080/617] fix(itama): Add missing attribute Meta.verbose_name to "Device Model" model ref: #248 #345 #346 --- .../0007_alter_devicemodel_options.py | 17 +++++++++++ app/itam/models/device_models.py | 29 ++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 app/itam/migrations/0007_alter_devicemodel_options.py diff --git a/app/itam/migrations/0007_alter_devicemodel_options.py b/app/itam/migrations/0007_alter_devicemodel_options.py new file mode 100644 index 000000000..ae30b92f8 --- /dev/null +++ b/app/itam/migrations/0007_alter_devicemodel_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0006_alter_device_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='devicemodel', + options={'ordering': ['manufacturer', 'name'], 'verbose_name': 'Device Model', 'verbose_name_plural': 'Device Models'}, + ), + ] diff --git a/app/itam/models/device_models.py b/app/itam/models/device_models.py index b560648e8..6a5fcf14d 100644 --- a/app/itam/models/device_models.py +++ b/app/itam/models/device_models.py @@ -20,6 +20,8 @@ class Meta: 'name', ] + verbose_name = 'Device Model' + verbose_name_plural = 'Device Models' @@ -31,14 +33,6 @@ class Meta: blank= True ) - table_fields: list = [ - 'manufacturer', - 'name', - 'organization', - 'created', - 'modified' - ] - page_layout: dict = [ { "name": "Details", @@ -50,18 +44,31 @@ class Meta: 'organization', 'manufacturer', 'name', - 'created', - 'modified', + 'is_global', ], "right": [ 'model_notes', - 'is_global', + 'created', + 'modified', ] } ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] } ] + table_fields: list = [ + 'manufacturer', + 'name', + 'organization', + 'created', + 'modified' + ] + def clean(self): From 2f0a2e282bdafa9d0cda97b15297a3faef68254a Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:04:50 +0930 Subject: [PATCH 081/617] fix(itam): Add missing attribute Meta.ordering to "Device Software" model ref: #248 #345 #346 --- app/itam/models/device.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index ceff58d57..f5d63ffa5 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -575,6 +575,10 @@ class DeviceOperatingSystem(DeviceCommonFields, SaveHistory): class Meta: + ordering = [ + 'device', + ] + verbose_name_plural = 'Device Operating Systems' From ab69d8d1742449cb42729f8cf8f7ddac6183508c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:05:05 +0930 Subject: [PATCH 082/617] fix(itam): Add missing attribute Meta.verbose_name to "Device Software" model ref: #248 #345 #346 --- .../0008_alter_deviceoperatingsystem_options.py | 17 +++++++++++++++++ app/itam/models/device.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/itam/migrations/0008_alter_deviceoperatingsystem_options.py diff --git a/app/itam/migrations/0008_alter_deviceoperatingsystem_options.py b/app/itam/migrations/0008_alter_deviceoperatingsystem_options.py new file mode 100644 index 000000000..28d9ebe90 --- /dev/null +++ b/app/itam/migrations/0008_alter_deviceoperatingsystem_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0007_alter_devicemodel_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='deviceoperatingsystem', + options={'ordering': ['device'], 'verbose_name': 'Device Operating System', 'verbose_name_plural': 'Device Operating Systems'}, + ), + ] diff --git a/app/itam/models/device.py b/app/itam/models/device.py index f5d63ffa5..f30272f4c 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -579,6 +579,8 @@ class Meta: 'device', ] + verbose_name = 'Device Operating System' + verbose_name_plural = 'Device Operating Systems' From da49b98c609fbcc4acadadd8b517d602ca90480b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:06:30 +0930 Subject: [PATCH 083/617] feat(itam): Add attribute page_layout to "Device Software" model ref: #248 #345 #346 --- app/itam/models/device.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index f30272f4c..68cb5ad6d 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -615,6 +615,22 @@ class Meta: default = None, ) + page_layout: list = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "single", + "fields": [ + 'operating_system_version', + 'version', + 'installdate' + ], + } + ] + } + ] @property def parent_object(self): From 26640926a87d8b3029459705e705800f718975d8 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:06:44 +0930 Subject: [PATCH 084/617] feat(itam): Add attribute table_fields to "Device Software" model ref: #248 #345 #346 --- app/itam/models/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 68cb5ad6d..bb6921c1e 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -632,6 +632,9 @@ class Meta: } ] + table_fields: list = [] + + @property def parent_object(self): """ Fetch the parent object """ From f24e645d55c77e364c975b230a1b095aaea03fc5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:09:51 +0930 Subject: [PATCH 085/617] fix(itam): Add missing attribute Meta.verbose_name to "Device Software" model ref: #248 #345 #346 --- .../0009_alter_devicesoftware_options.py | 17 +++++++++++++++++ app/itam/models/device.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/itam/migrations/0009_alter_devicesoftware_options.py diff --git a/app/itam/migrations/0009_alter_devicesoftware_options.py b/app/itam/migrations/0009_alter_devicesoftware_options.py new file mode 100644 index 000000000..2a2e7aef5 --- /dev/null +++ b/app/itam/migrations/0009_alter_devicesoftware_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0008_alter_deviceoperatingsystem_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='devicesoftware', + options={'ordering': ['-action', 'software'], 'verbose_name': 'Device Software', 'verbose_name_plural': 'Device Softwares'}, + ), + ] diff --git a/app/itam/models/device.py b/app/itam/models/device.py index bb6921c1e..d57d395ae 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -446,6 +446,8 @@ class Meta: 'software' ] + verbose_name = 'Device Software' + verbose_name_plural = 'Device Softwares' From f362e3493fb1e6176bdb70b07f4b94f5b067d950 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:10:04 +0930 Subject: [PATCH 086/617] feat(itam): Add attribute page_layout to "Device Software" model ref: #248 #345 #346 --- app/itam/models/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index d57d395ae..82794fe7a 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -510,6 +510,9 @@ class Actions(models.TextChoices): ) + page_layout: list = [] + + table_fields: list = [ "nbsp", "software", From 21a50f16aeeb0b56f425c9a81c9f4053a69a7ede Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:14:28 +0930 Subject: [PATCH 087/617] fix(itam): Add missing attribute Meta.ordering to "Device Software" model ref: #248 #345 #346 --- .../migrations/0010_alter_devicetype_options.py | 17 +++++++++++++++++ app/itam/models/device.py | 5 +++++ 2 files changed, 22 insertions(+) create mode 100644 app/itam/migrations/0010_alter_devicetype_options.py diff --git a/app/itam/migrations/0010_alter_devicetype_options.py b/app/itam/migrations/0010_alter_devicetype_options.py new file mode 100644 index 000000000..6ea3460ad --- /dev/null +++ b/app/itam/migrations/0010_alter_devicetype_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0009_alter_devicesoftware_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='devicetype', + options={'ordering': ['name'], 'verbose_name': 'Device Type', 'verbose_name_plural': 'Device Types'}, + ), + ] diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 82794fe7a..99eedf9f6 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -28,6 +28,11 @@ class DeviceType(DeviceCommonFieldsName, SaveHistory): class Meta: + ordering = [ + 'name' + ] + + verbose_name_plural = 'Device Types' From 2563f6f8e5fd78406348da1099feebf0753ebf56 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:15:20 +0930 Subject: [PATCH 088/617] fix(itam): Add missing attribute Meta.verbose_name to "Device Type" model ref: #248 #345 #346 --- app/itam/models/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 99eedf9f6..676e28ffe 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -32,6 +32,7 @@ class Meta: 'name' ] + verbose_name = 'Device Type' verbose_name_plural = 'Device Types' From 06159770244eef8995cc40bea1caa52f6584381c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:15:55 +0930 Subject: [PATCH 089/617] feat(itam): Add attribute page_layout to "Device Type" model ref: #248 #345 #346 --- app/itam/models/device.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 676e28ffe..57987698d 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -37,6 +37,33 @@ class Meta: verbose_name_plural = 'Device Types' + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def clean(self): app_settings = AppSettings.objects.get(owner_organization=None) From 61bfffa3ca1e77edea7947aa545c1ca0394582af Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:16:11 +0930 Subject: [PATCH 090/617] feat(itam): Add attribute table_fields to "Device Type" model ref: #248 #345 #346 --- app/itam/models/device.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 57987698d..3894a276d 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -64,6 +64,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def clean(self): app_settings = AppSettings.objects.get(owner_organization=None) From ce58d13ab5c2b4106f78b6126f240b2b4c62e2a1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:22:55 +0930 Subject: [PATCH 091/617] fix(itam): Add missing attribute Meta.ordering to Operating System model ref: #248 #345 #346 --- app/itam/models/operating_system.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index a638c0893..a5fd931ee 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -45,6 +45,10 @@ class OperatingSystem(OperatingSystemFieldsName, SaveHistory): class Meta: + ordering = [ + 'name' + ] + verbose_name_plural = 'Operating Systems' From 5042124803e8332ca4cf673d807d2c6fc0f185ae Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:23:44 +0930 Subject: [PATCH 092/617] fix(itam): Add missing attribute Meta.verbose_name to Operating System model ref: #248 #345 #346 --- .../0011_alter_operatingsystem_options.py | 17 +++++++++++++++++ app/itam/models/operating_system.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/itam/migrations/0011_alter_operatingsystem_options.py diff --git a/app/itam/migrations/0011_alter_operatingsystem_options.py b/app/itam/migrations/0011_alter_operatingsystem_options.py new file mode 100644 index 000000000..40abb50a7 --- /dev/null +++ b/app/itam/migrations/0011_alter_operatingsystem_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0010_alter_devicetype_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='operatingsystem', + options={'ordering': ['name'], 'verbose_name': 'Operating System', 'verbose_name_plural': 'Operating Systems'}, + ), + ] diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index a5fd931ee..040f66ade 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -49,6 +49,8 @@ class Meta: 'name' ] + verbose_name = 'Operating System' + verbose_name_plural = 'Operating Systems' From 5cc7ba02371fae9f063a095bd31ae1f3c95a89c2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:24:33 +0930 Subject: [PATCH 093/617] feat(itam): Add attribute page_layout to Operating System model ref: #248 #345 #346 --- app/itam/models/operating_system.py | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 040f66ade..2479268e7 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -62,6 +62,75 @@ class Meta: blank= True ) + + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'publisher', + 'name' + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Versions", + "slug": "version", + "sections": [ + { + "layout": "table", + "field": "software_version", + } + ] + }, + # { + # "name": "Licences", + # "slug": "licence", + # "sections": [ + # { + # "layout": "table", + # "field": "licence", + # } + # ] + # }, + { + "name": "Installations", + "slug": "installs", + "sections": [ + { + "layout": "table", + "field": "installations", + } + ] + }, + { + "name": "Tickets", + "slug": "ticket", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.name From 372e4c190fc270aea082df8ef117f6b082a9b1e2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:24:46 +0930 Subject: [PATCH 094/617] feat(itam): Add attribute table_field to Operating System model ref: #248 #345 #346 --- app/itam/models/operating_system.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 2479268e7..3aff50e29 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -131,6 +131,16 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'publisher', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return self.name From c1daab6069095e63ea34cf0b768523b764c76786 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:29:52 +0930 Subject: [PATCH 095/617] fix(itam): Add missing attribute Meta.ordering to Operating System Version model ref: #248 #345 #346 --- app/itam/models/operating_system.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 3aff50e29..6b97f3f30 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -151,6 +151,10 @@ class OperatingSystemVersion(OperatingSystemCommonFields, SaveHistory): class Meta: + ordering = [ + 'name', + ] + verbose_name_plural = 'Operating System Versions' From c45da0f676f8b6a1efcb46a6f0844ab7e7fbc09a Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:30:11 +0930 Subject: [PATCH 096/617] fix(itam): Add missing attribute Meta.verbose_name to Operating System Version model ref: #248 #345 #346 --- ...0012_alter_operatingsystemversion_options.py | 17 +++++++++++++++++ app/itam/models/operating_system.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/itam/migrations/0012_alter_operatingsystemversion_options.py diff --git a/app/itam/migrations/0012_alter_operatingsystemversion_options.py b/app/itam/migrations/0012_alter_operatingsystemversion_options.py new file mode 100644 index 000000000..606908a10 --- /dev/null +++ b/app/itam/migrations/0012_alter_operatingsystemversion_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 07:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0011_alter_operatingsystem_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='operatingsystemversion', + options={'ordering': ['name'], 'verbose_name': 'Operating System Version', 'verbose_name_plural': 'Operating System Versions'}, + ), + ] diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 6b97f3f30..e82642d04 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -155,6 +155,8 @@ class Meta: 'name', ] + verbose_name = 'Operating System Version' + verbose_name_plural = 'Operating System Versions' From 1e4d6087ff4f4da3332e3329c5fbf4a2be0822ab Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:31:08 +0930 Subject: [PATCH 097/617] feat(itam): Add attribute page_layout to Operating System Version model ref: #248 #345 #346 --- app/itam/models/operating_system.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index e82642d04..35daaf3e8 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -172,6 +172,9 @@ class Meta: unique = False, ) + # model not intended to be viewable on its own + # as it's a sub model + page_layout: list = [] @property def parent_object(self): From b1fc59be3ae0b1458dd4ece5a81bc1a7f5f4cace Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:32:02 +0930 Subject: [PATCH 098/617] feat(itam): Add attribute table_fields to Operating System Version model ref: #248 #345 #346 --- app/itam/models/operating_system.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 35daaf3e8..a7f1a1257 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -176,6 +176,12 @@ class Meta: # as it's a sub model page_layout: list = [] + table_fields: list = [ + 'name', + 'installations' + ] + + @property def parent_object(self): """ Fetch the parent object """ From 1d60e1105ab061579eec2086d3c4778cf871e9cc Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:41:14 +0930 Subject: [PATCH 099/617] fix(itam): Add missing attribute Meta.ordering to Software model ref: #248 #345 #346 --- app/itam/models/software.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index fb97d222a..46780cc63 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -64,6 +64,11 @@ class Software(SoftwareCommonFields, SaveHistory): class Meta: + ordering = [ + 'name', + 'publisher__name' + ] + verbose_name_plural = 'Softwares' From dbde9144d92b80d702afa0167ad035bb5b16722b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:41:49 +0930 Subject: [PATCH 100/617] fix(itam): Add missing attribute Meta.verbose_name to Software model ref: #248 #345 #346 --- .../migrations/0013_alter_software_options.py | 17 +++++++++++++++++ app/itam/models/software.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/itam/migrations/0013_alter_software_options.py diff --git a/app/itam/migrations/0013_alter_software_options.py b/app/itam/migrations/0013_alter_software_options.py new file mode 100644 index 000000000..005dc3b4f --- /dev/null +++ b/app/itam/migrations/0013_alter_software_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 08:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0012_alter_operatingsystemversion_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='software', + options={'ordering': ['name', 'publisher__name'], 'verbose_name': 'Software', 'verbose_name_plural': 'Softwares'}, + ), + ] diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 46780cc63..7d31fe073 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -69,6 +69,8 @@ class Meta: 'publisher__name' ] + verbose_name = 'Software' + verbose_name_plural = 'Softwares' From baef2e928793ecfd2595cbb5b7bb44388560b212 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:42:30 +0930 Subject: [PATCH 101/617] feat(itam): Add attribute page_layout to Software model ref: #248 #345 #346 --- app/itam/models/software.py | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 7d31fe073..3f24d8ce2 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -92,6 +92,75 @@ class Meta: ) + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'publisher', + 'name', + 'category', + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Versions", + "slug": "version", + "sections": [ + { + "layout": "table", + "field": "versions", + } + ] + }, + # { + # "name": "Licences", + # "slug": "licence", + # "sections": [ + # { + # "layout": "table", + # "field": "licences", + # } + # ], + # }, + { + "name": "Installations", + "slug": "installs", + "sections": [ + { + "layout": "table", + "field": "installations", + } + ], + }, + { + "name": "Tickets", + "slug": "tickets", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ], + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + } + ] + def clean(self): app_settings = AppSettings.objects.get(owner_organization=None) From d98cd846bffc3e60ea85f0a964506c160c624ce6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:42:42 +0930 Subject: [PATCH 102/617] feat(itam): Add attribute table_fields to Software model ref: #248 #345 #346 --- app/itam/models/software.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 3f24d8ce2..fd8f19fa3 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -161,6 +161,17 @@ class Meta: } ] + + table_fields: list = [ + "name", + "publisher", + "category", + "organization", + "created", + "modified", + ] + + def clean(self): app_settings = AppSettings.objects.get(owner_organization=None) From 66b60e02e6f3d31e3628fa5befa8cff13e136ce7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:48:08 +0930 Subject: [PATCH 103/617] fix(itam): Add missing attribute Meta.ordering to Software Category model ref: #248 #345 #346 --- app/itam/models/software.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index fd8f19fa3..6f58f0da2 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -39,6 +39,10 @@ class SoftwareCategory(SoftwareCommonFields, SaveHistory): class Meta: + ordering = [ + 'name', + ] + verbose_name_plural = 'Software Categories' From 5565670495addc7e28a8f911a5e0813f693820cd Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:48:43 +0930 Subject: [PATCH 104/617] fix(itam): Add missing attribute Meta.verbose_name to Software Category model ref: #248 #345 #346 --- .../0014_alter_softwarecategory_options.py | 17 +++++++++++++++++ app/itam/models/software.py | 2 ++ 2 files changed, 19 insertions(+) create mode 100644 app/itam/migrations/0014_alter_softwarecategory_options.py diff --git a/app/itam/migrations/0014_alter_softwarecategory_options.py b/app/itam/migrations/0014_alter_softwarecategory_options.py new file mode 100644 index 000000000..559af963d --- /dev/null +++ b/app/itam/migrations/0014_alter_softwarecategory_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 08:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0013_alter_software_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='softwarecategory', + options={'ordering': ['name'], 'verbose_name': 'Software Category', 'verbose_name_plural': 'Software Categories'}, + ), + ] diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 6f58f0da2..418ecc0ea 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -43,6 +43,8 @@ class Meta: 'name', ] + verbose_name = 'Software Category' + verbose_name_plural = 'Software Categories' From 73517733b52f7e43a09dcb54a2331e4c7b9cb221 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:49:36 +0930 Subject: [PATCH 105/617] feat(itam): Add attribute table_field to Software Category model ref: #248 #345 #346 --- app/itam/models/software.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 418ecc0ea..6e94ed6de 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -48,6 +48,41 @@ class Meta: verbose_name_plural = 'Software Categories' + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name', + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + } + ] + + + table_fields: list = [ + "name", + "organization", + "created", + "modified", + ] + def clean(self): From 939424788fde5f364e63f64fdd66f24cda4c5828 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 17:59:48 +0930 Subject: [PATCH 106/617] feat(itim): Add attribute page_layout to Cluster model ref: #248 #345 #346 --- app/itim/models/clusters.py | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index 9351a574c..03c9387ca 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -141,6 +141,78 @@ class Meta: modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'parent_cluster', + 'cluster_type', + 'name' + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + { + "layout": "table", + "name": "Nodes", + "field": "nodes", + }, + { + "layout": "table", + "name": "Devices", + "field": "devices", + }, + { + "layout": "table", + "name": "Services", + "field": "service", + }, + { + "layout": "single", + "fields": [ + 'config', + ] + } + ] + }, + { + "name": "Rendered Config", + "slug": "config_management", + "sections": [ + { + "layout": "single", + "fields": [ + "rendered_config", + ] + } + ] + }, + { + "name": "Tickets", + "slug": "ticket", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + @property def rendered_config(self): From 54796badc93bb4b0785aed6ee4f995701bc3707a Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:00:01 +0930 Subject: [PATCH 107/617] feat(itim): Add attribute table_field to Cluster model ref: #248 #345 #346 --- app/itim/models/clusters.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index 03c9387ca..fa5cf4ee8 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -213,6 +213,17 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'parent_cluster', + 'cluster_type', + 'organization', + 'created', + 'modified' + ] + + @property def rendered_config(self): From 790cbaf45286b6e2c273c2aa90b5af74645aee04 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:05:30 +0930 Subject: [PATCH 108/617] feat(itim): Add attribute page_layout to Cluster Type model ref: #248 #345 #346 --- app/itim/models/clusters.py | 55 +++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index fa5cf4ee8..fbeed1d65 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -54,6 +54,61 @@ class Meta: modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + { + "layout": "single", + "fields": [ + 'config', + ] + } + ] + }, + { + "name": "Rendered Config", + "slug": "config_management", + "sections": [ + { + "layout": "single", + "fields": [ + "rendered_config", + ] + } + ] + }, + { + "name": "Tickets", + "slug": "ticket", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.name From 8f410b370dbd849c559ea7089987b9a952c69fa4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:05:44 +0930 Subject: [PATCH 109/617] feat(itim): Add attribute table_fields to Cluster Type model ref: #248 #345 #346 --- app/itim/models/clusters.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index fbeed1d65..727fac339 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -109,6 +109,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return self.name From 3998325acdb5f626d5204dce2056f2667977997f Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:11:54 +0930 Subject: [PATCH 110/617] feat(itim): Add attribute page_layout to Service Port model ref: #248 #345 #346 --- app/itim/models/services.py | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 72d5cd7fb..bc0997525 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -76,6 +76,44 @@ def validation_port_number(number: int): modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'display_name', + 'description', + 'is_global', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + ] + }, + { + "name": "Services", + "slug": "services", + "sections": [ + { + "layout": "table", + "field": "services", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return str(self.protocol) + '/' + str(self.number) From c205a75ce00a7e122395fc5d0f9daaefcc222fae Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:12:08 +0930 Subject: [PATCH 111/617] feat(itim): Add attribute table_fields to Service Port model ref: #248 #345 #346 --- app/itim/models/services.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index bc0997525..8756283ef 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -114,6 +114,15 @@ def validation_port_number(number: int): }, ] + + table_fields: list = [ + 'display_name', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return str(self.protocol) + '/' + str(self.number) From 80583692768abb4b57abd1ae01b7a8e78633714c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:21:16 +0930 Subject: [PATCH 112/617] feat(itim): Add attribute page_layout to Service model ref: #248 #345 #346 --- app/itim/models/services.py | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 8756283ef..09bb896dc 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -247,6 +247,76 @@ def validate_config_key_variable(value): modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'config_key_variable', + 'template', + 'is_template', + ], + "right": [ + 'model_notes', + 'created', + 'modified', + ] + }, + { + "layout": "single", + "fields": [ + 'config', + ] + }, + { + "layout": "single", + "fields": [ + 'dependent_service' + ] + }, + { + "layout": "single", + "name": "Ports", + "fields": [ + 'port' + ], + } + ] + }, + { + "name": "Rendered Config", + "slug": "config_management", + "sections": [ + { + "layout": "single", + "fields": [ + "rendered_config", + ] + } + ] + }, + { + "name": "Tickets", + "slug": "ticket", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + table_fields: list = [ "nbsp", "name", From 5cbb081462754cdc80118ca406fe2d80d895d713 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:21:30 +0930 Subject: [PATCH 113/617] feat(itim): Add attribute table_fields to Service model ref: #248 #345 #346 --- app/itim/models/services.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 09bb896dc..6b83b5b3e 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -317,11 +317,13 @@ def validate_config_key_variable(value): }, ] + table_fields: list = [ - "nbsp", - "name", - "port", - "nbsp" + 'name', + 'deployed_to' + 'organization', + 'created', + 'modified' ] From 396566c3be7f564140df4f8d9eaedd8b196a21d2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:31:59 +0930 Subject: [PATCH 114/617] feat(project_management): Add attribute page_layout to Project model ref: #248 #345 #346 --- app/project_management/models/projects.py | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/app/project_management/models/projects.py b/app/project_management/models/projects.py index 3606915b1..0654afe66 100644 --- a/app/project_management/models/projects.py +++ b/app/project_management/models/projects.py @@ -182,6 +182,76 @@ class Priority(models.IntegerChoices): ) + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'code', + 'name' + 'priority', + 'project_type', + 'state', + 'percent_completed', + ], + "right": [ + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'duration_project' + 'created', + 'modified', + ] + }, + { + "layout": "double", + "left": [ + 'manager_user', + ], + "right": [ + 'manager_team', + ] + }, + { + "layout": "single", + "fields": [ + 'description' + ] + } + ] + }, + { + "name": "Tasks", + "slug": "ticket", + "sections": [ + { + "layout": "table", + "field": "tickets", + } + ] + }, + { + "name": "Milestones", + "slug": "milestones", + "sections": [ + { + "layout": "table", + "field": "milestones", + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + fields_all: list = [] From 8ffffab3954866f269db6780293fa57967bc6927 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:32:13 +0930 Subject: [PATCH 115/617] feat(project_management): Add attribute table_fields to Project model ref: #248 #345 #346 --- app/project_management/models/projects.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/project_management/models/projects.py b/app/project_management/models/projects.py index 0654afe66..d8f5462e3 100644 --- a/app/project_management/models/projects.py +++ b/app/project_management/models/projects.py @@ -253,6 +253,16 @@ class Priority(models.IntegerChoices): ] + table_fields: list = [ + 'code', + 'name', + 'project_type' + 'state', + 'organization', + 'modified' + ] + + fields_all: list = [] fields_import: list = [] From 4e358a05412ba9a7c3d243403b9d5b5c78328d02 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:36:15 +0930 Subject: [PATCH 116/617] feat(project_management): Add attribute table_fields to Project Milestone model ref: #248 #345 #346 --- app/project_management/models/project_milestone.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/project_management/models/project_milestone.py b/app/project_management/models/project_milestone.py index d4a73fb15..a63617d52 100644 --- a/app/project_management/models/project_milestone.py +++ b/app/project_management/models/project_milestone.py @@ -59,6 +59,10 @@ class Meta: ) + # model not intended to be vieable on its own page + # as this model is a sub-model. + page_layout: dict = [] + def __str__(self): return self.name From dc6f9d3f1770048355324d3830b766f22a23ab9e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:36:31 +0930 Subject: [PATCH 117/617] feat(project_management): Add attribute page_layout to Project Milestone model ref: #248 #345 #346 --- app/project_management/models/project_milestone.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/project_management/models/project_milestone.py b/app/project_management/models/project_milestone.py index a63617d52..e2a3d794f 100644 --- a/app/project_management/models/project_milestone.py +++ b/app/project_management/models/project_milestone.py @@ -63,6 +63,15 @@ class Meta: # as this model is a sub-model. page_layout: dict = [] + + table_fields: list = [ + 'name', + 'percent_completed' + 'start_date', + 'finish_date', + ] + + def __str__(self): return self.name From 9f8d2acd993302f99e70775d0a13ce7f269be680 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:41:12 +0930 Subject: [PATCH 118/617] feat(project_management): Add attribute page_layout to Project State model ref: #248 #345 #346 --- .../models/project_states.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/project_management/models/project_states.py b/app/project_management/models/project_states.py index b45b882e2..272d648ed 100644 --- a/app/project_management/models/project_states.py +++ b/app/project_management/models/project_states.py @@ -66,6 +66,36 @@ class Meta: verbose_name = 'State Completed', ) + + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'runbook', + 'is_global', + 'is_completed', + ], + "right": [ + 'model_notes' + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.name From 3f03b8710f595c25567b41d180f1c8b03cefe60d Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:41:25 +0930 Subject: [PATCH 119/617] feat(project_management): Add attribute table_fields to Project State model ref: #248 #345 #346 --- app/project_management/models/project_states.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/project_management/models/project_states.py b/app/project_management/models/project_states.py index 272d648ed..cc6bf34eb 100644 --- a/app/project_management/models/project_states.py +++ b/app/project_management/models/project_states.py @@ -96,6 +96,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return self.name From c0a0bc544a7939c057da53bd820cd8c6bfa7e472 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:44:28 +0930 Subject: [PATCH 120/617] feat(project_management): Add attribute page_layout to Project Type model ref: #248 #345 #346 --- .../models/project_types.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/project_management/models/project_types.py b/app/project_management/models/project_types.py index e4768f36d..1582d86b1 100644 --- a/app/project_management/models/project_types.py +++ b/app/project_management/models/project_types.py @@ -58,6 +58,34 @@ class Meta: ) + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name' + 'runbook', + 'is_global', + ], + "right": [ + 'model_notes' + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): return self.name From 4a5991f9db725c628d3b7ae2694b1b0ba085d9ff Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:44:47 +0930 Subject: [PATCH 121/617] feat(project_management): Add attribute table_fields to Project Type model ref: #248 #345 #346 --- app/project_management/models/project_types.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/project_management/models/project_types.py b/app/project_management/models/project_types.py index 1582d86b1..e1f3a0e01 100644 --- a/app/project_management/models/project_types.py +++ b/app/project_management/models/project_types.py @@ -86,6 +86,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): return self.name From f67051fb157160dc634eef4b5697a17ffd499f2e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:51:23 +0930 Subject: [PATCH 122/617] fix(settingns): Add missing attribute Meta.ordering to External Links model ref: #248 #345 #346 --- app/settings/models/external_link.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py index 92b5e622a..47f69115e 100644 --- a/app/settings/models/external_link.py +++ b/app/settings/models/external_link.py @@ -10,6 +10,11 @@ class ExternalLink(TenancyObject): class Meta: + ordering = [ + 'name', + 'organization', + ] + verbose_name_plural = 'External Links' From d64d744f9f67c19d81a64983f436dd727b124246 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:52:26 +0930 Subject: [PATCH 123/617] fix(settings): Add missing attribute Meta.verbose_name to External Links model ref: #248 #345 #346 --- app/itim/models/services.py | 2 +- app/project_management/models/project_states.py | 2 +- app/project_management/models/projects.py | 2 +- .../0005_alter_externallink_options.py | 17 +++++++++++++++++ app/settings/models/external_link.py | 2 ++ 5 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 app/settings/migrations/0005_alter_externallink_options.py diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 6b83b5b3e..584e3d065 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -256,7 +256,7 @@ def validate_config_key_variable(value): "layout": "double", "left": [ 'organization', - 'name' + 'name', 'config_key_variable', 'template', 'is_template', diff --git a/app/project_management/models/project_states.py b/app/project_management/models/project_states.py index cc6bf34eb..1e385c3e1 100644 --- a/app/project_management/models/project_states.py +++ b/app/project_management/models/project_states.py @@ -76,7 +76,7 @@ class Meta: "layout": "double", "left": [ 'organization', - 'name' + 'name', 'runbook', 'is_global', 'is_completed', diff --git a/app/project_management/models/projects.py b/app/project_management/models/projects.py index d8f5462e3..005ff1e11 100644 --- a/app/project_management/models/projects.py +++ b/app/project_management/models/projects.py @@ -192,7 +192,7 @@ class Priority(models.IntegerChoices): "left": [ 'organization', 'code', - 'name' + 'name', 'priority', 'project_type', 'state', diff --git a/app/settings/migrations/0005_alter_externallink_options.py b/app/settings/migrations/0005_alter_externallink_options.py new file mode 100644 index 000000000..15f9147a2 --- /dev/null +++ b/app/settings/migrations/0005_alter_externallink_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-13 09:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0004_externallink_cluster'), + ] + + operations = [ + migrations.AlterModelOptions( + name='externallink', + options={'ordering': ['name', 'organization'], 'verbose_name': 'External Link', 'verbose_name_plural': 'External Links'}, + ), + ] diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py index 47f69115e..bb1bce5c4 100644 --- a/app/settings/models/external_link.py +++ b/app/settings/models/external_link.py @@ -15,6 +15,8 @@ class Meta: 'organization', ] + verbose_name = 'External Link' + verbose_name_plural = 'External Links' From 630223b15a5cf00711462f5e031124713cc13dec Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:52:51 +0930 Subject: [PATCH 124/617] fix(settings): Add attribute page_layout to External Links model ref: #248 #345 #346 --- app/settings/models/external_link.py | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py index bb1bce5c4..92fee958b 100644 --- a/app/settings/models/external_link.py +++ b/app/settings/models/external_link.py @@ -80,6 +80,48 @@ class Meta: modified = AutoLastModifiedField() + page_layout: dict = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'organization', + 'name', + 'template', + 'colour', + 'is_global', + ], + "right": [ + 'model_notes' + 'created', + 'modified', + ] + }, + { + "name": "Assignable to", + "layout": "double", + "left": [ + 'cluster', + 'software', + ], + "right": [ + 'devices' + 'created', + 'modified', + ] + } + ] + }, + { + "name": "Notes", + "slug": "notes", + "sections": [] + }, + ] + def __str__(self): """ Return the Template to render """ From 31af310d3d25e4885cf7d855a50db48640d6c8ed Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 18:53:10 +0930 Subject: [PATCH 125/617] fix(settings): Add attribute table_fields to External Links model ref: #248 #345 #346 --- app/settings/models/external_link.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py index 92fee958b..f887bf672 100644 --- a/app/settings/models/external_link.py +++ b/app/settings/models/external_link.py @@ -122,6 +122,15 @@ class Meta: }, ] + + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified' + ] + + def __str__(self): """ Return the Template to render """ From 783f063ef0bd2670097422895b4bdc98d30f734b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 19:34:34 +0930 Subject: [PATCH 126/617] feat(api): add v2 endpoint ref: #248 #345 #346 --- app/api/views/index.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/api/views/index.py b/app/api/views/index.py index ce32c0d12..be6f45063 100644 --- a/app/api/views/index.py +++ b/app/api/views/index.py @@ -1,3 +1,5 @@ +from django.conf import settings as django_settings + from django.utils.safestring import mark_safe from rest_framework import generics, permissions, routers, viewsets @@ -27,8 +29,8 @@ def get_view_description(self, html=False) -> str: def list(self, request, pk=None): - return Response( - { + + API: dict = { # "teams": reverse("_api_teams", request=request), 'assistance': reverse("API:_api_assistance", request=request), "devices": reverse("API:device-list", request=request), @@ -38,5 +40,7 @@ def list(self, request, pk=None): 'project_management': reverse("API:_api_project_management", request=request), "settings": reverse('API:_settings', request=request), "software": reverse("API:software-list", request=request), + 'v2': reverse("API:_api_v2_home-list", request=request) } - ) + + return Response( API ) From 1f9070c42055e8c3aaa7dd8722b38f73fa9d2fe1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 19:35:29 +0930 Subject: [PATCH 127/617] fix(api): correct logic for permission check to use either queryset or get_queryset ref: #248 #345 #346 --- app/api/views/mixin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index d0dec1990..8c56db026 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -48,6 +48,7 @@ def permission_check(self, request, view, obj=None) -> bool: elif hasattr(view, 'queryset'): if view.queryset.model._meta: + self.obj = view.queryset.model object_organization = None From ad72cc4f7d32790fcab4739393d1f7fd352d0b41 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 21:01:59 +0930 Subject: [PATCH 128/617] test: Ensure that during model field creation, attribute help_text is defined and not empty ref: #248 #345 #346 --- app/app/tests/abstract/models.py | 69 +++++++++++++++++++ .../centurion_erp/development/models.md | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 4cb1ba379..a69fb1237 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -61,6 +61,75 @@ def test_attribute_type_ordering(self): + def test_model_fields_parameter_has_help_text(self): + """Test Field called with Parameter + + During field creation, it should have been called with paramater `help_text` + """ + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + print(f'Checking field {field.attname} has attribute "help_text"') + + if not hasattr(field, 'help_text'): + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + + def test_model_fields_parameter_type_help_text(self): + """Test Field called with Parameter + + During field creation, paramater `help_text` must be of type str + """ + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + print(f'Checking field {field.attname} is of type str') + + if not type(field.help_text) is str: + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + + def test_model_fields_parameter_not_empty_help_text(self): + """Test Field called with Parameter + + During field creation, paramater `help_text` must not be `None` or empty ('') + """ + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + print(f'Checking field {field.attname} is not empty') + + if ( + field.help_text is not None + or field.help_text != '' + ): + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + class TenancyModel( BaseModel, TenancyObjectTestCases, diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index a142cde1d..d3cb29e18 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -38,7 +38,7 @@ All models must meet the following requirements: !!! danger "Requirement" Multi-field validation, or validation that requires access to multiple fields must be done within the [form class](./forms.md#requirements). -- contains a `Meta` sub-class with following parameters: +- contains a `Meta` sub-class with following attributes: - `verbose_name_plural` From 01da3f6fd05337675129cbf97081fa1e2c466ecc Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 21:02:28 +0930 Subject: [PATCH 129/617] test: Ensure that during model field creation, attribute verbose_name is defined and not empty ref: #248 #345 #346 --- app/app/tests/abstract/models.py | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index a69fb1237..51e85e512 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -130,6 +130,77 @@ def test_model_fields_parameter_not_empty_help_text(self): assert fields_have_test_value + + def test_model_fields_parameter_has_verbose_name(self): + """Test Field called with Parameter + + During field creation, it should have been called with paramater `verbose_name` + """ + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + print(f'Checking field {field.attname} has attribute "verbose_name"') + + if not hasattr(field, 'verbose_name'): + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + + def test_model_fields_parameter_type_verbose_name(self): + """Test Field called with Parameter + + During field creation, paramater `verbose_name` must be of type str + """ + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + print(f'Checking field {field.attname} is of type str') + + if not type(field.verbose_name) is str: + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + + def test_model_fields_parameter_not_empty_verbose_name(self): + """Test Field called with Parameter + + During field creation, paramater `verbose_name` must not be `None` or empty ('') + """ + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + print(f'Checking field {field.attname} is not empty') + + if ( + field.verbose_name is not None + or field.verbose_name != '' + ): + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + + class TenancyModel( BaseModel, TenancyObjectTestCases, From e765b03d3bff3f828e517a742260d8a23e37fc11 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 13 Oct 2024 21:06:38 +0930 Subject: [PATCH 130/617] docs: add new requirements for creating a model. ref: #248 #345 #346 --- docs/projects/centurion_erp/development/models.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index d3cb29e18..5337a4a1f 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -40,10 +40,18 @@ All models must meet the following requirements: - contains a `Meta` sub-class with following attributes: - - `verbose_name_plural` + - `ordering` _Order the results are returned in._ + + - `verbose_name` _Name of the Model._ + + - `verbose_name_plural` _Plural Name of the model_ - If creating a new model, function `access.functions.permissions.permission_queryset()` has been updated to display the models permission(s) +- Attribute `page_layout` is defined with the models UI page layout + +- Attribute `table_fields` is defined with the fields to display by default for viewing the model within a table. + ## Checklist @@ -53,6 +61,9 @@ This section details the additional items that may need to be done when adding a - If the model is a primary model, add it to the model link slash command in `app/core/lib/slash_commands/linked_model.py` function `command_linked_model` +!!! tip + It's a good idea to create the initial model class, then create and add the model tests for that class. This way you can run the tests to ensure that the requirements are met. Of Note, the tests may not cover ALL of the requirements section, due diligence will need to be exercised. + ## History From 32d5008f63055e1fc7198668d7bbdec9d94b75c5 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 01:02:09 +0930 Subject: [PATCH 131/617] fix: Ensure all Model fields are created with attributes `help_text` and `verbose_name` ref: #248 #346 --- app/access/fields.py | 26 ++ ..._id_alter_organization_manager_and_more.py | 57 +++ app/access/models.py | 36 +- ...ken_expires_alter_authtoken_id_and_more.py | 41 +++ app/api/models/tokens.py | 22 +- ...ove_knowledgebasecategory_slug_and_more.py | 55 +++ app/assistance/models/knowledge_base.py | 8 +- ...6_alter_configgrouphosts_group_and_more.py | 123 +++++++ app/config_management/models/groups.py | 51 ++- ...ory_action_alter_history_after_and_more.py | 206 +++++++++++ app/core/models/history.py | 37 +- app/core/models/manufacturer.py | 6 +- app/core/models/notes.py | 57 +-- ...model_alter_device_device_type_and_more.py | 337 ++++++++++++++++++ app/itam/models/device.py | 69 ++-- app/itam/models/device_common.py | 6 +- app/itam/models/device_models.py | 6 +- app/itam/models/operating_system.py | 19 +- app/itam/models/software.py | 22 +- ..._cluster_type_alter_cluster_id_and_more.py | 106 ++++++ app/itim/models/clusters.py | 12 +- app/itim/models/services.py | 8 +- ...t_description_alter_project_id_and_more.py | 103 ++++++ .../models/project_common.py | 6 +- app/project_management/models/projects.py | 10 +- ...ettings_device_model_is_global_and_more.py | 93 +++++ app/settings/models/app_settings.py | 28 +- app/settings/models/external_link.py | 12 +- app/settings/models/user_settings.py | 12 +- 29 files changed, 1438 insertions(+), 136 deletions(-) create mode 100644 app/access/migrations/0003_alter_organization_id_alter_organization_manager_and_more.py create mode 100644 app/api/migrations/0002_alter_authtoken_expires_alter_authtoken_id_and_more.py create mode 100644 app/assistance/migrations/0002_remove_knowledgebasecategory_slug_and_more.py create mode 100644 app/config_management/migrations/0006_alter_configgrouphosts_group_and_more.py create mode 100644 app/core/migrations/0010_alter_history_action_alter_history_after_and_more.py create mode 100644 app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py create mode 100644 app/itim/migrations/0005_alter_cluster_cluster_type_alter_cluster_id_and_more.py create mode 100644 app/project_management/migrations/0002_alter_project_description_alter_project_id_and_more.py create mode 100644 app/settings/migrations/0006_alter_appsettings_device_model_is_global_and_more.py diff --git a/app/access/fields.py b/app/access/fields.py index 1ef8ef219..715b88c89 100644 --- a/app/access/fields.py +++ b/app/access/fields.py @@ -11,12 +11,20 @@ class AutoCreatedField(models.DateTimeField): """ + help_text = 'Date and time of creation' + + verbose_name = 'Created' + def __init__(self, *args, **kwargs): kwargs.setdefault("editable", False) kwargs.setdefault("default", now) + kwargs.setdefault("help_text", self.help_text) + + kwargs.setdefault("verbose_name", self.verbose_name) + super().__init__(*args, **kwargs) @@ -28,6 +36,10 @@ class AutoLastModifiedField(AutoCreatedField): """ + help_text = 'Date and time of last modification' + + verbose_name = 'Modified' + def pre_save(self, model_instance, add): value = now() @@ -45,6 +57,20 @@ class AutoSlugField(models.SlugField): """ + help_text = 'slug for this field' + + verbose_name = 'Slug' + + + def __init__(self, *args, **kwargs): + + kwargs.setdefault("help_text", self.help_text) + + kwargs.setdefault("verbose_name", self.verbose_name) + + super().__init__(*args, **kwargs) + + def pre_save(self, model_instance, add): if not model_instance.slug or model_instance.slug == '_': diff --git a/app/access/migrations/0003_alter_organization_id_alter_organization_manager_and_more.py b/app/access/migrations/0003_alter_organization_id_alter_organization_manager_and_more.py new file mode 100644 index 000000000..ee85fcf2b --- /dev/null +++ b/app/access/migrations/0003_alter_organization_id_alter_organization_manager_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0002_alter_team_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='organization', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='organization', + name='manager', + field=models.ForeignKey(help_text='Manager for this organization', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Manager'), + ), + migrations.AlterField( + model_name='organization', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='organization', + name='name', + field=models.CharField(help_text='Name of this Organization', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='team', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='team', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='team', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='team', + name='team_name', + field=models.CharField(default='', help_text='Name to give this team', max_length=50, verbose_name='Name'), + ), + ] diff --git a/app/access/models.py b/app/access/models.py index afcfe3413..fabbebb56 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -23,28 +23,34 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) id = models.AutoField( + blank=False, + help_text = 'ID of this item', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) name = models.CharField( blank = False, + help_text = 'Name of this Organization', max_length = 50, unique = True, + verbose_name = 'Name' ) manager = models.ForeignKey( User, - on_delete=models.SET_NULL, blank = False, + help_text = 'Manager for this organization', null = True, - help_text = 'Organization Manager' + on_delete=models.SET_NULL, + verbose_name = 'Manager' ) model_notes = models.TextField( blank = True, default = None, + help_text = 'Tid bits of information', null= True, verbose_name = 'Notes', ) @@ -214,23 +220,36 @@ def validatate_organization_exists(self): raise ValidationError('You must provide an organization') + id = models.AutoField( + blank=False, + help_text = 'ID of the item', + primary_key=True, + unique=True, + verbose_name = 'ID' + ) + organization = models.ForeignKey( Organization, - on_delete=models.CASCADE, blank = False, + help_text = 'Organization this belongs to', null = True, + on_delete = models.CASCADE, validators = [validatate_organization_exists], + verbose_name = 'Organization' ) is_global = models.BooleanField( + blank = False, default = False, - blank = False + help_text = 'Is this a global object?', + verbose_name = 'Global Object' ) model_notes = models.TextField( blank = True, default = None, - null= True, + help_text = 'Tid bits of information', + null = True, verbose_name = 'Notes', ) @@ -267,11 +286,12 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields team_name = models.CharField( - verbose_name = 'Name', blank = False, + default = '', + help_text = 'Name to give this team', max_length = 50, unique = False, - default = '' + verbose_name = 'Name', ) created = AutoCreatedField() diff --git a/app/api/migrations/0002_alter_authtoken_expires_alter_authtoken_id_and_more.py b/app/api/migrations/0002_alter_authtoken_expires_alter_authtoken_id_and_more.py new file mode 100644 index 000000000..e7d7ed735 --- /dev/null +++ b/app/api/migrations/0002_alter_authtoken_expires_alter_authtoken_id_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='authtoken', + name='expires', + field=models.DateTimeField(help_text='When this token expires', verbose_name='Expiry Date'), + ), + migrations.AlterField( + model_name='authtoken', + name='id', + field=models.AutoField(help_text='ID of this token', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='authtoken', + name='note', + field=models.CharField(blank=True, default=None, help_text='A note about this token', max_length=50, null=True, verbose_name='Note'), + ), + migrations.AlterField( + model_name='authtoken', + name='token', + field=models.CharField(db_index=True, help_text='The authorization token', max_length=64, unique=True, verbose_name='Auth Token'), + ), + migrations.AlterField( + model_name='authtoken', + name='user', + field=models.ForeignKey(help_text='User this token belongs to', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Owner'), + ), + ] diff --git a/app/api/models/tokens.py b/app/api/models/tokens.py index f4caf4119..809356469 100644 --- a/app/api/models/tokens.py +++ b/app/api/models/tokens.py @@ -48,37 +48,45 @@ def validate_note_no_token(self, note, token): id = models.AutoField( + blank=False, + help_text = 'ID of this token', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) note = models.CharField( blank = True, - max_length = 50, default = None, + help_text = 'A note about this token', + max_length = 50, null= True, + verbose_name = 'Note' ) token = models.CharField( - verbose_name = 'Auth Token', + blank = False, db_index=True, + help_text = 'The authorization token', max_length = 64, null = False, - blank = False, unique = True, + verbose_name = 'Auth Token', ) user = models.ForeignKey( settings.AUTH_USER_MODEL, - on_delete=models.CASCADE + help_text = 'User this token belongs to', + on_delete=models.CASCADE, + verbose_name = 'Owner' ) expires = models.DateTimeField( - verbose_name = 'Expiry Date', + blank = False, + help_text = 'When this token expires', null = False, - blank = False + verbose_name = 'Expiry Date', ) diff --git a/app/assistance/migrations/0002_remove_knowledgebasecategory_slug_and_more.py b/app/assistance/migrations/0002_remove_knowledgebasecategory_slug_and_more.py new file mode 100644 index 000000000..d72490515 --- /dev/null +++ b/app/assistance/migrations/0002_remove_knowledgebasecategory_slug_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('assistance', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='knowledgebasecategory', + name='slug', + ), + migrations.AlterField( + model_name='knowledgebase', + name='id', + field=models.AutoField(help_text='ID of this KB article', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='knowledgebase', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='knowledgebase', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='knowledgebasecategory', + name='id', + field=models.AutoField(help_text='ID of the item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='knowledgebasecategory', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='knowledgebasecategory', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='knowledgebasecategory', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + ] diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index 9dd7c6593..197364d66 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -40,10 +40,6 @@ class Meta: verbose_name = 'Title', ) - - slug = AutoSlugField() - - target_team = models.ManyToManyField( Team, blank = True, @@ -140,9 +136,11 @@ class Meta: id = models.AutoField( + blank=False, + help_text = 'ID of this KB article', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) diff --git a/app/config_management/migrations/0006_alter_configgrouphosts_group_and_more.py b/app/config_management/migrations/0006_alter_configgrouphosts_group_and_more.py new file mode 100644 index 000000000..7211f636d --- /dev/null +++ b/app/config_management/migrations/0006_alter_configgrouphosts_group_and_more.py @@ -0,0 +1,123 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import config_management.models.groups +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('config_management', '0005_alter_configgroupsoftware_options'), + ('itam', '0014_alter_softwarecategory_options'), + ] + + operations = [ + migrations.AlterField( + model_name='configgrouphosts', + name='group', + field=models.ForeignKey(help_text='Group that this host is part of', on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups', verbose_name='Group'), + ), + migrations.AlterField( + model_name='configgrouphosts', + name='host', + field=models.ForeignKey(help_text='Host that will be apart of this config group', on_delete=django.db.models.deletion.CASCADE, to='itam.device', validators=[config_management.models.groups.ConfigGroupHosts.validate_host_no_parent_group], verbose_name='Host'), + ), + migrations.AlterField( + model_name='configgrouphosts', + name='id', + field=models.AutoField(help_text='ID of this Group', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='configgrouphosts', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='configgrouphosts', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='configgrouphosts', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='configgroups', + name='config', + field=models.JSONField(blank=True, default=None, help_text='Configuration for this Group', null=True, validators=[config_management.models.groups.ConfigGroups.validate_config_keys_not_reserved], verbose_name='Configuration'), + ), + migrations.AlterField( + model_name='configgroups', + name='id', + field=models.AutoField(help_text='ID of this Group', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='configgroups', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='configgroups', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='configgroups', + name='name', + field=models.CharField(help_text='Name of this Group', max_length=50, verbose_name='Name'), + ), + migrations.AlterField( + model_name='configgroups', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='configgroups', + name='parent', + field=models.ForeignKey(blank=True, default=None, help_text='Parent of this Group', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='config_management.configgroups', verbose_name='Parent Group'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='action', + field=models.CharField(blank=True, choices=[('1', 'Install'), ('0', 'Remove')], default=None, help_text='ACtion to perform with this software', max_length=1, null=True, verbose_name='Action'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='config_group', + field=models.ForeignKey(default=None, help_text='Config group this softwre will be linked to', on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups', verbose_name='Config Group'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='id', + field=models.AutoField(help_text='ID of this Group', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='software', + field=models.ForeignKey(default=None, help_text='Software to add to this config Group', on_delete=django.db.models.deletion.CASCADE, to='itam.software', verbose_name='Software'), + ), + migrations.AlterField( + model_name='configgroupsoftware', + name='version', + field=models.ForeignKey(blank=True, default=None, help_text='Software Version for this config group', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.softwareversion', verbose_name='Verrsion'), + ), + ] diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 0807fdd20..b42572173 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -22,9 +22,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID of this Group', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField() @@ -64,25 +66,31 @@ def validate_config_keys_not_reserved(self): parent = models.ForeignKey( 'self', - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Parent of this Group', null = True, - blank= True + on_delete=models.SET_DEFAULT, + verbose_name = 'Parent Group' ) name = models.CharField( blank = False, + help_text = 'Name of this Group', max_length = 50, unique = False, + verbose_name = 'Name' ) config = models.JSONField( blank = True, default = None, + help_text = 'Configuration for this Group', null = True, - validators=[ validate_config_keys_not_reserved ] + validators=[ validate_config_keys_not_reserved ], + verbose_name = 'Configuration' ) @@ -334,18 +342,22 @@ def validate_host_no_parent_group(self): host = models.ForeignKey( Device, + blank= False, + help_text = 'Host that will be apart of this config group', on_delete=models.CASCADE, null = False, - blank= False, - validators = [ validate_host_no_parent_group ] + validators = [ validate_host_no_parent_group ], + verbose_name = 'Host', ) group = models.ForeignKey( ConfigGroups, + blank= False, + help_text = 'Group that this host is part of', on_delete=models.CASCADE, null = False, - blank= False + verbose_name = 'Group', ) @@ -376,35 +388,44 @@ class Meta: config_group = models.ForeignKey( ConfigGroups, - on_delete=models.CASCADE, + blank= False, default = None, + help_text = 'Config group this softwre will be linked to', null = False, - blank= False + on_delete=models.CASCADE, + verbose_name = 'Config Group' ) software = models.ForeignKey( Software, - on_delete=models.CASCADE, + blank= False, default = None, + help_text = 'Software to add to this config Group', null = False, - blank= False + on_delete=models.CASCADE, + verbose_name = 'Software' ) + action = models.CharField( - max_length=1, + blank = True, choices=DeviceSoftware.Actions, default=None, + help_text = 'ACtion to perform with this software', + max_length=1, null=True, - blank = True, + verbose_name = 'Action' ) version = models.ForeignKey( SoftwareVersion, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Software Version for this config group', null = True, - blank= True + on_delete=models.CASCADE, + verbose_name = 'Verrsion', ) # This model is not intended to be viewable on it's own page diff --git a/app/core/migrations/0010_alter_history_action_alter_history_after_and_more.py b/app/core/migrations/0010_alter_history_action_alter_history_after_and_more.py new file mode 100644 index 000000000..1a03ba66f --- /dev/null +++ b/app/core/migrations/0010_alter_history_action_alter_history_after_and_more.py @@ -0,0 +1,206 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('config_management', '0006_alter_configgrouphosts_group_and_more'), + ('core', '0009_alter_notes_options'), + ('itam', '0014_alter_softwarecategory_options'), + ('itim', '0005_alter_cluster_cluster_type_alter_cluster_id_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='history', + name='action', + field=models.IntegerField(choices=[('1', 'Create'), ('2', 'Update'), ('3', 'Delete')], default=None, help_text='History action performed', null=True, verbose_name='Action'), + ), + migrations.AlterField( + model_name='history', + name='after', + field=models.JSONField(blank=True, default=None, help_text='JSON Object After Change', null=True, verbose_name='After'), + ), + migrations.AlterField( + model_name='history', + name='before', + field=models.JSONField(blank=True, default=None, help_text='JSON Object before Change', null=True, verbose_name='Before'), + ), + migrations.AlterField( + model_name='history', + name='id', + field=models.AutoField(help_text='ID for this history entry', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='history', + name='item_class', + field=models.CharField(default=None, help_text='Class of the item this history relates to', max_length=50, null=True), + ), + migrations.AlterField( + model_name='history', + name='item_parent_class', + field=models.CharField(default=None, help_text='Class oof the Paarent Item this history relates to', max_length=50, null=True, verbose_name='Parent Class'), + ), + migrations.AlterField( + model_name='history', + name='item_parent_pk', + field=models.IntegerField(default=None, help_text='Primary Key of the Parent Item this history relates to', null=True, verbose_name='Parent ID'), + ), + migrations.AlterField( + model_name='history', + name='item_pk', + field=models.IntegerField(default=None, help_text='Primary Key of the item this history relates to', null=True, verbose_name='Item ID'), + ), + migrations.AlterField( + model_name='history', + name='user', + field=models.ForeignKey(help_text='User whom performed the action this history relates to', null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + migrations.AlterField( + model_name='manufacturer', + name='id', + field=models.AutoField(help_text='ID of the item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='manufacturer', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='manufacturer', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='manufacturer', + name='name', + field=models.CharField(help_text='Name of this manufacturer', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='manufacturer', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='notes', + name='config_group', + field=models.ForeignKey(blank=True, default=None, help_text='Config group this note belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='config_management.configgroups', verbose_name='Config Group'), + ), + migrations.AlterField( + model_name='notes', + name='device', + field=models.ForeignKey(blank=True, default=None, help_text='Device this note belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.device', verbose_name='Device'), + ), + migrations.AlterField( + model_name='notes', + name='id', + field=models.AutoField(help_text='ID of this note', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='notes', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='notes', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='notes', + name='note', + field=models.TextField(blank=True, default=None, help_text='The tid bit you wish to add', null=True, verbose_name='Note'), + ), + migrations.AlterField( + model_name='notes', + name='operatingsystem', + field=models.ForeignKey(blank=True, default=None, help_text='Operating system this note belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem', verbose_name='Operating System'), + ), + migrations.AlterField( + model_name='notes', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='notes', + name='service', + field=models.ForeignKey(blank=True, default=None, help_text='Service this note belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itim.service', verbose_name='Service'), + ), + migrations.AlterField( + model_name='notes', + name='software', + field=models.ForeignKey(blank=True, default=None, help_text='Software this note belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='itam.software', verbose_name='Software'), + ), + migrations.AlterField( + model_name='notes', + name='usercreated', + field=models.ForeignKey(blank=True, default=None, help_text='User whom added Note', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='usercreated', to=settings.AUTH_USER_MODEL, verbose_name='Added By'), + ), + migrations.AlterField( + model_name='notes', + name='usermodified', + field=models.ForeignKey(blank=True, default=None, help_text='User whom modified the note', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='usermodified', to=settings.AUTH_USER_MODEL, verbose_name='Edited By'), + ), + migrations.AlterField( + model_name='relatedtickets', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticket', + name='id', + field=models.AutoField(help_text='ID of the item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='ticket', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticketcategory', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='ticketcategory', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='ticketcategory', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticketcomment', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticketcommentcategory', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='ticketcommentcategory', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='ticketcommentcategory', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='ticketlinkeditem', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + ] diff --git a/app/core/models/history.py b/app/core/models/history.py index c986c33ae..e68a96188 100644 --- a/app/core/models/history.py +++ b/app/core/models/history.py @@ -10,9 +10,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID for this history entry', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField() @@ -36,62 +38,75 @@ class Actions(models.TextChoices): before = models.JSONField( - help_text = 'JSON Object before Change', blank = True, default = None, - null = True + help_text = 'JSON Object before Change', + null = True, + verbose_name = 'Before' ) after = models.JSONField( - help_text = 'JSON Object After Change', blank = True, default = None, - null = True + help_text = 'JSON Object After Change', + null = True, + verbose_name = 'After' ) action = models.IntegerField( + blank = False, choices=Actions, default=None, + help_text = 'History action performed', null=True, - blank = False, + verbose_name = 'Action' ) user = models.ForeignKey( User, - on_delete=models.DO_NOTHING, - null = True, blank= False, + help_text = 'User whom performed the action this history relates to', + null = True, + on_delete=models.DO_NOTHING, + verbose_name = 'User' ) item_pk = models.IntegerField( + blank = False, default=None, + help_text = 'Primary Key of the item this history relates to', null = True, - blank = False, + verbose_name = 'Item ID' ) item_class = models.CharField( blank = False, default=None, + help_text = 'Class of the item this history relates to', null = True, max_length = 50, unique = False, ) item_parent_pk = models.IntegerField( + blank = False, default=None, + help_text = 'Primary Key of the Parent Item this history relates to', null = True, - blank = False, + verbose_name = 'Parent ID' ) item_parent_class = models.CharField( blank = False, default=None, - null = True, + help_text = 'Class oof the Paarent Item this history relates to', max_length = 50, + null = True, unique = False, + verbose_name = 'Parent Class' ) diff --git a/app/core/models/manufacturer.py b/app/core/models/manufacturer.py index 04fa39ad3..da08bf186 100644 --- a/app/core/models/manufacturer.py +++ b/app/core/models/manufacturer.py @@ -14,9 +14,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID of manufacturer', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField() @@ -41,8 +43,10 @@ class Meta: name = models.CharField( blank = False, + help_text = 'Name of this manufacturer', max_length = 50, unique = True, + verbose_name = 'Name' ) diff --git a/app/core/models/notes.py b/app/core/models/notes.py index 826def8ad..af2252bbc 100644 --- a/app/core/models/notes.py +++ b/app/core/models/notes.py @@ -20,9 +20,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID of this note', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField() @@ -53,71 +55,84 @@ class Meta: note = models.TextField( - verbose_name = 'Note', blank = True, default = None, - null = True + help_text = 'The tid bit you wish to add', + null = True, + verbose_name = 'Note', ) usercreated = models.ForeignKey( User, - verbose_name = 'Added By', - related_name = 'usercreated', - on_delete=models.SET_DEFAULT, + blank= True, default = None, + help_text = 'User whom added Note', null = True, - blank= True + on_delete=models.DO_NOTHING, + related_name = 'usercreated', + verbose_name = 'Added By', ) usermodified = models.ForeignKey( User, - verbose_name = 'Edited By', - related_name = 'usermodified', - on_delete=models.SET_DEFAULT, + blank= True, default = None, + help_text = 'User whom modified the note', null = True, - blank= True + on_delete=models.DO_NOTHING, + related_name = 'usermodified', + verbose_name = 'Edited By', ) config_group = models.ForeignKey( ConfigGroups, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Config group this note belongs to', null = True, - blank= True + on_delete=models.CASCADE, + verbose_name = 'Config Group' ) device = models.ForeignKey( Device, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Device this note belongs to', null = True, - blank= True + on_delete=models.CASCADE, + verbose_name = 'Device' ) service = models.ForeignKey( Service, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Service this note belongs to', null = True, - blank= True + on_delete=models.CASCADE, + verbose_name = 'Service' ) software = models.ForeignKey( Software, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Software this note belongs to', null = True, - blank= True + on_delete=models.CASCADE, + verbose_name = 'Software' ) operatingsystem = models.ForeignKey( OperatingSystem, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Operating system this note belongs to', null = True, - blank= True + on_delete=models.CASCADE, + verbose_name = 'Operating System' ) # this model is not intended to have its own viewable page as diff --git a/app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py b/app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py new file mode 100644 index 000000000..52cb9868a --- /dev/null +++ b/app/itam/migrations/0015_alter_device_device_model_alter_device_device_type_and_more.py @@ -0,0 +1,337 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +import itam.models.device +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('itam', '0014_alter_softwarecategory_options'), + ] + + operations = [ + migrations.AlterField( + model_name='device', + name='device_model', + field=models.ForeignKey(blank=True, default=None, help_text='Model of the device.', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.devicemodel', verbose_name='Model'), + ), + migrations.AlterField( + model_name='device', + name='device_type', + field=models.ForeignKey(blank=True, default=None, help_text='Type of device.', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.devicetype', verbose_name='Type'), + ), + migrations.AlterField( + model_name='device', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='device', + name='inventorydate', + field=models.DateTimeField(blank=True, help_text='Date and time of the last inventory', null=True, verbose_name='Last Inventory Date'), + ), + migrations.AlterField( + model_name='device', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='device', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='device', + name='name', + field=models.CharField(help_text='Hostname of this device', max_length=50, unique=True, validators=[itam.models.device.Device.validate_hostname_format], verbose_name='Name'), + ), + migrations.AlterField( + model_name='device', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='devicemodel', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='devicemodel', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='devicemodel', + name='manufacturer', + field=models.ForeignKey(blank=True, default=None, help_text='Manufacturer this model is from', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='core.manufacturer', verbose_name='Manufacturer'), + ), + migrations.AlterField( + model_name='devicemodel', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='devicemodel', + name='name', + field=models.CharField(help_text='The items name', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='devicemodel', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='device', + field=models.ForeignKey(help_text='Device for the Operating System', on_delete=django.db.models.deletion.CASCADE, to='itam.device', verbose_name='Device'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='installdate', + field=models.DateTimeField(blank=True, default=None, help_text='Date and time detected as installed', null=True, verbose_name='Install Date'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='operating_system_version', + field=models.ForeignKey(help_text='Operating system version', on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystemversion', verbose_name='Operating System/Version'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='deviceoperatingsystem', + name='version', + field=models.CharField(help_text='Version detected as installed', max_length=15, verbose_name='Installed Version'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='device', + field=models.ForeignKey(help_text='Device this software is on', on_delete=django.db.models.deletion.CASCADE, to='itam.device', verbose_name='Device'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='installedversion', + field=models.ForeignKey(blank=True, default=None, help_text='Version that is installed', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='installedversion', to='itam.softwareversion', verbose_name='Installed Version'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='devicesoftware', + name='software', + field=models.ForeignKey(help_text='Software Name', on_delete=django.db.models.deletion.CASCADE, to='itam.software', verbose_name='Software'), + ), + migrations.AlterField( + model_name='devicetype', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='devicetype', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='devicetype', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='devicetype', + name='name', + field=models.CharField(help_text='The items name', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='devicetype', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='operatingsystem', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='operatingsystem', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='operatingsystem', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='operatingsystem', + name='name', + field=models.CharField(help_text='Name of this item', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='operatingsystem', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='operatingsystem', + name='publisher', + field=models.ForeignKey(blank=True, default=None, help_text='Who publishes this Operating System', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='core.manufacturer', verbose_name='Publisher'), + ), + migrations.AlterField( + model_name='operatingsystemversion', + name='id', + field=models.AutoField(help_text='ID of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='operatingsystemversion', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='operatingsystemversion', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='operatingsystemversion', + name='name', + field=models.CharField(help_text='Major version number for the Operating System', max_length=50, verbose_name='Major Version'), + ), + migrations.AlterField( + model_name='operatingsystemversion', + name='operating_system', + field=models.ForeignKey(help_text='Operating system this version applies to', on_delete=django.db.models.deletion.CASCADE, to='itam.operatingsystem', verbose_name='Operaating System'), + ), + migrations.AlterField( + model_name='operatingsystemversion', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='software', + name='category', + field=models.ForeignKey(blank=True, default=None, help_text='Category of this Softwarae', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itam.softwarecategory', verbose_name='Category'), + ), + migrations.AlterField( + model_name='software', + name='id', + field=models.AutoField(help_text='Id of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='software', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='software', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='software', + name='name', + field=models.CharField(help_text='Name of this item', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='software', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='software', + name='publisher', + field=models.ForeignKey(blank=True, default=None, help_text='Who publishes this software', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='core.manufacturer', verbose_name='Publisher'), + ), + migrations.AlterField( + model_name='softwarecategory', + name='id', + field=models.AutoField(help_text='Id of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='softwarecategory', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='softwarecategory', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='softwarecategory', + name='name', + field=models.CharField(help_text='Name of this item', max_length=50, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='softwarecategory', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='softwareversion', + name='id', + field=models.AutoField(help_text='Id of this item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='softwareversion', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='softwareversion', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='softwareversion', + name='name', + field=models.CharField(help_text='Name of for the software version', max_length=50, verbose_name='Name'), + ), + migrations.AlterField( + model_name='softwareversion', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='softwareversion', + name='software', + field=models.ForeignKey(help_text='Software this version applies', on_delete=django.db.models.deletion.CASCADE, to='itam.software', verbose_name='Software'), + ), + ] diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 3894a276d..db4f72013 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -141,65 +141,70 @@ def validate_hostname_format(self): name = models.CharField( blank = False, + help_text = 'Hostname of this device', max_length = 50, unique = True, - validators = [ validate_hostname_format ] + validators = [ validate_hostname_format ], + verbose_name = 'Name' ) serial_number = models.CharField( - verbose_name = 'Serial Number', - max_length = 50, + blank = True, default = None, + help_text = 'Serial number of the device.', + max_length = 50, null = True, - blank = True, unique = True, - help_text = 'Serial number of the device.', + verbose_name = 'Serial Number', ) uuid = models.CharField( - verbose_name = 'UUID', - max_length = 50, + blank = True, default = None, + help_text = 'System GUID/UUID.', + max_length = 50, null = True, - blank = True, unique = True, - help_text = 'System GUID/UUID.', - validators = [ validate_uuid_format ] + validators = [ validate_uuid_format ], + verbose_name = 'UUID' ) device_model = models.ForeignKey( DeviceModel, - on_delete=models.CASCADE, - default = None, - null = True, blank= True, + default = None, help_text = 'Model of the device.', + null = True, + on_delete=models.SET_DEFAULT, + verbose_name = 'Model' ) device_type = models.ForeignKey( DeviceType, - on_delete=models.CASCADE, - default = None, - null = True, blank= True, + default = None, help_text = 'Type of device.', + null = True, + on_delete=models.SET_DEFAULT, + verbose_name = 'Type' ) config = models.JSONField( blank = True, default = None, + help_text = 'Configuration for this device', null = True, validators=[ validate_config_keys_not_reserved ], verbose_name = 'Host Configuration', - help_text = 'Configuration for this device' ) inventorydate = models.DateTimeField( - verbose_name = 'Last Inventory Date', - null = True, blank = True, + help_text = 'Date and time of the last inventory', + null = True, + verbose_name = 'Last Inventory Date', ) is_virtual = models.BooleanField( @@ -502,15 +507,19 @@ class Actions(models.TextChoices): device = models.ForeignKey( Device, blank= False, + help_text = 'Device this software is on', on_delete=models.CASCADE, null = False, + verbose_name = 'Device' ) software = models.ForeignKey( Software, blank= False, + help_text = 'Software Name', null = False, on_delete=models.CASCADE, + verbose_name = 'Software' ) action = models.CharField( @@ -538,6 +547,7 @@ class Actions(models.TextChoices): SoftwareVersion, blank= True, default = None, + help_text = 'Version that is installed', null = True, on_delete=models.CASCADE, related_name = 'installedversion', @@ -633,33 +643,38 @@ class Meta: device = models.ForeignKey( Device, + blank = False, + help_text = 'Device for the Operating System', on_delete = models.CASCADE, null = False, - blank = False, + verbose_name = 'Device' ) operating_system_version = models.ForeignKey( OperatingSystemVersion, - verbose_name = 'Operating System/Version', - on_delete = models.CASCADE, + blank = False, + help_text = 'Operating system version', null = False, - blank = False + on_delete = models.CASCADE, + verbose_name = 'Operating System/Version', ) version = models.CharField( - verbose_name = 'Installed Version', + blank = False, + help_text = 'Version detected as installed', max_length = 15, null = False, - blank = False, + verbose_name = 'Installed Version', ) installdate = models.DateTimeField( - verbose_name = 'Install Date', - null = True, blank = True, default = None, + help_text = 'Date and time detected as installed', + null = True, + verbose_name = 'Install Date', ) page_layout: list = [ diff --git a/app/itam/models/device_common.py b/app/itam/models/device_common.py index 159fcc3bd..5178c461d 100644 --- a/app/itam/models/device_common.py +++ b/app/itam/models/device_common.py @@ -10,9 +10,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID of this item', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField() @@ -28,8 +30,10 @@ class Meta: name = models.CharField( blank = False, + help_text = 'The items name', max_length = 50, unique = True, + verbose_name = 'Name' ) slug = AutoSlugField() diff --git a/app/itam/models/device_models.py b/app/itam/models/device_models.py index 6a5fcf14d..6a9ee5ec1 100644 --- a/app/itam/models/device_models.py +++ b/app/itam/models/device_models.py @@ -27,10 +27,12 @@ class Meta: manufacturer = models.ForeignKey( Manufacturer, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Manufacturer this model is from', null = True, - blank= True + on_delete=models.SET_DEFAULT, + verbose_name = 'Manufacturer' ) page_layout: dict = [ diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index a7f1a1257..00d72b94e 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -14,9 +14,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID of this item', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField() @@ -32,8 +34,10 @@ class Meta: name = models.CharField( blank = False, + help_text = 'Name of this item', max_length = 50, unique = True, + verbose_name = 'Name' ) slug = AutoSlugField() @@ -56,10 +60,12 @@ class Meta: publisher = models.ForeignKey( Manufacturer, - on_delete=models.CASCADE, + blank = True, default = None, + help_text = 'Who publishes this Operating System', null = True, - blank= True + on_delete = models.SET_DEFAULT, + verbose_name = 'Publisher' ) @@ -162,14 +168,17 @@ class Meta: operating_system = models.ForeignKey( OperatingSystem, - on_delete=models.CASCADE, + help_text = 'Operating system this version applies to', + on_delete = models.CASCADE, + verbose_name = 'Operaating System' ) name = models.CharField( - verbose_name = 'Major Version', blank = False, + help_text = 'Major version number for the Operating System', max_length = 50, unique = False, + verbose_name = 'Major Version', ) # model not intended to be viewable on its own diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 6e94ed6de..0b1de3a3d 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -15,15 +15,19 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'Id of this item', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) name = models.CharField( blank = False, + help_text = 'Name of this item', max_length = 50, unique = True, + verbose_name = 'Name' ) slug = AutoSlugField() @@ -117,18 +121,22 @@ class Meta: publisher = models.ForeignKey( Manufacturer, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Who publishes this software', null = True, - blank= True + on_delete=models.SET_DEFAULT, + verbose_name = 'Publisher', ) category = models.ForeignKey( SoftwareCategory, - on_delete=models.CASCADE, + blank= True, default = None, + help_text = 'Category of this Softwarae', null = True, - blank= True + on_delete=models.SET_DEFAULT, + verbose_name = 'Category' ) @@ -239,13 +247,17 @@ class Meta: software = models.ForeignKey( Software, + help_text = 'Software this version applies', on_delete=models.CASCADE, + verbose_name = 'Software', ) name = models.CharField( blank = False, + help_text = 'Name of for the software version', max_length = 50, unique = False, + verbose_name = 'Name' ) diff --git a/app/itim/migrations/0005_alter_cluster_cluster_type_alter_cluster_id_and_more.py b/app/itim/migrations/0005_alter_cluster_cluster_type_alter_cluster_id_and_more.py new file mode 100644 index 000000000..c31c814af --- /dev/null +++ b/app/itim/migrations/0005_alter_cluster_cluster_type_alter_cluster_id_and_more.py @@ -0,0 +1,106 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('itim', '0004_alter_service_config_key_variable'), + ] + + operations = [ + migrations.AlterField( + model_name='cluster', + name='cluster_type', + field=models.ForeignKey(blank=True, default=None, help_text='Type of Cluster', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itim.clustertype', verbose_name='Cluster Type'), + ), + migrations.AlterField( + model_name='cluster', + name='id', + field=models.AutoField(help_text='ID for this cluster', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='cluster', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='cluster', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='cluster', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='cluster', + name='parent_cluster', + field=models.ForeignKey(blank=True, default=None, help_text='Parent Cluster for this cluster', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='itim.cluster', verbose_name='Parent Cluster'), + ), + migrations.AlterField( + model_name='clustertype', + name='id', + field=models.AutoField(help_text='ID for this cluster type', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='clustertype', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='clustertype', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='clustertype', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='port', + name='id', + field=models.AutoField(help_text='ID of this port', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='port', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='port', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='port', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='service', + name='id', + field=models.AutoField(help_text='Id for this Service', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='service', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='service', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='service', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + ] diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index 727fac339..c39c879ba 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -24,9 +24,11 @@ class Meta: id = models.AutoField( + blank=False, + help_text = 'ID for this cluster type', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) name = models.CharField( @@ -139,9 +141,11 @@ class Meta: id = models.AutoField( + blank=False, + help_text = 'ID for this cluster', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) parent_cluster = models.ForeignKey( @@ -150,7 +154,7 @@ class Meta: default = None, help_text = 'Parent Cluster for this cluster', null = True, - on_delete = models.CASCADE, + on_delete = models.SET_DEFAULT, verbose_name = 'Parent Cluster', ) @@ -160,7 +164,7 @@ class Meta: default = None, help_text = 'Type of Cluster', null = True, - on_delete = models.CASCADE, + on_delete = models.SET_DEFAULT, verbose_name = 'Cluster Type', ) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 584e3d065..ce83d9d32 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -40,9 +40,11 @@ def validation_port_number(number: int): id = models.AutoField( + blank=False, + help_text = 'ID of this port', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) number = models.IntegerField( @@ -156,9 +158,11 @@ def validate_config_key_variable(value): id = models.AutoField( + blank=False, + help_text = 'Id for this Service', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) is_template = models.BooleanField( diff --git a/app/project_management/migrations/0002_alter_project_description_alter_project_id_and_more.py b/app/project_management/migrations/0002_alter_project_description_alter_project_id_and_more.py new file mode 100644 index 000000000..784f7f75b --- /dev/null +++ b/app/project_management/migrations/0002_alter_project_description_alter_project_id_and_more.py @@ -0,0 +1,103 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('project_management', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='description', + field=models.TextField(blank=True, default=None, help_text='Outline of this Project', null=True, verbose_name='Description'), + ), + migrations.AlterField( + model_name='project', + name='id', + field=models.AutoField(help_text='ID of this Item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='project', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='project', + name='manager_team', + field=models.ForeignKey(blank=True, help_text='Team which contains the Project Managers', null=True, on_delete=django.db.models.deletion.SET_NULL, to='access.team', verbose_name='Project Manager Team'), + ), + migrations.AlterField( + model_name='project', + name='manager_user', + field=models.ForeignKey(blank=True, help_text='User who is the Project Manager', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='manager_user', to=settings.AUTH_USER_MODEL, verbose_name='Manager'), + ), + migrations.AlterField( + model_name='project', + name='name', + field=models.CharField(help_text='Name of the item', max_length=100, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='project', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='projectmilestone', + name='id', + field=models.AutoField(help_text='ID of this Item', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='projectmilestone', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='projectmilestone', + name='name', + field=models.CharField(help_text='Name of the item', max_length=100, unique=True, verbose_name='Name'), + ), + migrations.AlterField( + model_name='projectmilestone', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='projectstate', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='projectstate', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='projectstate', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='projecttype', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='projecttype', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='projecttype', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + ] diff --git a/app/project_management/models/project_common.py b/app/project_management/models/project_common.py index c885ccd59..f453cea0e 100644 --- a/app/project_management/models/project_common.py +++ b/app/project_management/models/project_common.py @@ -10,9 +10,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID of this Item', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) created = AutoCreatedField( @@ -30,8 +32,10 @@ class Meta: name = models.CharField( blank = False, + help_text = 'Name of the item', max_length = 100, unique = True, + verbose_name = 'Name' ) slug = AutoSlugField() diff --git a/app/project_management/models/projects.py b/app/project_management/models/projects.py index 005ff1e11..ef53fc282 100644 --- a/app/project_management/models/projects.py +++ b/app/project_management/models/projects.py @@ -80,8 +80,10 @@ class Priority(models.IntegerChoices): description = models.TextField( blank = True, + help_text = 'Outline of this Project', default = None, null= True, + verbose_name = 'Description' ) priority = models.IntegerField( @@ -152,18 +154,20 @@ class Priority(models.IntegerChoices): manager_user = models.ForeignKey( User, blank= True, - help_text = '', + help_text = 'User who is the Project Manager', on_delete=models.SET_NULL, null = True, - related_name = 'manager_user' + related_name = 'manager_user', + verbose_name = 'Manager' ) manager_team = models.ForeignKey( Team, blank= True, - help_text = '', + help_text = 'Team which contains the Project Managers', on_delete=models.SET_NULL, null = True, + verbose_name = 'Project Manager Team' ) model_notes = None diff --git a/app/settings/migrations/0006_alter_appsettings_device_model_is_global_and_more.py b/app/settings/migrations/0006_alter_appsettings_device_model_is_global_and_more.py new file mode 100644 index 000000000..936139cf4 --- /dev/null +++ b/app/settings/migrations/0006_alter_appsettings_device_model_is_global_and_more.py @@ -0,0 +1,93 @@ +# Generated by Django 5.1.2 on 2024-10-13 15:27 + +import access.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + ('settings', '0005_alter_externallink_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='appsettings', + name='device_model_is_global', + field=models.BooleanField(default=False, help_text='Should Device Models be global', verbose_name='Global Device Models'), + ), + migrations.AlterField( + model_name='appsettings', + name='device_type_is_global', + field=models.BooleanField(default=False, help_text='Should Device Types be global', verbose_name='Global Device Types'), + ), + migrations.AlterField( + model_name='appsettings', + name='global_organization', + field=models.ForeignKey(blank=True, default=None, help_text='Organization global items will be created in', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='global_organization', to='access.organization', verbose_name='Global Organization'), + ), + migrations.AlterField( + model_name='appsettings', + name='id', + field=models.AutoField(help_text='Id of this setting', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='appsettings', + name='manufacturer_is_global', + field=models.BooleanField(default=False, help_text='Should Manufacturers / Publishers be global', verbose_name='Global Manufacturers / Publishers'), + ), + migrations.AlterField( + model_name='appsettings', + name='owner_organization', + field=models.ForeignKey(blank=True, default=None, help_text='Organization the settings belong to', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='owner_organization', to='access.organization'), + ), + migrations.AlterField( + model_name='appsettings', + name='software_categories_is_global', + field=models.BooleanField(default=False, help_text='Should Software be global', verbose_name='Global Software Categories'), + ), + migrations.AlterField( + model_name='appsettings', + name='software_is_global', + field=models.BooleanField(default=False, help_text='Should Software be global', verbose_name='Global Software'), + ), + migrations.AlterField( + model_name='externallink', + name='id', + field=models.AutoField(help_text='ID for this external link', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='externallink', + name='is_global', + field=models.BooleanField(default=False, help_text='Is this a global object?', verbose_name='Global Object'), + ), + migrations.AlterField( + model_name='externallink', + name='model_notes', + field=models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes'), + ), + migrations.AlterField( + model_name='externallink', + name='organization', + field=models.ForeignKey(help_text='Organization this belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'), + ), + migrations.AlterField( + model_name='usersettings', + name='default_organization', + field=models.ForeignKey(blank=True, default=None, help_text='Users default Organization', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='access.organization', verbose_name='Default Organization'), + ), + migrations.AlterField( + model_name='usersettings', + name='id', + field=models.AutoField(help_text='ID for this user Setting', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='usersettings', + name='user', + field=models.ForeignKey(help_text='User this Setting belongs to', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/app/settings/models/app_settings.py b/app/settings/models/app_settings.py index 52ee33825..50fd05c73 100644 --- a/app/settings/models/app_settings.py +++ b/app/settings/models/app_settings.py @@ -12,9 +12,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'Id of this setting', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) slug = None @@ -40,42 +42,47 @@ class AppSettings(AppSettingsCommonFields, SaveHistory): owner_organization = models.ForeignKey( Organization, - on_delete=models.CASCADE, blank= True, + help_text = 'Organization the settings belong to', default = None, null = True, - help_text = 'Organization the settings belong to', + on_delete=models.SET_DEFAULT, related_name = 'owner_organization' ) device_model_is_global = models.BooleanField ( - verbose_name = 'All Device Models are global', blank= False, + help_text = 'Should Device Models be global', default = False, + verbose_name = 'Global Device Models', ) device_type_is_global = models.BooleanField ( - verbose_name = 'All Device Types is global', blank= False, + help_text = 'Should Device Types be global', default = False, + verbose_name = 'Global Device Types', ) manufacturer_is_global = models.BooleanField ( - verbose_name = 'All Manufacturer / Publishers are global', blank= False, + help_text = 'Should Manufacturers / Publishers be global', default = False, + verbose_name = 'Global Manufacturers / Publishers', ) software_is_global = models.BooleanField ( - verbose_name = 'All Software is global', blank= False, default = False, + help_text = 'Should Software be global', + verbose_name = 'Global Software', ) software_categories_is_global = models.BooleanField ( - verbose_name = 'All Software Categories are global', blank= False, default = False, + help_text = 'Should Software be global', + verbose_name = 'Global Software Categories', ) global_organization = models.ForeignKey( @@ -83,9 +90,10 @@ class AppSettings(AppSettingsCommonFields, SaveHistory): on_delete=models.SET_DEFAULT, blank= True, default = None, - null = True, help_text = 'Organization global items will be created in', - related_name = 'global_organization' + null = True, + related_name = 'global_organization', + verbose_name = 'Global Organization' ) def clean(self): diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py index f887bf672..d2212153a 100644 --- a/app/settings/models/external_link.py +++ b/app/settings/models/external_link.py @@ -21,16 +21,18 @@ class Meta: id = models.AutoField( + blank=False, + help_text = 'ID for this external link', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) name = models.CharField( blank = False, + help_text = 'Name to display on link button', max_length = 30, unique = True, - help_text = 'Name to display on link button', verbose_name = 'Button Name', ) @@ -38,19 +40,19 @@ class Meta: template = models.CharField( blank = False, + help_text = 'External Link template', max_length = 180, unique = False, - help_text = 'External Link template', verbose_name = 'Link Template', ) colour = models.CharField( blank = True, - null = True, default = None, + help_text = 'Colour to render the link button. Use HTML colour code', max_length = 80, + null = True, unique = False, - help_text = 'Colour to render the link button. Use HTML colour code', verbose_name = 'Button Colour', ) diff --git a/app/settings/models/user_settings.py b/app/settings/models/user_settings.py index fbdc80912..ff4b4db70 100644 --- a/app/settings/models/user_settings.py +++ b/app/settings/models/user_settings.py @@ -13,9 +13,11 @@ class Meta: abstract = True id = models.AutoField( + blank=False, + help_text = 'ID for this user Setting', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) slug = None @@ -30,17 +32,21 @@ class UserSettings(UserSettingsCommonFields): user = models.ForeignKey( User, - on_delete=models.CASCADE, blank= False, + help_text = 'User this Setting belongs to', + on_delete=models.CASCADE, + verbose_name = 'User' ) default_organization = models.ForeignKey( Organization, - on_delete=models.DO_NOTHING, blank= True, default = None, + help_text = 'Users default Organization', null = True, + on_delete=models.SET_DEFAULT, + verbose_name = 'Default Organization' ) From a1625517d15946a02cccda933db5a2e9f8a3dd46 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 01:05:17 +0930 Subject: [PATCH 132/617] fix(access): if permission_required attribute doesn't exist during permission check, return empty list ref: #346 --- app/access/mixin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/access/mixin.py b/app/access/mixin.py index ac603a4a3..7353ea40b 100644 --- a/app/access/mixin.py +++ b/app/access/mixin.py @@ -136,6 +136,10 @@ def get_permission_required(self): Override of 'PermissionRequiredMixin' method so that this mixin can obtain the required permission. """ + if not hasattr(self, 'permission_required'): + + return [] + if self.permission_required is None: raise ImproperlyConfigured( f"{self.__class__.__name__} is missing the " From 77ef69488b1d5b98b47c9ead97bc0bca63f3a6e9 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 01:06:04 +0930 Subject: [PATCH 133/617] test: use correct logic when testin field parameters as not being empty or none ref: #346 --- app/app/tests/abstract/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/app/tests/abstract/models.py b/app/app/tests/abstract/models.py index 51e85e512..f1de66afa 100644 --- a/app/app/tests/abstract/models.py +++ b/app/app/tests/abstract/models.py @@ -118,8 +118,8 @@ def test_model_fields_parameter_not_empty_help_text(self): print(f'Checking field {field.attname} is not empty') if ( - field.help_text is not None - or field.help_text != '' + field.help_text is None + or field.help_text == '' ): print(f' Failure on field {field.attname}') @@ -188,8 +188,8 @@ def test_model_fields_parameter_not_empty_verbose_name(self): print(f'Checking field {field.attname} is not empty') if ( - field.verbose_name is not None - or field.verbose_name != '' + field.verbose_name is None + or field.verbose_name == '' ): print(f' Failure on field {field.attname}') From c185c192a750c4687ee34f69c7a6eb7f26ab2dbc Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 01:07:32 +0930 Subject: [PATCH 134/617] test(itim): port placeholder test for invalid port number ref: #346 --- app/itim/tests/unit/port/test_port.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/itim/tests/unit/port/test_port.py b/app/itim/tests/unit/port/test_port.py index b787dda0c..98b4cf27b 100644 --- a/app/itim/tests/unit/port/test_port.py +++ b/app/itim/tests/unit/port/test_port.py @@ -40,3 +40,11 @@ def setUpTestData(self): organization = self.organization, number = 2, ) + + @pytest.mark.skip(reason = 'to be written') + def test_field_entry_invalid_port_to_high(self): + """Test Model Field + + Ensure that a validation error is thrown and that is displays to the user + """ + pass \ No newline at end of file From a05cf021c13b375c120831bf7b78981b5da69a98 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 01:17:06 +0930 Subject: [PATCH 135/617] test(access): Team custom tests to ensure that during model field creation, attribute verbose_name is defined and not empty as team extends group, filtering of group fields is required so they are not checked when testing ref: #248 #345 #346 --- app/access/tests/unit/team/test_team.py | 72 ++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/app/access/tests/unit/team/test_team.py b/app/access/tests/unit/team/test_team.py index f0d1c1b06..113127140 100644 --- a/app/access/tests/unit/team/test_team.py +++ b/app/access/tests/unit/team/test_team.py @@ -67,4 +67,74 @@ def test_attribute_is_type_objects(self): @pytest.mark.skip(reason="uses Django group manager") def test_model_class_tenancy_manager_function_get_queryset_called(self): - pass \ No newline at end of file + pass + + + def test_model_fields_parameter_not_empty_help_text(self): + """Test Field called with Parameter + + This is a custom test of a test derived of the samae name. It's required + as the team model extends the Group model. + + During field creation, paramater `help_text` must not be `None` or empty ('') + """ + + group_mode_fields_to_ignore: list = [ + 'id', + 'name', + 'group_ptr_id' + ] + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + if field.attname in group_mode_fields_to_ignore: + + continue + + print(f'Checking field {field.attname} is not empty') + + if ( + field.help_text is None + or field.help_text == '' + ): + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value + + def test_model_fields_parameter_type_verbose_name(self): + """Test Field called with Parameter + + This is a custom test of a test derived of the samae name. It's required + as the team model extends the Group model. + + During field creation, paramater `verbose_name` must be of type str + """ + + group_mode_fields_to_ignore: list = [ + 'name', + ] + + fields_have_test_value: bool = True + + for field in self.model._meta.fields: + + if field.attname in group_mode_fields_to_ignore: + + continue + + print(f'Checking field {field.attname} is of type str') + + if not type(field.verbose_name) is str: + + print(f' Failure on field {field.attname}') + + fields_have_test_value = False + + + assert fields_have_test_value From 1073b2228e98f4d70eb4dc1d43dd386a1dd6cb17 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 01:17:44 +0930 Subject: [PATCH 136/617] docs(release_notes): fluff out feature freeze details ref: #346 --- Release-Notes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Release-Notes.md b/Release-Notes.md index 83f84df3a..3566af2ed 100644 --- a/Release-Notes.md +++ b/Release-Notes.md @@ -1,9 +1,13 @@ ## Version 1.4.0 -- Depreciation of **ALL** API urls. will be [removed in v2.0.0](https://github.com/nofusscomputing/centurion_erp/issues/343) release of Centurion. +API redesign in preparation for moving the UI out of centurion to it's [own project](https://github.com/nofusscomputing/centurion_erp_ui). This release introduces a **Feature freeze** to the current UI. Only bug fixes will be done for the current UI. - New API will be at path `api/v2` and will remain until v2.0.0 release of Centurion on which the `api/v2` path will be moved to `api` +- API v1 is now **Feature frozen** with only bug fixes being completed. It's recommended that you move to and start using API v2 as this has feature parity with API v1. + +- Depreciation of **ALL** API urls. API v1 Will be [removed in v2.0.0](https://github.com/nofusscomputing/centurion_erp/issues/343) release of Centurion. + # Version 1.3.0 From 074d12b99b9b903ddb58a62e99aada0291c9f3ff Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 17:28:29 +0930 Subject: [PATCH 137/617] docs(swagger): normalize api v1/v2 docs ref: #248 #348 --- app/app/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/app/settings.py b/app/app/settings.py index ba4e530fa..d4a1c00fb 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -309,6 +309,7 @@ """, 'VERSION': '1.0.0', + 'SCHEMA_PATH_PREFIX': '/api(/v2)?', 'SERVE_INCLUDE_SCHEMA': False, 'SWAGGER_UI_DIST': 'SIDECAR', From b9301e4697227934b2a428ee59e6578735da32be Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 17:47:49 +0930 Subject: [PATCH 138/617] feat(base): Add user API endpoint ref: #248 #348 --- app/api/urls.py | 5 +++ app/api/viewsets/index.py | 1 + app/app/serializers/user.py | 5 +++ app/app/viewsets/base/index.py | 29 +++++++++++++ app/app/viewsets/base/user.py | 77 ++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+) create mode 100644 app/app/viewsets/base/index.py create mode 100644 app/app/viewsets/base/user.py diff --git a/app/api/urls.py b/app/api/urls.py index 1cbc80aca..d84b6adbf 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -33,6 +33,11 @@ index as v2 ) +from app.viewsets.base import ( + index as base_index_v2, + user as user_v2 +) + from access.viewset import ( index as access_v2 ) diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py index a5456003e..a739d138b 100644 --- a/app/api/viewsets/index.py +++ b/app/api/viewsets/index.py @@ -31,6 +31,7 @@ def list(self, request, *args, **kwargs): { "access": reverse('API:_api_v2_access_home-list', request=request), "assistance": reverse('API:_api_v2_assistance_home-list', request=request), + "base": reverse('API:_api_v2_base_home-list', request=request), "itam": reverse('API:_api_v2_itam_home-list', request=request), "itim": reverse('API:_api_v2_itim_home-list', request=request), "config_management": reverse('API:_api_v2_config_management_home-list', request=request), diff --git a/app/app/serializers/user.py b/app/app/serializers/user.py index 721a5cbda..23a959b9a 100644 --- a/app/app/serializers/user.py +++ b/app/app/serializers/user.py @@ -13,6 +13,9 @@ def get_display_name(self, item): return str( item ) + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_user-detail", format="html" + ) class Meta: @@ -27,6 +30,7 @@ class Meta: 'last_name', 'username', 'is_active', + 'url' ] read_only_fields = [ @@ -36,4 +40,5 @@ class Meta: 'last_name', 'username', 'is_active', + 'url' ] diff --git a/app/app/viewsets/base/index.py b/app/app/viewsets/base/index.py new file mode 100644 index 000000000..ede043db0 --- /dev/null +++ b/app/app/viewsets/base/index.py @@ -0,0 +1,29 @@ +from drf_spectacular.utils import extend_schema + +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from api.viewsets.common import CommonViewSet + + + +@extend_schema(exclude = True) +class Index(CommonViewSet): + + allowed_methods: list = [ + 'GET', + 'OPTIONS' + ] + + view_description = "Base Objects" + + view_name = "Base" + + + def list(self, request, pk=None): + + return Response( + { + "user": reverse('API:_api_v2_user-list', request=request) + } + ) diff --git a/app/app/viewsets/base/user.py b/app/app/viewsets/base/user.py new file mode 100644 index 000000000..119d7481b --- /dev/null +++ b/app/app/viewsets/base/user.py @@ -0,0 +1,77 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated + +from api.react_ui_metadata import ReactUIMetadata +from api.viewsets.common import ModelViewSet + +from app.serializers.user import ( + User, + UserBaseSerializer +) + + + +@extend_schema_view( + list = extend_schema( + summary = 'Fetch all users', + description='', + responses = { + 200: OpenApiResponse(description='', response=UserBaseSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single user', + description='', + responses = { + 200: OpenApiResponse(description='', response=UserBaseSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), +) +class ViewSet( + viewsets.ReadOnlyModelViewSet +): + + + documentation: str = '' + + filterset_fields = [ + 'username', + 'first_name', + 'last_name', + 'is_active' + ] + + metadata_class = ReactUIMetadata + + model = User + + permission_classes = [ + IsAuthenticated, + ] + + queryset = User.objects.all() + + search_fields = [ + 'username', + 'first_name', + 'last_name', + ] + + view_description = 'Centurion Users' + + + def get_serializer_class(self): + + return UserBaseSerializer + + def get_view_name(self): + + if self.detail: + + return 'User' + + return 'Users' From 79f17a7d579966ac7c4c64bf283ab3f3c77e2ef8 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 19:21:56 +0930 Subject: [PATCH 139/617] feat(api): Add Read Only abstract ViewSet ref: #248 #348 --- app/api/viewsets/common.py | 23 +++++++++++++++++-- app/app/viewsets/base/user.py | 16 ++----------- .../centurion_erp/development/views.md | 8 ++++++- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index d48c8ac61..631691179 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -1,6 +1,7 @@ from django.utils.safestring import mark_safe from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated from access.mixin import OrganizationMixin @@ -94,8 +95,7 @@ def get_view_name(self): -class ModelViewSet( - viewsets.ModelViewSet, +class ModelViewSetBase( CommonViewSet ): @@ -187,3 +187,22 @@ def get_serializer_class(self): return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] + + + +class ModelViewSet( + viewsets.ModelViewSet, + ModelViewSetBase +): + + pass + + +class ReadOnlyModelViewSet( + viewsets.ReadOnlyModelViewSet, + ModelViewSetBase +): + + permission_classes = [ + IsAuthenticated, + ] diff --git a/app/app/viewsets/base/user.py b/app/app/viewsets/base/user.py index 119d7481b..9a796a21c 100644 --- a/app/app/viewsets/base/user.py +++ b/app/app/viewsets/base/user.py @@ -1,10 +1,6 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse -from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated - -from api.react_ui_metadata import ReactUIMetadata -from api.viewsets.common import ModelViewSet +from api.viewsets.common import ReadOnlyModelViewSet from app.serializers.user import ( User, @@ -32,7 +28,7 @@ ), ) class ViewSet( - viewsets.ReadOnlyModelViewSet + ReadOnlyModelViewSet ): @@ -45,16 +41,8 @@ class ViewSet( 'is_active' ] - metadata_class = ReactUIMetadata - model = User - permission_classes = [ - IsAuthenticated, - ] - - queryset = User.objects.all() - search_fields = [ 'username', 'first_name', diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 471dd04f5..9e7e4ad5a 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -16,7 +16,13 @@ Views are used with Centurion ERP to Fetch the data for rendering. - Views are class based -- Inherits from base class `api.viewsets.common.CommonViewSet` +- Inherits from one of the following base class': + + - Index Viewset `api.viewsets.common.CommonViewSet` + + - Model Viewset `api.viewsets.common.ModelViewSet` + + - Model Viewset that are to be Read-Only `api.viewsets.common.ReadOnlyModelViewSet` - **ALL** views are `ViewSets` From bb93ef3f1dce081c0b4ce72bf438e1b1451eb114 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 20:05:47 +0930 Subject: [PATCH 140/617] feat(base): Add Content Type API endpoint ref: #248 #348 --- app/api/urls.py | 4 ++ app/app/serializers/content_type.py | 69 +++++++++++++++++++++++++++ app/app/viewsets/base/content_type.py | 59 +++++++++++++++++++++++ app/app/viewsets/base/index.py | 1 + 4 files changed, 133 insertions(+) create mode 100644 app/app/serializers/content_type.py create mode 100644 app/app/viewsets/base/content_type.py diff --git a/app/api/urls.py b/app/api/urls.py index d84b6adbf..72cb9c11f 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -35,6 +35,7 @@ from app.viewsets.base import ( index as base_index_v2, + content_type as content_type_v2, user as user_v2 ) @@ -107,6 +108,9 @@ router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') +router.register('v2/base', base_index_v2.Index, basename='_api_v2_base_home') +router.register('v2/base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') + router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') diff --git a/app/app/serializers/content_type.py b/app/app/serializers/content_type.py new file mode 100644 index 000000000..79ae32dee --- /dev/null +++ b/app/app/serializers/content_type.py @@ -0,0 +1,69 @@ +from django.contrib.auth.models import ContentType + +from rest_framework import serializers +from rest_framework.reverse import reverse + + + +class ContentTypeBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_content_type-detail", format="html" + ) + + class Meta: + + model = ContentType + + fields = '__all__' + + fields = [ + 'id', + 'display_name', + 'url' + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url' + ] + + + +class ContentTypeViewSerializer(ContentTypeBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_content_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + } + + + class Meta: + + model = ContentType + + fields = [ + 'id', + 'app_label', + 'model', + '_urls', + ] + + read_only_fields = [ + 'id', + 'app_label', + 'model', + '_urls', + ] \ No newline at end of file diff --git a/app/app/viewsets/base/content_type.py b/app/app/viewsets/base/content_type.py new file mode 100644 index 000000000..2e66aa743 --- /dev/null +++ b/app/app/viewsets/base/content_type.py @@ -0,0 +1,59 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ReadOnlyModelViewSet + +from app.serializers.content_type import ( + ContentType, + ContentTypeViewSerializer +) + + + +@extend_schema_view( + list = extend_schema( + summary = 'Fetch all content types', + description='', + responses = { + 200: OpenApiResponse(description='', response=ContentTypeViewSerializer), + } + ), + retrieve = extend_schema( + summary = 'Fetch a content type', + description='', + responses = { + 200: OpenApiResponse(description='', response=ContentTypeViewSerializer), + } + ), +) +class ViewSet( + ReadOnlyModelViewSet +): + + + filterset_fields = [ + 'app_label', + 'model', + ] + + documentation: str = '' + + model = ContentType + + search_fields = [ + 'display_name', + ] + + view_description = 'Centurion Content Types' + + + def get_serializer_class(self): + + return ContentTypeViewSerializer + + def get_view_name(self): + + if self.detail: + + return 'Content Type' + + return 'Content Types' diff --git a/app/app/viewsets/base/index.py b/app/app/viewsets/base/index.py index ede043db0..ea8007d21 100644 --- a/app/app/viewsets/base/index.py +++ b/app/app/viewsets/base/index.py @@ -24,6 +24,7 @@ def list(self, request, pk=None): return Response( { + "content_type": reverse('API:_api_v2_content_type-list', request=request), "user": reverse('API:_api_v2_user-list', request=request) } ) From 0bd057b43634e93d03d532b7ce2950a448a45d61 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 20:11:14 +0930 Subject: [PATCH 141/617] feat(base): Add Permission API endpoint ref: #248 #348 --- app/api/urls.py | 7 ++- app/app/serializers/permission.py | 77 ++++++++++++++++++++++++++++++ app/app/viewsets/base/index.py | 1 + app/app/viewsets/base/permisson.py | 50 +++++++++++++++++++ 4 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 app/app/serializers/permission.py create mode 100644 app/app/viewsets/base/permisson.py diff --git a/app/api/urls.py b/app/api/urls.py index 72cb9c11f..f563c619e 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -36,6 +36,7 @@ from app.viewsets.base import ( index as base_index_v2, content_type as content_type_v2, + permisson as permission_v2, user as user_v2 ) @@ -108,10 +109,12 @@ router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') +router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') + router.register('v2/base', base_index_v2.Index, basename='_api_v2_base_home') router.register('v2/base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') - -router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') +router.register('v2/base/permission', permission_v2.ViewSet, basename='_api_v2_permission') +router.register('v2/base/user', user_v2.ViewSet, basename='_api_v2_user') router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') diff --git a/app/app/serializers/permission.py b/app/app/serializers/permission.py new file mode 100644 index 000000000..8b110bca3 --- /dev/null +++ b/app/app/serializers/permission.py @@ -0,0 +1,77 @@ +from django.contrib.auth.models import Permission + +from rest_framework import serializers +from rest_framework.reverse import reverse + +from app.serializers.content_type import ContentTypeBaseSerializer + + +class PermissionBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_permission-detail", format="html" + ) + + class Meta: + + model = Permission + + fields = '__all__' + + fields = [ + 'id', + 'display_name', + 'url' + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url' + ] + + + +class PermissionViewSerializer(PermissionBaseSerializer): + + + content_type = ContentTypeBaseSerializer() + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_permission-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + } + + + class Meta: + + model = Permission + + fields = [ + 'id', + 'name', + 'display_name', + 'codename', + 'content_type', + '_urls', + ] + + read_only_fields = [ + 'id', + 'name', + 'display_name', + 'codename', + 'content_type', + '_urls', + ] + diff --git a/app/app/viewsets/base/index.py b/app/app/viewsets/base/index.py index ea8007d21..7cfda34cb 100644 --- a/app/app/viewsets/base/index.py +++ b/app/app/viewsets/base/index.py @@ -25,6 +25,7 @@ def list(self, request, pk=None): return Response( { "content_type": reverse('API:_api_v2_content_type-list', request=request), + "permission": reverse('API:_api_v2_permission-list', request=request), "user": reverse('API:_api_v2_user-list', request=request) } ) diff --git a/app/app/viewsets/base/permisson.py b/app/app/viewsets/base/permisson.py new file mode 100644 index 000000000..c10728007 --- /dev/null +++ b/app/app/viewsets/base/permisson.py @@ -0,0 +1,50 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ReadOnlyModelViewSet + +from app.serializers.permission import ( + Permission, + PermissionViewSerializer +) + + + +@extend_schema_view( + list = extend_schema( + summary = 'Fetch all permissions', + description='', + responses = { + 200: OpenApiResponse(description='', response=PermissionViewSerializer), + } + ), + retrieve = extend_schema( + summary = 'Fetch a permission', + description='', + responses = { + 200: OpenApiResponse(description='', response=PermissionViewSerializer), + } + ), +) +class ViewSet( + ReadOnlyModelViewSet +): + + + documentation: str = '' + + model = Permission + + view_description = 'Centurion Permissions' + + + def get_serializer_class(self): + + return PermissionViewSerializer + + def get_view_name(self): + + if self.detail: + + return 'Permission' + + return 'Permissions' From c6c8bfd045ce8f6488a80c7f4589f54163199c28 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 20:41:06 +0930 Subject: [PATCH 142/617] feat(base): Add Team API endpoint ref: #248 #348 --- app/access/serializers/teams.py | 129 ++++++++++++++++++++++++++++++++ app/access/viewsets/team.py | 98 ++++++++++++++++++++++++ app/api/urls.py | 6 +- 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 app/access/serializers/teams.py create mode 100644 app/access/viewsets/team.py diff --git a/app/access/serializers/teams.py b/app/access/serializers/teams.py new file mode 100644 index 000000000..2fdaf2ed9 --- /dev/null +++ b/app/access/serializers/teams.py @@ -0,0 +1,129 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.models import Team +from access.serializers.organization import OrganizationBaseSerializer + +from app.serializers.permission import PermissionBaseSerializer + + + +class TeamBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_organization_team-detail", + request=self.context['view'].request, + kwargs={ + 'organization_id': item.organization.id, + 'pk': item.pk + } + ) + + + class Meta: + + model = Team + + fields = [ + 'id', + 'display_name', + 'team_name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'team_name', + 'url', + ] + + + +class TeamModelSerializer(TeamBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + 'API:_api_v2_organization_team-detail', + request=self.context['view'].request, + kwargs={ + 'organization_id': item.organization.id, + 'pk': item.pk + } + ) + } + + + class Meta: + + model = Team + + fields = '__all__' + + fields = [ + 'id', + 'display_name', + 'team_name', + 'model_notes', + 'permissions', + 'organization', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'organization', + 'created', + 'modified', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=True) -> bool: + + is_valid = False + + try: + + is_valid = super().is_valid(raise_exception=raise_exception) + + self.validated_data['organization_id'] = int(self._context['view'].kwargs['organization_id']) + + except Exception as unhandled_exception: + + serializers.ParseError( + detail=f"Server encountered an error during validation, Traceback: {unhandled_exception.with_traceback}" + ) + + return is_valid + + + +class TeamViewSerializer(TeamModelSerializer): + + organization = OrganizationBaseSerializer(many=False, read_only=True) + + permissions = PermissionBaseSerializer(many = True) diff --git a/app/access/viewsets/team.py b/app/access/viewsets/team.py new file mode 100644 index 000000000..be9703631 --- /dev/null +++ b/app/access/viewsets/team.py @@ -0,0 +1,98 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from access.serializers.teams import ( + Team, + TeamModelSerializer, + TeamViewSerializer +) + +from api.viewsets.common import ModelViewSet + + + +# @extend_schema(tags=['access']) +@extend_schema_view( + create=extend_schema( + summary = 'Create a team within this organization', + description='', + responses = { + 200: OpenApiResponse(description='Allready exists', response=TeamViewSerializer), + 201: OpenApiResponse(description='Created', response=TeamViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a team from this organization', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all teams from this organization', + description='', + responses = { + 200: OpenApiResponse(description='', response=TeamViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single team from this organization', + description='', + responses = { + 200: OpenApiResponse(description='', response=TeamViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a team within this organization', + description = '', + responses = { + 200: OpenApiResponse(description='', response=TeamViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'team_name', + ] + + search_fields = [ + 'team_name', + ] + + model = Team + + documentation: str = '' + + view_description = 'Teams belonging to a single organization' + + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter(organization_id=self.kwargs['organization_id']) + + self.queryset = queryset + + return self.queryset + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] + diff --git a/app/api/urls.py b/app/api/urls.py index f563c619e..9f45051d0 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -40,8 +40,9 @@ user as user_v2 ) -from access.viewset import ( - index as access_v2 +from access.viewsets import ( + index as access_v2, + team as team_v2 ) from assistance.viewset import ( @@ -108,6 +109,7 @@ router.register('v2', v2.Index, basename='_api_v2_home') router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') +router.register('v2/access/organization/(?P[0-9]+)/teams', team_v2.ViewSet, basename='_api_v2_organization_team') router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') From 8da3a0473011e8b83ac6ffca1fceaee82fed31c7 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 21:04:43 +0930 Subject: [PATCH 143/617] feat(access): Add Organization API endpoint ref: #248 #348 --- app/access/models.py | 8 +- app/access/serializers/organization.py | 84 +++++++++++++++++++++ app/access/{viewset => viewsets}/index.py | 2 +- app/access/viewsets/organization.py | 89 +++++++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 app/access/serializers/organization.py rename app/access/{viewset => viewsets}/index.py (84%) create mode 100644 app/access/viewsets/organization.py diff --git a/app/access/models.py b/app/access/models.py index fabbebb56..b6700109d 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -12,6 +12,7 @@ class Organization(SaveHistory): class Meta: + verbose_name = "Organization" verbose_name_plural = "Organizations" ordering = ['name'] @@ -98,7 +99,12 @@ def __str__(self): { "name": "Teams", "slug": "teams", - "sections": [] + "sections": [ + { + "layout": "table", + "field": "teams" + } + ] }, { "name": "Notes", diff --git a/app/access/serializers/organization.py b/app/access/serializers/organization.py new file mode 100644 index 000000000..72855314b --- /dev/null +++ b/app/access/serializers/organization.py @@ -0,0 +1,84 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.models import Organization + +from app.serializers.user import UserBaseSerializer + + + +class OrganizationBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_organization-detail", format="html" + ) + + class Meta: + + model = Organization + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class OrganizationModelSerializer(OrganizationBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_organization-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'teams': reverse("API:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}), + } + + + class Meta: + + model = Organization + + fields = '__all__' + + fields = [ + 'id', + 'display_name', + 'name', + 'model_notes', + 'manager', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + +class OrganizationViewSerializer(OrganizationModelSerializer): + pass + + manager = UserBaseSerializer(many=False, read_only = True) diff --git a/app/access/viewset/index.py b/app/access/viewsets/index.py similarity index 84% rename from app/access/viewset/index.py rename to app/access/viewsets/index.py index 2a1824b8d..c93c9c004 100644 --- a/app/access/viewset/index.py +++ b/app/access/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "organization": "ToDo" + "organization": reverse('API:_api_v2_organization-list', request=request) } ) diff --git a/app/access/viewsets/organization.py b/app/access/viewsets/organization.py new file mode 100644 index 000000000..9c0757e56 --- /dev/null +++ b/app/access/viewsets/organization.py @@ -0,0 +1,89 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from access.serializers.organization import ( + Organization, + OrganizationModelSerializer, + OrganizationViewSerializer +) + +from api.viewsets.common import ModelViewSet + + + +# @extend_schema(tags=['access']) +@extend_schema_view( + create=extend_schema( + summary = 'Create an orgnaization', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=OrganizationViewSerializer), + 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete an orgnaization', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all orgnaizations', + description='', + responses = { + 200: OpenApiResponse(description='', response=OrganizationViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single orgnaization', + description='', + responses = { + 200: OpenApiResponse(description='', response=OrganizationViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update an orgnaization', + description = '', + responses = { + 200: OpenApiResponse(description='', response=OrganizationViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'name', + 'manager', + ] + + search_fields = [ + 'name', + ] + + model = Organization + + documentation: str = '' + + view_description = 'Centurion Organizations' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] + From 606477cb0dbde4d19b5eb7be4391c651ec78d249 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 21:07:44 +0930 Subject: [PATCH 144/617] feat(access): Depreciate Organization API v1 endpoint ref: #248 #348 #343 --- app/api/views/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/views/access.py b/app/api/views/access.py index ee892eca1..5723f8277 100644 --- a/app/api/views/access.py +++ b/app/api/views/access.py @@ -12,7 +12,7 @@ from api.serializers.access import OrganizationSerializer, OrganizationListSerializer, TeamSerializer, TeamPermissionSerializer from api.views.mixin import OrganizationPermissionAPI - +@extend_schema(deprecated=True) @extend_schema_view( get=extend_schema( summary = "Fetch Organizations", @@ -34,7 +34,7 @@ def get_view_name(self): return "Organizations" - +@extend_schema(deprecated=True) @extend_schema_view( get=extend_schema( summary = "Get An Organization", From e917fbf68d1a0dbad8ebf73eec6c5de35cce3c32 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 21:08:20 +0930 Subject: [PATCH 145/617] feat(access): Depreciate Team API v1 endpoint ref: #248 #348 #343 --- app/api/views/access.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/views/access.py b/app/api/views/access.py index 5723f8277..208098f5f 100644 --- a/app/api/views/access.py +++ b/app/api/views/access.py @@ -61,7 +61,7 @@ def get_view_name(self): return "Organization" - +@extend_schema(deprecated=True) @extend_schema_view( post=extend_schema( summary = "Create a Team", @@ -97,7 +97,7 @@ def get_view_name(self): return "Organization Teams" - +@extend_schema(deprecated=True) @extend_schema_view( get=extend_schema( summary = "Fetch a Team", @@ -149,7 +149,7 @@ class TeamDetail(generics.RetrieveUpdateDestroyAPIView): lookup_field = 'group_ptr_id' - +@extend_schema(deprecated=True) @extend_schema_view( get=extend_schema( summary = "Fetch a teams permissions", From 5edbdd76f408c02cc9a39fbf82b420d35edd9a34 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 21:09:42 +0930 Subject: [PATCH 146/617] docs(swagger): refine normalisation of api v1/v2 docs ref: #248 #345 #346 --- app/app/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/settings.py b/app/app/settings.py index d4a1c00fb..404871920 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -309,7 +309,7 @@ """, 'VERSION': '1.0.0', - 'SCHEMA_PATH_PREFIX': '/api(/v2)?', + 'SCHEMA_PATH_PREFIX': '/api/v2/([a-z]+)|/api', 'SERVE_INCLUDE_SCHEMA': False, 'SWAGGER_UI_DIST': 'SIDECAR', From 2690e17f93d58475003d9b1e1af51426d6825539 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 21:53:34 +0930 Subject: [PATCH 147/617] test(api): API Permission ViewSet Abstract Class added ref: #15 #248 #345 --- .../tests/abstract/api_permissions_viewset.py | 470 ++++++++++++++++++ .../api/tests/model_permission_api_add.md | 11 - .../api/tests/model_permission_api_change.md | 11 - .../api/tests/model_permission_api_delete.md | 11 - .../api/tests/model_permission_api_view.md | 11 - .../api/tests/model_permissions_api.md | 12 - .../centurion_erp/development/testing.md | 34 +- mkdocs.yml | 10 - 8 files changed, 496 insertions(+), 74 deletions(-) create mode 100644 app/api/tests/abstract/api_permissions_viewset.py delete mode 100644 docs/projects/centurion_erp/development/api/tests/model_permission_api_add.md delete mode 100644 docs/projects/centurion_erp/development/api/tests/model_permission_api_change.md delete mode 100644 docs/projects/centurion_erp/development/api/tests/model_permission_api_delete.md delete mode 100644 docs/projects/centurion_erp/development/api/tests/model_permission_api_view.md delete mode 100644 docs/projects/centurion_erp/development/api/tests/model_permissions_api.md diff --git a/app/api/tests/abstract/api_permissions_viewset.py b/app/api/tests/abstract/api_permissions_viewset.py new file mode 100644 index 000000000..9f40b6e3c --- /dev/null +++ b/app/api/tests/abstract/api_permissions_viewset.py @@ -0,0 +1,470 @@ +import pytest +import unittest + +from django.shortcuts import reverse +from django.test import TestCase, Client + + + +class APIPermissionView: + + + model: object + """ Item Model to test """ + + app_namespace: str = None + """ URL namespace """ + + url_name: str + """ URL name of the view to test """ + + url_view_kwargs: dict = None + """ URL kwargs of the item page """ + + + def test_view_user_anon_denied(self): + """ Check correct permission for view + + Attempt to view as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + response = client.get(url) + + assert response.status_code == 401 + + + def test_view_no_permission_denied(self): + """ Check correct permission for view + + Attempt to view with user missing permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.no_permissions_user) + response = client.get(url) + + assert response.status_code == 403 + + + def test_view_different_organizaiton_denied(self): + """ Check correct permission for view + + Attempt to view with user from different organization + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.different_organization_user) + response = client.get(url) + + assert response.status_code == 403 + + + def test_view_has_permission(self): + """ Check correct permission for view + + Attempt to view as user with view permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + assert response.status_code == 200 + + + +class APIPermissionAdd: + + + model: object + """ Item Model to test """ + + app_namespace: str = None + """ URL namespace """ + + url_list: str + """ URL view name of the item list page """ + + url_kwargs: dict = None + """ URL view kwargs for the item list page """ + + add_data: dict = None + + + def test_add_user_anon_denied(self): + """ Check correct permission for add + + Attempt to add as anon user + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + response = client.put(url, data=self.add_data) + + assert response.status_code == 401 + + # @pytest.mark.skip(reason="ToDO: figure out why fails") + def test_add_no_permission_denied(self): + """ Check correct permission for add + + Attempt to add as user with no permissions + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.no_permissions_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 403 + + + # @pytest.mark.skip(reason="ToDO: figure out why fails") + def test_add_different_organization_denied(self): + """ Check correct permission for add + + attempt to add as user from different organization + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.different_organization_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 403 + + + def test_add_permission_view_denied(self): + """ Check correct permission for add + + Attempt to add a user with view permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.view_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 403 + + + def test_add_has_permission(self): + """ Check correct permission for add + + Attempt to add as user with permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 201 + + + +class APIPermissionChange: + + + model: object + """ Item Model to test """ + + app_namespace: str = None + """ URL namespace """ + + url_name: str + """ URL name of the view to test """ + + url_view_kwargs: dict = None + """ URL kwargs of the item page """ + + change_data: dict = None + + + def test_change_user_anon_denied(self): + """ Check correct permission for change + + Attempt to change as anon + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 401 + + + def test_change_no_permission_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user without permissions + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.no_permissions_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 403 + + + def test_change_different_organization_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user from different organization + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.different_organization_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 403 + + + def test_change_permission_view_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user with view permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 403 + + + def test_change_permission_add_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user with add permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.add_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 403 + + + def test_change_has_permission(self): + """ Check correct permission for change + + Make change with user who has change permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 200 + + + +class APIPermissionDelete: + + + model: object + """ Item Model to test """ + + app_namespace: str = None + """ URL namespace """ + + url_name: str + """ URL name of the view to test """ + + url_view_kwargs: dict = None + """ URL kwargs of the item page """ + + delete_data: dict = None + + + def test_delete_user_anon_denied(self): + """ Check correct permission for delete + + Attempt to delete item as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 401 + + + def test_delete_no_permission_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with no permissons + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.no_permissions_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + def test_delete_different_organization_denied(self): + """ Check correct permission for delete + + Attempt to delete as user from different organization + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.different_organization_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + def test_delete_permission_view_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with veiw permission only + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + def test_delete_permission_add_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with add permission only + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.add_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + def test_delete_permission_change_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with change permission only + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + def test_delete_has_permission(self): + """ Check correct permission for delete + + Delete item as user with delete permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.delete_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 204 + + + +class APIPermissions( + APIPermissionAdd, + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +): + """ Abstract class containing all API Permission test cases """ + + model: object + """ Item Model to test """ diff --git a/docs/projects/centurion_erp/development/api/tests/model_permission_api_add.md b/docs/projects/centurion_erp/development/api/tests/model_permission_api_add.md deleted file mode 100644 index e786f7776..000000000 --- a/docs/projects/centurion_erp/development/api/tests/model_permission_api_add.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: API Model Permission add Test Cases -description: No Fuss Computings model permissions add unit tests -date: 2024-06-16 -template: project.html -about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp ---- - -::: app.api.tests.abstract.api_permissions.APIPermissionAdd - options: - show_source: true diff --git a/docs/projects/centurion_erp/development/api/tests/model_permission_api_change.md b/docs/projects/centurion_erp/development/api/tests/model_permission_api_change.md deleted file mode 100644 index 423903993..000000000 --- a/docs/projects/centurion_erp/development/api/tests/model_permission_api_change.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Model Permissions Change Test Cases -description: No Fuss Computings model permissions change unit tests -date: 2024-06-15 -template: project.html -about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp ---- - -::: app.api.tests.abstract.api_permissions.APIPermissionChange - options: - show_source: true diff --git a/docs/projects/centurion_erp/development/api/tests/model_permission_api_delete.md b/docs/projects/centurion_erp/development/api/tests/model_permission_api_delete.md deleted file mode 100644 index 0e4f6512a..000000000 --- a/docs/projects/centurion_erp/development/api/tests/model_permission_api_delete.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Model Permissions Delete Test Cases -description: No Fuss Computings model permissions delete unit tests -date: 2024-06-15 -template: project.html -about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp ---- - -::: app.api.tests.abstract.api_permissions.APIPermissionDelete - options: - show_source: true diff --git a/docs/projects/centurion_erp/development/api/tests/model_permission_api_view.md b/docs/projects/centurion_erp/development/api/tests/model_permission_api_view.md deleted file mode 100644 index 00c8e7e07..000000000 --- a/docs/projects/centurion_erp/development/api/tests/model_permission_api_view.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Model Permissions View Test Cases -description: No Fuss Computings model permissions view unit tests -date: 2024-06-15 -template: project.html -about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp ---- - -::: app.api.tests.abstract.api_permissions.APIPermissionView - options: - show_source: true diff --git a/docs/projects/centurion_erp/development/api/tests/model_permissions_api.md b/docs/projects/centurion_erp/development/api/tests/model_permissions_api.md deleted file mode 100644 index 045dcdb84..000000000 --- a/docs/projects/centurion_erp/development/api/tests/model_permissions_api.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: API Model Permissions Test Cases -description: No Fuss Computings model permissions add unit tests -date: 2024-06-16 -template: project.html -about: https://gitlab.com/nofusscomputing/infrastructure/configuration-management/centurion_erp ---- - -::: app.api.tests.abstract.api_permissions.APIPermissions - options: - show_source: true - inherited_members: true diff --git a/docs/projects/centurion_erp/development/testing.md b/docs/projects/centurion_erp/development/testing.md index fed58be3a..31188380d 100644 --- a/docs/projects/centurion_erp/development/testing.md +++ b/docs/projects/centurion_erp/development/testing.md @@ -10,14 +10,36 @@ Unit tests are written to aid in application stability and to assist in preventi User Interface (UI) test are written _if applicable_ to test the user interface to ensure that it functions as it should. Changes to the UI will need to be tested. +!!! note + As of release v1.3, the UI has moved to it's [own project](https://github.com/nofusscomputing/centurion_erp_ui) with the current Django UI feature locked and depreciated. + In most cases functional tests will not need to be written, however you should confirm this with a maintainer. Integration tests **will** be required if the development introduces code that interacts with an independent third-party application. +## Available Test classes + +To aid in development we have written test classes that you can inherit from for your test classes + +- API Permission Checks + + _These tests ensure that only a user with the correct permissions can perform an action against a Model within Centurion_ + + - `api.tests.abstract.api_permissions_viewset.APIPermissionAdd` _Add permission checks_ + + - `api.tests.abstract.api_permissions_viewset.APIPermissionChange` _Change permission check_ + + - `api.tests.abstract.api_permissions_viewset.APIPermissionDelete` _Delete permission check_ + + - `api.tests.abstract.api_permissions_viewset.APIPermissionView` _View permission check_ + + - `api.tests.abstract.api_permissions_viewset.APIPermissions` _Add, Change, Delete and View permission checks_ + + ## Writing Tests -We use class based tests. Each class will require a `setUpTestData` method for test setup. To furhter assist in the writing of tests, we have written the test cases for common items as an abstract class. You are advised to review the [test cases](./api/tests/index.md) and if it's applicable to the item you have added, than add the test case class to be inherited by your test class. +We use class based tests. Each class will require a `setUpTestData` method for test setup. To furhter assist in the writing of tests, we have written the test cases for common items as an abstract class. You are advised to inherit from our test classes _(see above)_ as a strating point and extend from there. Naming of test classes is in `CamelCase` in format `` for example the class name for device model history entry tests would be `DeviceHistory`. @@ -30,7 +52,6 @@ Example of a model history test class. ``` py import pytest -import unittest import requests from django.test import TestCase, Client @@ -82,22 +103,19 @@ example file system structure showing the layout of the tests directory for a mo │      ├── test__core_history.py │      ├── test__history_permission.py │      ├── test_.py -│      └── test__views.py +│      └── test__viewsets.py ``` Tests are broken up into the type the test is (sub-directory to test), and they are `unit`, `functional`, `UI` and `integration`. These sub-directories each contain a sub-directory for each model they are testing. - Items to test include, and are not limited to: - CRUD permissions admin site -- CRUD permissions api site - [ModelPermissions (API)](./api/tests/model_permissions_api.md) - -- CRUD permissions main site - [ModelPermissions](./api/tests/model_permissions.md) +- CRUD permissions api site -- can only access organization object - [ModelPermissions](./api/tests/model_permissions.md), [ModelPermissions (API)](./api/tests/model_permissions_api.md) +- can only access organization object - can access global object (still to require model CRUD permission) diff --git a/mkdocs.yml b/mkdocs.yml index 49c686c16..52a51464a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,16 +139,6 @@ nav: - projects/centurion_erp/development/api/tests/model_permission_view_organization_manager.md - - projects/centurion_erp/development/api/tests/model_permissions_api.md - - - projects/centurion_erp/development/api/tests/model_permission_api_add.md - - - projects/centurion_erp/development/api/tests/model_permission_api_change.md - - - projects/centurion_erp/development/api/tests/model_permission_api_delete.md - - - projects/centurion_erp/development/api/tests/model_permission_api_view.md - - projects/centurion_erp/development/api/tests/model_tenancy_object.md - projects/centurion_erp/development/api/tests/model_views.md From c34dd9f2a427f3c652edd11bb502b205f7dba7ff Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 22:41:31 +0930 Subject: [PATCH 148/617] fix(access): ensure org id is an integer during permission checks ref: #348 --- app/access/mixin.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/access/mixin.py b/app/access/mixin.py index 7353ea40b..e0316927d 100644 --- a/app/access/mixin.py +++ b/app/access/mixin.py @@ -48,7 +48,7 @@ def object_organization(self) -> int: if hasattr(self, '_object_organization'): - return self._object_organization + return int(self._object_organization) try: @@ -124,9 +124,13 @@ def is_member(self, organization: int) -> bool: is_member = False - if organization in self.user_organizations(): + if organization is None: - return True + return False + + if int(organization) in self.user_organizations(): + + is_member = True return is_member @@ -220,6 +224,11 @@ def has_organization_permission(self, organization: int = None, permissions_requ organization = self.object_organization() + else: + + organization = int(organization) + + if self.is_member(organization) or organization == 0: groups = Group.objects.filter(pk__in=self.user_groups) From 1d198dd2df6912a8644da2d54eb20b97594aaccb Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 22:43:24 +0930 Subject: [PATCH 149/617] test(access): Organization API ViewSet permission checks ref: #15 #248 #348 --- .../test_organizaiton_permission_viewset.py | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 app/access/tests/unit/organization/test_organizaiton_permission_viewset.py diff --git a/app/access/tests/unit/organization/test_organizaiton_permission_viewset.py b/app/access/tests/unit/organization/test_organizaiton_permission_viewset.py new file mode 100644 index 000000000..4be799ffb --- /dev/null +++ b/app/access/tests/unit/organization/test_organizaiton_permission_viewset.py @@ -0,0 +1,231 @@ +import pytest +import unittest + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissionChange, APIPermissionView + + +class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionView): + + model = Organization + + model_name = 'organization' + app_label = 'access' + + app_namespace = 'API' + + url_name = '_api_v2_organization' + + change_data = {'name': 'device'} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a device + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = organization + + self.url_view_kwargs = {'pk': self.item.id} + + self.url_kwargs = {'pk': self.item.id} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.super_user = User.objects.create_user(username="super_user", password="password", is_superuser=True) + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + def test_add_is_prohibited_anon_user(self): + """ Ensure Organization cant be created + + Attempt to create organization as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 401 + + + def test_add_is_prohibited_diff_org_user(self): + """ Ensure Organization cant be created + + Attempt to create organization as user with different org permissions. + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.different_organization_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 403 + + + def test_add_not_prohibited_super_user(self): + """ Ensure Organization can be created + + Attempt to create organization as user who is super user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.super_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 201 + + + def test_add_is_prohibited_user_same_org(self): + """ Ensure Organization cant be created + + Attempt to create organization as user with permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data={'name': 'should not create'}, content_type='application/json') + + assert response.status_code == 403 From 200909fb820534d70805ad9f06bef13e1cf87e0d Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 22:44:17 +0930 Subject: [PATCH 150/617] test(access): Team API ViewSet permission checks ref: #15 #248 #348 --- .../unit/team/test_team_permission_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/access/tests/unit/team/test_team_permission_viewset.py diff --git a/app/access/tests/unit/team/test_team_permission_viewset.py b/app/access/tests/unit/team/test_team_permission_viewset.py new file mode 100644 index 000000000..2c8c5fba7 --- /dev/null +++ b/app/access/tests/unit/team/test_team_permission_viewset.py @@ -0,0 +1,173 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + + + +class TeamPermissionsAPI(TestCase, APIPermissions): + + model = Team + + app_namespace = 'API' + + url_name = '_api_v2_organization_team' + + change_data = {'name': 'device'} + + delete_data = {'device': 'device'} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + name = 'teamone' + ) + + + self.url_kwargs = {'organization_id': self.organization.id} + + self.url_view_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id} + + self.add_data = {'team_name': 'team_post'} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From af3a84f0dc2aa76b889437faee67511d88023a6a Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 23:10:20 +0930 Subject: [PATCH 151/617] test(access): Organization API v2 field checks ref: #15 #248 #348 --- .../organization/test_organizaiton_api_v2.py | 192 ++++++++++++++++++ app/access/tests/unit/test_access_viewset.py | 2 +- 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 app/access/tests/unit/organization/test_organizaiton_api_v2.py diff --git a/app/access/tests/unit/organization/test_organizaiton_api_v2.py b/app/access/tests/unit/organization/test_organizaiton_api_v2.py new file mode 100644 index 000000000..60076c46c --- /dev/null +++ b/app/access/tests/unit/organization/test_organizaiton_api_v2.py @@ -0,0 +1,192 @@ +import pytest +import unittest + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APICommonFields + + +class OrganizationAPI( + TestCase, + APICommonFields +): + + model = Organization + + app_namespace = 'API' + + url_name = '_api_v2_organization' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create the object + 2. create view user + 3. add user as org manager + 4. make api request + """ + + organization = Organization.objects.create(name='test_org', model_notes='random text') + + self.organization = organization + + + self.item = organization + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + organization.manager = self.view_user + + organization.save() + + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + + def test_api_field_exists_manager(self): + """ Test for existance of API Field + + manager field must exist + """ + + assert 'manager' in self.api_data + + + def test_api_field_type_manager(self): + """ Test for type for API Field + + manager field must be dict + """ + + assert type(self.api_data['manager']) is dict + + + def test_api_field_exists_manager_id(self): + """ Test for existance of API Field + + manager.id field must exist + """ + + assert 'id' in self.api_data['manager'] + + + def test_api_field_type_manager_id(self): + """ Test for type for API Field + + manager.id field must be int + """ + + assert type(self.api_data['manager']['id']) is int + + + def test_api_field_exists_manager_display_name(self): + """ Test for existance of API Field + + manager.display_name field must exist + """ + + assert 'display_name' in self.api_data['manager'] + + + def test_api_field_type_manager_display_name(self): + """ Test for type for API Field + + manager.display_name field must be int + """ + + assert type(self.api_data['manager']['display_name']) is str + + + def test_api_field_exists_manager_url(self): + """ Test for existance of API Field + + manager.display_name field must exist + """ + + assert 'url' in self.api_data['manager'] + + + def test_api_field_type_manager_url(self): + """ Test for type for API Field + + manager.url field must be Hyperlink + """ + + assert type(self.api_data['manager']['url']) is Hyperlink + + + + def test_api_field_exists_url_teams(self): + """ Test for existance of API Field + + _urls.teams field must exist + """ + + assert 'teams' in self.api_data['_urls'] + + + def test_api_field_type_url_teams(self): + """ Test for type for API Field + + _urls.teams field must be Hyperlink + """ + + assert type(self.api_data['_urls']['teams']) is str diff --git a/app/access/tests/unit/test_access_viewset.py b/app/access/tests/unit/test_access_viewset.py index 6fa10e1aa..eb2e2250d 100644 --- a/app/access/tests/unit/test_access_viewset.py +++ b/app/access/tests/unit/test_access_viewset.py @@ -6,7 +6,7 @@ from api.tests.abstract.viewsets import ViewSetCommon -from access.viewset.index import Index +from access.viewsets.index import Index class AccessViewset( From b6acba9930db5648f702d6f382781b10a09ba1ee Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 14 Oct 2024 23:54:50 +0930 Subject: [PATCH 152/617] test(api): API Response Field checks Abstract Class added ref: #15 #248 #348 --- app/api/tests/abstract/api_fields.py | 248 ++++++++++++++++++ .../centurion_erp/development/testing.md | 16 +- 2 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 app/api/tests/abstract/api_fields.py diff --git a/app/api/tests/abstract/api_fields.py b/app/api/tests/abstract/api_fields.py new file mode 100644 index 000000000..241dd205f --- /dev/null +++ b/app/api/tests/abstract/api_fields.py @@ -0,0 +1,248 @@ +from rest_framework.relations import Hyperlink + + + +class APICommonFields: + """Test Cases for fields common to All API responses + + Must contain: + - id + - display_name + - _urls + - _urls._self + """ + + + api_data: object + """ API Response data """ + + + + def test_api_field_exists_id(self): + """ Test for existance of API Field + + id field must exist + """ + + assert 'id' in self.api_data + + + def test_api_field_type_id(self): + """ Test for type for API Field + + id field must be int + """ + + assert type(self.api_data['id']) is int + + + def test_api_field_exists_display_name(self): + """ Test for existance of API Field + + display_name field must exist + """ + + assert 'display_name' in self.api_data + + + def test_api_field_type_display_name(self): + """ Test for type for API Field + + display_name field must be str + """ + + assert type(self.api_data['display_name']) is str + + + + def test_api_field_exists_urls(self): + """ Test for existance of API Field + + _urls field must exist + """ + + assert '_urls' in self.api_data + + + def test_api_field_type_urls(self): + """ Test for type for API Field + + _urls field must be str + """ + + assert type(self.api_data['_urls']) is dict + + + def test_api_field_exists_urls_self(self): + """ Test for existance of API Field + + _urls._self field must exist + """ + + assert '_self' in self.api_data['_urls'] + + + def test_api_field_type_urls(self): + """ Test for type for API Field + + _urls._self field must be str + """ + + assert type(self.api_data['_urls']['_self']) is str + + + +class APIModelFields( + APICommonFields +): + """Test Cases for fields common to All API Model responses + + Must contain: + - id + - display_name + - _urls + - _urls._self + """ + + + api_data: object + """ API Response data """ + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + model_notes field must exist + """ + + assert 'model_notes' in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + model_notes field must be str + """ + + assert type(self.api_data['model_notes']) is str + + + + def test_api_field_exists_created(self): + """ Test for existance of API Field + + created field must exist + """ + + assert 'created' in self.api_data + + + def test_api_field_type_created(self): + """ Test for type for API Field + + created field must be str + """ + + assert type(self.api_data['created']) is str + + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + modified field must exist + """ + + assert 'modified' in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + modified field must be str + """ + + assert type(self.api_data['modified']) is str + + + +class APITenancyObject( + APIModelFields +): + + + api_data: object + """ API Response data """ + + + + def test_api_field_exists_organization(self): + """ Test for existance of API Field + + organization field must exist + """ + + assert 'organization' in self.api_data + + + def test_api_field_type_organization(self): + """ Test for type for API Field + + organization field must be dict + """ + + assert type(self.api_data['organization']) is dict + + + + def test_api_field_exists_organization_id(self): + """ Test for existance of API Field + + organization.id field must exist + """ + + assert 'id' in self.api_data['organization'] + + + def test_api_field_type_organization_id(self): + """ Test for type for API Field + + organization.id field must be dict + """ + + assert type(self.api_data['organization']['id']) is int + + + def test_api_field_exists_organization_display_name(self): + """ Test for existance of API Field + + organization.display_name field must exist + """ + + assert 'display_name' in self.api_data['organization'] + + + def test_api_field_type_organization_display_name(self): + """ Test for type for API Field + + organization.display_name field must be str + """ + + assert type(self.api_data['organization']['display_name']) is str + + def test_api_field_exists_organization_url(self): + """ Test for existance of API Field + + organization.url field must exist + """ + + assert 'url' in self.api_data['organization'] + + + def test_api_field_type_organization_url(self): + """ Test for type for API Field + + organization.url field must be str + """ + + assert type(self.api_data['organization']['url']) is Hyperlink diff --git a/docs/projects/centurion_erp/development/testing.md b/docs/projects/centurion_erp/development/testing.md index 31188380d..46e946231 100644 --- a/docs/projects/centurion_erp/development/testing.md +++ b/docs/projects/centurion_erp/development/testing.md @@ -24,7 +24,7 @@ To aid in development we have written test classes that you can inherit from for - API Permission Checks - _These tests ensure that only a user with the correct permissions can perform an action against a Model within Centurion_ + _These test cases ensure that only a user with the correct permissions can perform an action against a Model within Centurion_ - `api.tests.abstract.api_permissions_viewset.APIPermissionAdd` _Add permission checks_ @@ -36,10 +36,20 @@ To aid in development we have written test classes that you can inherit from for - `api.tests.abstract.api_permissions_viewset.APIPermissions` _Add, Change, Delete and View permission checks_ +- API Field Checks + + _These test cases ensure that all of the specified fields are rendered as part of an API response_ + + - `api.tests.abstract.api_fields.APICommonFields` _Fields that should be part of ALL API responses_ + + - `api.tests.abstract.api_fields.APIModelFields` _Fields that should be part of ALL model API Responses. Includes `APICommonFields` test cases_ + + - `api.tests.abstract.api_fields.APITenancyObject` _Fields that should be part of ALL Tenancy Object model API Responses. Includes `APICommonFields` and `APIModelFields` test cases_ + ## Writing Tests -We use class based tests. Each class will require a `setUpTestData` method for test setup. To furhter assist in the writing of tests, we have written the test cases for common items as an abstract class. You are advised to inherit from our test classes _(see above)_ as a strating point and extend from there. +We use class based tests. Each class will require a `setUpTestData` method for test setup. To furhter assist in the writing of tests, we have written the test cases for common items as an abstract class. You are advised to inherit from our test classes _(see above)_ as a starting point and extend from there. Naming of test classes is in `CamelCase` in format `` for example the class name for device model history entry tests would be `DeviceHistory`. @@ -147,7 +157,7 @@ Items to test include, and are not limited to: - API Fields - _Field exists, Type is checked_ + _Field(s) exists, Type is checked_ ## Running Tests From fb7fda7ea23d74ab01111f43486d308b56d802da Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 00:10:41 +0930 Subject: [PATCH 153/617] test(access): Team API v2 field checks ref: #15 #248 #348 --- .../tests/unit/team/test_team_api_v2.py | 174 ++++++++++++++++++ app/api/tests/abstract/api_fields.py | 1 + 2 files changed, 175 insertions(+) create mode 100644 app/access/tests/unit/team/test_team_api_v2.py diff --git a/app/access/tests/unit/team/test_team_api_v2.py b/app/access/tests/unit/team/test_team_api_v2.py new file mode 100644 index 000000000..f7b2c0bd4 --- /dev/null +++ b/app/access/tests/unit/team/test_team_api_v2.py @@ -0,0 +1,174 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + + + +class TeamAPI( + TestCase, + APITenancyObject +): + + model = Team + + app_namespace = 'API' + + url_name = '_api_v2_organization_team' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create the object + 2. create view user + 3. add user as org manager + 4. make api request + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.item = self.model.objects.create( + organization=organization, + team_name = 'teamone', + model_notes = 'random note' + ) + + self.url_view_kwargs = {'organization_id': self.organization.id, 'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + self.item.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = self.item, + user = self.view_user + ) + + organization.manager = self.view_user + + organization.save() + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + def test_api_field_exists_team_name(self): + """ Test for existance of API Field + + team_name field must exist + """ + + assert 'team_name' in self.api_data + + + def test_api_field_type_team_name(self): + """ Test for type for API Field + + team_name field must be str + """ + + assert type(self.api_data['team_name']) is str + + + + def test_api_field_exists_permissions(self): + """ Test for existance of API Field + + permissions field must exist + """ + + assert 'permissions' in self.api_data + + + def test_api_field_type_permissions(self): + """ Test for type for API Field + + url field must be list + """ + + assert type(self.api_data['permissions']) is list + + + + def test_api_field_exists_permissions_id(self): + """ Test for existance of API Field + + permissions.id field must exist + """ + + assert 'id' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_id(self): + """ Test for type for API Field + + permissions.id field must be int + """ + + assert type(self.api_data['permissions'][0]['id']) is int + + + def test_api_field_exists_permissions_display_name(self): + """ Test for existance of API Field + + permissions.display_name field must exist + """ + + assert 'display_name' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_display_name(self): + """ Test for type for API Field + + permissions.display_name field must be str + """ + + assert type(self.api_data['permissions'][0]['display_name']) is str + + + + def test_api_field_exists_permissions_url(self): + """ Test for existance of API Field + + permissions.url field must exist + """ + + assert 'url' in self.api_data['permissions'][0] + + + def test_api_field_type_permissions_url(self): + """ Test for type for API Field + + permissions.url field must be str + """ + + assert type(self.api_data['permissions'][0]['url']) is Hyperlink diff --git a/app/api/tests/abstract/api_fields.py b/app/api/tests/abstract/api_fields.py index 241dd205f..d3642bd99 100644 --- a/app/api/tests/abstract/api_fields.py +++ b/app/api/tests/abstract/api_fields.py @@ -230,6 +230,7 @@ def test_api_field_type_organization_display_name(self): assert type(self.api_data['organization']['display_name']) is str + def test_api_field_exists_organization_url(self): """ Test for existance of API Field From 0faa6a5a18933fcc96c0a1cab17b69e97ab033d6 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 00:19:01 +0930 Subject: [PATCH 154/617] docs(api): Add serializer dev docs ref: #15 #248 #348 --- .../development/api/serializer/index.md | 137 +++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/docs/projects/centurion_erp/development/api/serializer/index.md b/docs/projects/centurion_erp/development/api/serializer/index.md index 12f429003..d6552de42 100644 --- a/docs/projects/centurion_erp/development/api/serializer/index.md +++ b/docs/projects/centurion_erp/development/api/serializer/index.md @@ -8,5 +8,140 @@ about: https://gitlab.com/nofusscomputing/infrastructure/configuration-managemen This section contains the application API documentation for Serializers to assist in application development. The target audience is anyone whom would be developing the application. - - [Inventory](./inventory.md) + + +## Requirements + +- All Serializers are Class based. + +- All models must be serialized. + +- Serializer files must contain the following defined serializers: + + - `BaseSerializer` + + - `ModelSerializer` inheriting `BaseSerializer` + + - `ViewSerializer` inheriting `ModelSerializer` + +- Serializers are defined within the `serializer` sub-directory within the app the model is defined. + +- Serializer file names are lower case and named the same as the model / related field. + +- fields that are required to have an initial value have it specified `self.fields.fields[].initial` + + +## Base Serializer + +This serializer is read-only and used as the serializer for related items within the model that has this model as related. Must contain the following fields: + +- `id` The models primary key + +- `display_name` value of model function `__str__()` + +- `name` Name/title of the model + +- `url` URL to the models page + + +## Model Serializer + +This serializer is write-only and is used for adding and updating a model. This serializer must include all fields the model has. Validation as required is to be done as part of this serializer. + + +## View Serializer + +This serializer is read-only and is used for the viewing of a model within list and detail views. This serializer must define the following fields: + +- `_urls` A dictionary of all child models urls + +- Each related field redefined as it's base serializer. i.e. `organization = OrganizationBaseSerializer(many=False, read_only=True)` + + +## Example Serializer + +Below is an truncated example serializer. + +``` py +from rest_framework.reverse import reverse +from rest_framework import serializers + +from itam.models.device import Device + + + +class DeviceBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device-detail", format="html" + ) + + + class Meta: + + model = Device + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class DeviceModelSerializer(DeviceBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'software': reverse("API:_api_v2_device_software-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + } + + + class Meta: + + model = Device + + fields = [ + 'id', + 'display_name', + '...', + 'created', + 'modified', + 'organization', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class DeviceViewSerializer(DeviceModelSerializer): + + device_model = DeviceModelBaseSerializer(many=False, read_only=True) + device_type = DeviceTypeBaseSerializer(many=False, read_only=True) + + organization = OrganizationBaseSerializer(many=False, read_only=True) + +``` From 0877fcf7e90499a72899f02e6cf25672faac6670 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 00:23:41 +0930 Subject: [PATCH 155/617] fix(api): Add missing organization url routes ref: #15 #248 #348 --- app/api/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/urls.py b/app/api/urls.py index 9f45051d0..f127e1e3a 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -42,6 +42,7 @@ from access.viewsets import ( index as access_v2, + organization as organization_v2, team as team_v2 ) @@ -109,6 +110,7 @@ router.register('v2', v2.Index, basename='_api_v2_home') router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') +router.register('v2/access/organization', organization_v2.ViewSet, basename='_api_v2_organization') router.register('v2/access/organization/(?P[0-9]+)/teams', team_v2.ViewSet, basename='_api_v2_organization_team') router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') From 636f6c5b58f91bfcefdc7b3217067120793c2f45 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 00:44:59 +0930 Subject: [PATCH 156/617] docs: remove links for files removed ref: #348 --- .../centurion_erp/development/api/tests/index.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/projects/centurion_erp/development/api/tests/index.md b/docs/projects/centurion_erp/development/api/tests/index.md index 485d8b837..75773fa97 100644 --- a/docs/projects/centurion_erp/development/api/tests/index.md +++ b/docs/projects/centurion_erp/development/api/tests/index.md @@ -30,16 +30,6 @@ Models are tested using the following test cases: - [View Permission (organization Manager)](./model_permission_view_organization_manager.md) -- [ALL API Model Permission](./model_permissions_api.md) - -- [API Add Permission](./model_permission_api_add.md) - -- [API Change Permission](./model_permission_api_change.md) - -- [API Delete Permission](./model_permission_api_delete.md) - -- [API View Permission](./model_permission_api_view.md) - - [History Entry](./model_history.md) - [History Entry (Child Item)](./model_history_child_item.md) From 9beb9a9d2cbb6d6e530612a3d7419dd73c02b4d8 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 02:58:13 +0930 Subject: [PATCH 157/617] feat(access): Add Team Users API endpoint ref: #248 #348 --- app/access/serializers/team_user.py | 118 ++++++++++++++++++++++++++++ app/access/serializers/teams.py | 8 ++ app/access/viewsets/team_user.py | 99 +++++++++++++++++++++++ app/api/urls.py | 6 +- 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 app/access/serializers/team_user.py create mode 100644 app/access/viewsets/team_user.py diff --git a/app/access/serializers/team_user.py b/app/access/serializers/team_user.py new file mode 100644 index 000000000..45c97f9e0 --- /dev/null +++ b/app/access/serializers/team_user.py @@ -0,0 +1,118 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers +from rest_framework.exceptions import ParseError + +from access.models import TeamUsers +from app.serializers.user import UserBaseSerializer + + + +class TeamUserBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_organization_team_user-detail", + request=self.context['view'].request, + kwargs={ + 'organization_id': item.team.organization.id, + 'team_id': item.team.id, + 'pk': item.pk + } + ) + + + class Meta: + + model = TeamUsers + + fields = [ + 'id', + 'display_name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url', + ] + + + +class TeamUserModelSerializer(TeamUserBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + 'API:_api_v2_organization_team_user-detail', + request=self.context['view'].request, + kwargs={ + 'organization_id': item.team.organization.id, + 'team_id': item.team.id, + 'pk': item.pk + } + ) + } + + + class Meta: + + model = TeamUsers + + fields = [ + 'id', + 'display_name', + 'manager', + 'user', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=True) -> bool: + + is_valid = False + + try: + + is_valid = super().is_valid(raise_exception=raise_exception) + + self.validated_data['team_id'] = int(self._context['view'].kwargs['team_id']) + + except Exception as unhandled_exception: + + ParseError( + detail=f"Server encountered an error during validation, Traceback: {unhandled_exception.with_traceback}" + ) + + return is_valid + + + +class TeamUserViewSerializer(TeamUserModelSerializer): + + user = UserBaseSerializer(read_only = True) diff --git a/app/access/serializers/teams.py b/app/access/serializers/teams.py index 2fdaf2ed9..7bda46b18 100644 --- a/app/access/serializers/teams.py +++ b/app/access/serializers/teams.py @@ -67,6 +67,14 @@ def get_url(self, item): 'organization_id': item.organization.id, 'pk': item.pk } + ), + 'users': reverse( + 'API:_api_v2_organization_team_user-list', + request=self.context['view'].request, + kwargs={ + 'organization_id': item.organization.id, + 'team_id': item.pk + } ) } diff --git a/app/access/viewsets/team_user.py b/app/access/viewsets/team_user.py new file mode 100644 index 000000000..e6c464d38 --- /dev/null +++ b/app/access/viewsets/team_user.py @@ -0,0 +1,99 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from access.serializers.team_user import ( + TeamUsers, + TeamUserModelSerializer, + TeamUserViewSerializer +) + +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a user within this team', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=TeamUserViewSerializer), + 201: OpenApiResponse(description='Created', response=TeamUserViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a user from this team', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all users from this team', + description='', + responses = { + 200: OpenApiResponse(description='', response=TeamUserViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single user from this team', + description='', + responses = { + 200: OpenApiResponse(description='', response=TeamUserViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a user within this team', + description = '', + responses = { + 200: OpenApiResponse(description='', response=TeamUserViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'manager', + 'team__organization', + ] + + search_fields = [] + + model = TeamUsers + + documentation: str = '' + + view_description = 'Users belonging to a single team' + + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter( + team_id = self.kwargs['team_id'] + ) + + self.queryset = queryset + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + diff --git a/app/api/urls.py b/app/api/urls.py index f127e1e3a..f2644b736 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -43,7 +43,8 @@ from access.viewsets import ( index as access_v2, organization as organization_v2, - team as team_v2 + team as team_v2, + team_user as team_user_v2 ) from assistance.viewset import ( @@ -111,7 +112,8 @@ router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') router.register('v2/access/organization', organization_v2.ViewSet, basename='_api_v2_organization') -router.register('v2/access/organization/(?P[0-9]+)/teams', team_v2.ViewSet, basename='_api_v2_organization_team') +router.register('v2/access/organization/(?P[0-9]+)/team', team_v2.ViewSet, basename='_api_v2_organization_team') +router.register('v2/access/organization/(?P[0-9]+)/team/(?P[0-9]+)/user', team_user_v2.ViewSet, basename='_api_v2_organization_team_user') router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') From 80d307b2a5a44381e30fdb327cd6d92b2bf125f9 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 02:59:42 +0930 Subject: [PATCH 158/617] test(access): Team User API ViewSet permission checks ref: #15 #248 #348 --- .../test_team_user_permission_viewset.py | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 app/access/tests/unit/team_user/test_team_user_permission_viewset.py diff --git a/app/access/tests/unit/team_user/test_team_user_permission_viewset.py b/app/access/tests/unit/team_user/test_team_user_permission_viewset.py new file mode 100644 index 000000000..941a42b47 --- /dev/null +++ b/app/access/tests/unit/team_user/test_team_user_permission_viewset.py @@ -0,0 +1,170 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + + + +class TeamUserPermissionsAPI(TestCase, APIPermissions): + + model = TeamUsers + + app_namespace = 'API' + + url_name = '_api_v2_organization_team_user' + + change_data = {'name': 'device'} + + delete_data = {'device': 'device'} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + self.item = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.url_view_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id, 'pk': self.item.id} + + self.url_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id} + + + random_user = User.objects.create_user(username="random_user", password="password") + self.add_data = {'user': random_user.id} + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 4b873a4e442c40691aab0eec7dcb93fc11a8131d Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 03:11:58 +0930 Subject: [PATCH 159/617] test(access): Team Users API v2 field checks ref: #15 #248 #348 --- .../unit/team_user/test_team_user_api_v2.py | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 app/access/tests/unit/team_user/test_team_user_api_v2.py diff --git a/app/access/tests/unit/team_user/test_team_user_api_v2.py b/app/access/tests/unit/team_user/test_team_user_api_v2.py new file mode 100644 index 000000000..2b3cca096 --- /dev/null +++ b/app/access/tests/unit/team_user/test_team_user_api_v2.py @@ -0,0 +1,214 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APICommonFields + + + +class TeamUserAPI( + TestCase, + APICommonFields +): + + model = TeamUsers + + app_namespace = 'API' + + url_name = '_api_v2_organization_team_user' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create the object + 2. create view user + 3. add user as org manager + 4. make api request + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + self.item = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.url_view_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id, 'pk': self.item.id} + + self.url_kwargs = {'organization_id': self.organization.id, 'team_id': view_team.id} + + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + def test_api_field_exists_manager(self): + """ Test for existance of API Field + + manager field must exist + """ + + assert 'manager' in self.api_data + + + def test_api_field_type_manager(self): + """ Test for type for API Field + + manager field must be bool + """ + + assert type(self.api_data['manager']) is bool + + + + def test_api_field_exists_created(self): + """ Test for existance of API Field + + created field must exist + """ + + assert 'created' in self.api_data + + + def test_api_field_type_created(self): + """ Test for type for API Field + + created field must be str + """ + + assert type(self.api_data['created']) is str + + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + modified field must exist + """ + + assert 'modified' in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + modified field must be str + """ + + assert type(self.api_data['modified']) is str + + + + + # def test_api_field_exists_permissions(self): + # """ Test for existance of API Field + + # permissions field must exist + # """ + + # assert 'permissions' in self.api_data + + + # def test_api_field_type_permissions(self): + # """ Test for type for API Field + + # url field must be list + # """ + + # assert type(self.api_data['permissions']) is list + + + + # def test_api_field_exists_permissions_id(self): + # """ Test for existance of API Field + + # permissions.id field must exist + # """ + + # assert 'id' in self.api_data['permissions'][0] + + + # def test_api_field_type_permissions_id(self): + # """ Test for type for API Field + + # permissions.id field must be int + # """ + + # assert type(self.api_data['permissions'][0]['id']) is int + + + # def test_api_field_exists_permissions_display_name(self): + # """ Test for existance of API Field + + # permissions.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['permissions'][0] + + + # def test_api_field_type_permissions_display_name(self): + # """ Test for type for API Field + + # permissions.display_name field must be str + # """ + + # assert type(self.api_data['permissions'][0]['display_name']) is str + + + + # def test_api_field_exists_permissions_url(self): + # """ Test for existance of API Field + + # permissions.url field must exist + # """ + + # assert 'url' in self.api_data['permissions'][0] + + + # def test_api_field_type_permissions_url(self): + # """ Test for type for API Field + + # permissions.url field must be str + # """ + + # assert type(self.api_data['permissions'][0]['url']) is Hyperlink From 59e34cae4d10f333514ed552eec644ba8cdab601 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 03:18:12 +0930 Subject: [PATCH 160/617] test(access): Add missing test cases to Team Users Model ref: #15 #248 #348 --- app/access/tests/unit/team_user/test_team_user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/access/tests/unit/team_user/test_team_user.py b/app/access/tests/unit/team_user/test_team_user.py index b183eac66..8c30764cf 100644 --- a/app/access/tests/unit/team_user/test_team_user.py +++ b/app/access/tests/unit/team_user/test_team_user.py @@ -6,9 +6,13 @@ from access.models import Organization, Team, TeamUsers, Permission +from app.tests.abstract.models import BaseModel -class TeamUsersModel(TestCase): +class TeamUsersModel( + TestCase, + BaseModel +): model = TeamUsers From 84de741f53312e66183c5c25f4b43ace56f44531 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 03:20:58 +0930 Subject: [PATCH 161/617] fix(access): Add missing parameters to Team User fields ref: #15 #248 #348 --- app/access/models.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/app/access/models.py b/app/access/models.py index b6700109d..871e84e4d 100644 --- a/app/access/models.py +++ b/app/access/models.py @@ -370,36 +370,52 @@ def __str__(self): class TeamUsers(SaveHistory): class Meta: - # proxy = True - verbose_name_plural = "Team Users" + ordering = ['user'] + verbose_name = "Team User" + + verbose_name_plural = "Team Users" + + id = models.AutoField( + blank=False, + help_text = 'ID of this Team User', primary_key=True, unique=True, - blank=False + verbose_name = 'ID' ) team = models.ForeignKey( Team, + help_text = 'Team user belongs to', + on_delete=models.CASCADE, related_name="team", - on_delete=models.CASCADE) + verbose_name = 'Team' + ) user = models.ForeignKey( settings.AUTH_USER_MODEL, - on_delete=models.CASCADE + help_text = 'User who will be added to the team', + on_delete=models.CASCADE, + verbose_name = 'User' ) manager = models.BooleanField( - verbose_name='manager', + blank=True, default=False, - blank=True + help_text = 'Is this user to be a manager of this team', + verbose_name='manager', ) created = AutoCreatedField() modified = AutoLastModifiedField() + page_layout: list = [] + + table_fields: list = [] + def delete(self, using=None, keep_parents=False): """ Delete Team From 835e5258a5b33b78d5ebe0c6821c90f32e817c68 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 16:09:22 +0930 Subject: [PATCH 162/617] test(base): Content Type API ViewSet permission checks ref: #15 #248 #348 --- .../test_content_type_permission_viewset.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/app/tests/unit/content_type/test_content_type_permission_viewset.py diff --git a/app/app/tests/unit/content_type/test_content_type_permission_viewset.py b/app/app/tests/unit/content_type/test_content_type_permission_viewset.py new file mode 100644 index 000000000..250a24be0 --- /dev/null +++ b/app/app/tests/unit/content_type/test_content_type_permission_viewset.py @@ -0,0 +1,61 @@ +import pytest +import unittest +import requests + +from django.contrib.auth.models import User, ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + + + +class ContentTypePermissionsAPI(TestCase): + + model = ContentType + + app_namespace = 'API' + + url_name = '_api_v2_content_type' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. create a user + """ + + self.url_kwargs = {} + + self.url_view_kwargs = {'pk': 1} + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + + def test_view_user_anon_denied(self): + """ Check correct permission for view + + Attempt to view as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + response = client.get(url) + + assert response.status_code == 401 + + + def test_view_authenticated_user(self): + """ Check correct permission for view + + Attempt to view as user who is authenticated + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + assert response.status_code == 200 From 2b24f298372c29d68ad49770256ce0e7d03671a1 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 16:09:41 +0930 Subject: [PATCH 163/617] test(base): Permission API ViewSet permission checks ref: #15 #248 #348 --- .../test_permissions_permission_viewset.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/app/tests/unit/permission/test_permissions_permission_viewset.py diff --git a/app/app/tests/unit/permission/test_permissions_permission_viewset.py b/app/app/tests/unit/permission/test_permissions_permission_viewset.py new file mode 100644 index 000000000..6fbbe9907 --- /dev/null +++ b/app/app/tests/unit/permission/test_permissions_permission_viewset.py @@ -0,0 +1,61 @@ +import pytest +import unittest +import requests + +from django.contrib.auth.models import User, Permission +from django.shortcuts import reverse +from django.test import Client, TestCase + + + +class PermissionPermissionsAPI(TestCase): + + model = Permission + + app_namespace = 'API' + + url_name = '_api_v2_permission' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. create a user + """ + + self.url_kwargs = {} + + self.url_view_kwargs = {'pk': 1} + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + + def test_view_user_anon_denied(self): + """ Check correct permission for view + + Attempt to view as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + response = client.get(url) + + assert response.status_code == 401 + + + def test_view_authenticated_user(self): + """ Check correct permission for view + + Attempt to view as user who is authenticated + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + assert response.status_code == 200 From 6f7638d9cf682707dbefa0bd4d31f240e8f0261b Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 16:09:58 +0930 Subject: [PATCH 164/617] test(base): User API ViewSet permission checks ref: #15 #248 #348 --- .../unit/user/test_user_permission_viewset.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 app/app/tests/unit/user/test_user_permission_viewset.py diff --git a/app/app/tests/unit/user/test_user_permission_viewset.py b/app/app/tests/unit/user/test_user_permission_viewset.py new file mode 100644 index 000000000..27a94a3e4 --- /dev/null +++ b/app/app/tests/unit/user/test_user_permission_viewset.py @@ -0,0 +1,61 @@ +import pytest +import unittest +import requests + +from django.contrib.auth.models import User +from django.shortcuts import reverse +from django.test import Client, TestCase + + + +class UserPermissionsAPI(TestCase): + + model = User + + app_namespace = 'API' + + url_name = '_api_v2_user' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. create a user + """ + + self.url_kwargs = {} + + self.url_view_kwargs = {'pk': 1} + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + + def test_view_user_anon_denied(self): + """ Check correct permission for view + + Attempt to view as anon user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + response = client.get(url) + + assert response.status_code == 401 + + + def test_view_authenticated_user(self): + """ Check correct permission for view + + Attempt to view as user who is authenticated + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + assert response.status_code == 200 From a77e01f86fe119c523dc903b9c975f8f6ad055bf Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 16:22:10 +0930 Subject: [PATCH 165/617] feat(api): Depreciate API v1 permission endpoint ref: #15 #248 #348 --- app/api/views/settings/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/settings/permissions.py b/app/api/views/settings/permissions.py index fca58e673..86b5f3ed0 100644 --- a/app/api/views/settings/permissions.py +++ b/app/api/views/settings/permissions.py @@ -9,7 +9,7 @@ from core.http.common import Http - +@extend_schema(deprecated=True) class View(views.APIView): permission_classes = [ From 3cb9aa6f59ea75f6909521af466a8b9c5247fd99 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 16:45:04 +0930 Subject: [PATCH 166/617] refactor(assistance): Correct viewset dir name to viwsets ref: #248 #352 --- app/api/urls.py | 2 +- app/assistance/{viewset => viewsets}/index.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/assistance/{viewset => viewsets}/index.py (100%) diff --git a/app/api/urls.py b/app/api/urls.py index f2644b736..a4281b78d 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -47,7 +47,7 @@ team_user as team_user_v2 ) -from assistance.viewset import ( +from assistance.viewsets import ( index as assistance_index_v2 ) diff --git a/app/assistance/viewset/index.py b/app/assistance/viewsets/index.py similarity index 100% rename from app/assistance/viewset/index.py rename to app/assistance/viewsets/index.py From 7e927603409cb7427ae673792e1038220a049f93 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 18:13:15 +0930 Subject: [PATCH 167/617] fix(api): on permission check error, return authorized=false ref: #248 #352 --- app/api/views/mixin.py | 146 +++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index 8c56db026..746fb48a1 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -27,111 +27,111 @@ def permission_check(self, request, view, obj=None) -> bool: return False - self.request = request + try: - method = self.request._request.method.lower() + self.request = request - if method.upper() not in view.allowed_methods: + method = self.request._request.method.lower() - view.http_method_not_allowed(request._request) + if method.upper() not in view.allowed_methods: - if request.user.is_authenticated and method == 'options': + view.http_method_not_allowed(request._request) - return True + if request.user.is_authenticated and method == 'options': - if hasattr(view, 'get_queryset'): + return True - queryset = view.get_queryset() + if hasattr(view, 'get_queryset'): - self.obj = queryset.model + queryset = view.get_queryset() - elif hasattr(view, 'queryset'): + self.obj = queryset.model - if view.queryset.model._meta: + elif hasattr(view, 'queryset'): - self.obj = view.queryset.model + if view.queryset.model._meta: - object_organization = None + self.obj = view.queryset.model - if method == 'get': + object_organization = None - action = 'view' - - elif method == 'post': + if method == 'get': - action = 'add' + action = 'view' + + elif method == 'post': - if 'organization' in request.data: + action = 'add' - if not request.data['organization']: - raise ValidationError('you must provide an organization') + if 'organization' in request.data: - object_organization = int(request.data['organization']) - elif method == 'patch': + if not request.data['organization']: + raise ValidationError('you must provide an organization') - action = 'change' + object_organization = int(request.data['organization']) + elif method == 'patch': - elif method == 'put': + action = 'change' - action = 'change' + elif method == 'put': - elif method == 'delete': + action = 'change' - action = 'delete' + elif method == 'delete': - else: + action = 'delete' - action = 'view' + else: - if hasattr(self, 'obj'): + action = 'view' - permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name + if hasattr(self, 'obj'): - self.permission_required = [ permission ] + permission = self.obj._meta.app_label + '.' + action + '_' + self.obj._meta.model_name - if hasattr(view, 'get_dynamic_permissions'): + self.permission_required = [ permission ] - self.permission_required = view.get_dynamic_permissions() + if hasattr(view, 'get_dynamic_permissions'): + self.permission_required = view.get_dynamic_permissions() + if view: + if 'organization_id' in view.kwargs: - if view: - if 'organization_id' in view.kwargs: + if view.kwargs['organization_id']: - if view.kwargs['organization_id']: + object_organization = view.kwargs['organization_id'] - object_organization = view.kwargs['organization_id'] + if object_organization is None and 'pk' in view.kwargs: - if object_organization is None and 'pk' in view.kwargs: - - try: + try: - self.obj = view.queryset.get(pk=view.kwargs['pk']) # Here + self.obj = view.queryset.get(pk=view.kwargs['pk']) # Here - except ObjectDoesNotExist: + except ObjectDoesNotExist: - return False + return False - if obj: + if obj: - if obj.get_organization(): + if obj.get_organization(): - object_organization = obj.get_organization().id + object_organization = obj.get_organization().id - if hasattr(self.obj, 'is_global'): - - if obj.is_global: + if hasattr(self.obj, 'is_global'): + + if obj.is_global: - object_organization = 0 + object_organization = 0 - if 'pk' in view.kwargs: + if 'pk' in view.kwargs: - if object_organization is None and view.queryset.model._meta.model_name == 'organization' and view.kwargs['pk']: + if object_organization is None and view.queryset.model._meta.model_name == 'organization' and view.kwargs['pk']: - object_organization = view.kwargs['pk'] + object_organization = view.kwargs['pk'] if object_organization is None: @@ -144,35 +144,39 @@ def permission_check(self, request, view, obj=None) -> bool: return False - if hasattr(self, 'obj') and object_organization is None and 'pk' in view.kwargs: + if hasattr(self, 'obj') and object_organization is None and 'pk' in view.kwargs: - if self.obj.get_organization(): + if self.obj.get_organization(): - object_organization = self.obj.get_organization().id + object_organization = self.obj.get_organization().id - if hasattr(self.obj, 'is_global'): + if hasattr(self.obj, 'is_global'): - if self.obj.is_global: + if self.obj.is_global: - object_organization = 0 + object_organization = 0 - # ToDo: implement proper checking of listview as this if allows ALL. - if 'pk' not in view.kwargs and method == 'get' and object_organization is None: + # ToDo: implement proper checking of listview as this if allows ALL. + if 'pk' not in view.kwargs and method == 'get' and object_organization is None: - return True + return True - if hasattr(self, 'default_organization'): - object_organization = self.default_organization + if hasattr(self, 'default_organization'): + object_organization = self.default_organization - if method == 'post' and hasattr(self, 'default_organization'): + if method == 'post' and hasattr(self, 'default_organization'): - if self.default_organization: + if self.default_organization: - object_organization = self.default_organization.id + object_organization = self.default_organization.id - if not self.has_organization_permission(object_organization) and not request.user.is_superuser: + if not self.has_organization_permission(object_organization) and not request.user.is_superuser: - raise PermissionDenied('You are not part of this organization') + raise PermissionDenied('You are not part of this organization') + + except Exception as e: + + return False return True From 61450b442d95b74042e6e6711134527271b56cf0 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 19:36:21 +0930 Subject: [PATCH 168/617] feat(assistance): Add Knowledge Base API v2 endpoint ref: #248 #348 --- app/api/urls.py | 4 +- app/assistance/models/knowledge_base.py | 14 +- app/assistance/serializers/knowledge_base.py | 158 +++++++++++++++++++ app/assistance/viewsets/index.py | 2 +- app/assistance/viewsets/knowledge_base.py | 96 +++++++++++ 5 files changed, 260 insertions(+), 14 deletions(-) create mode 100644 app/assistance/serializers/knowledge_base.py create mode 100644 app/assistance/viewsets/knowledge_base.py diff --git a/app/api/urls.py b/app/api/urls.py index a4281b78d..98a601820 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -48,7 +48,8 @@ ) from assistance.viewsets import ( - index as assistance_index_v2 + index as assistance_index_v2, + knowledge_base as knowledge_base_v2 ) from config_management.viewset import ( @@ -116,6 +117,7 @@ router.register('v2/access/organization/(?P[0-9]+)/team/(?P[0-9]+)/user', team_user_v2.ViewSet, basename='_api_v2_organization_team_user') router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') +router.register('v2/assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base') router.register('v2/base', base_index_v2.Index, basename='_api_v2_base_home') router.register('v2/base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index 197364d66..ef5f8cfd0 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -127,9 +127,9 @@ class Meta: 'title', ] - verbose_name = "Article" + verbose_name = "Knowledge Base" - verbose_name_plural = "Articles" + verbose_name_plural = "Knowledge Base Articles" model_notes = None @@ -297,16 +297,6 @@ class Meta: } ] }, - { - "name": "Articles", - "slug": "article", - "sections": [ - { - "layout": "table", - "field": "articles", - } - ] - }, { "name": "Notes", "slug": "notes", diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py new file mode 100644 index 000000000..e10e74d8c --- /dev/null +++ b/app/assistance/serializers/knowledge_base.py @@ -0,0 +1,158 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from access.serializers.organization import OrganizationBaseSerializer +from access.serializers.teams import TeamBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from assistance.models.knowledge_base import KnowledgeBase + + + +class KnowledgeBaseBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_knowledge_base-detail", + request=self.context['view'].request, + kwargs={ + 'pk': item.pk + } + ) + + + class Meta: + + model = KnowledgeBase + + fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + + +class KnowledgeBaseModelSerializer(KnowledgeBaseBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + 'API:_api_v2_knowledge_base-detail', + request=self.context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + 'organization': reverse( + 'API:_api_v2_organization-list', + request=self.context['view'].request, + ), + 'team': reverse( + 'API:_api_v2_organization_team-list', + request=self.context['view'].request, + kwargs={ + 'organization_id': item.organization.id, + } + ), + 'user': reverse( + 'API:_api_v2_user-list', + request=self.context['view'].request, + ) + } + + + class Meta: + + model = KnowledgeBase + + fields = [ + 'id', + 'organization', + 'category', + 'display_name', + 'title', + 'summary', + 'content', + 'release_date', + 'expiry_date', + 'target_user', + 'target_team', + 'responsible_user', + 'responsible_teams', + 'public', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=True) -> bool: + + is_valid = False + + is_valid = super().is_valid(raise_exception=raise_exception) + + + if self.validated_data['target_team'] and self.validated_data['target_user']: + + is_valid = False + + raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other') + + + if not self.validated_data['target_team'] and not self.validated_data['target_user']: + + raise ValidationError('A Target Team or Target User must be assigned.') + + + return is_valid + + + +class KnowledgeBaseViewSerializer(KnowledgeBaseModelSerializer): + + organization = OrganizationBaseSerializer( many=False, read_only=True ) + + responsible_teams = TeamBaseSerializer( read_only = True, many = True) + + responsible_user = UserBaseSerializer( read_only = True ) + + target_team = TeamBaseSerializer( read_only = True, many = True) + + target_user = UserBaseSerializer( read_only = True ) diff --git a/app/assistance/viewsets/index.py b/app/assistance/viewsets/index.py index 1195a6f7b..264dd650b 100644 --- a/app/assistance/viewsets/index.py +++ b/app/assistance/viewsets/index.py @@ -25,7 +25,7 @@ def list(self, request, pk=None): return Response( { - "knowledge_base": "ToDo", + "knowledge_base": reverse('API:_api_v2_knowledge_base-list', request=request), "request": "ToDo" } ) diff --git a/app/assistance/viewsets/knowledge_base.py b/app/assistance/viewsets/knowledge_base.py new file mode 100644 index 000000000..b022c8411 --- /dev/null +++ b/app/assistance/viewsets/knowledge_base.py @@ -0,0 +1,96 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from assistance.serializers.knowledge_base import ( + KnowledgeBase, + KnowledgeBaseModelSerializer, + KnowledgeBaseViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a knowledge base article', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=KnowledgeBaseViewSerializer), + 201: OpenApiResponse(description='Created', response=KnowledgeBaseViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a knowledge base article', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all knowledge base articles', + description='', + responses = { + 200: OpenApiResponse(description='', response=KnowledgeBaseViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single knowledge base article', + description='', + responses = { + 200: OpenApiResponse(description='', response=KnowledgeBaseViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a knowledge base article', + description = '', + responses = { + 200: OpenApiResponse(description='', response=KnowledgeBaseViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'category', + 'target_user', + 'target_team', + 'responsible_user', + 'responsible_teams', + 'public', + ] + + search_fields = [ + 'title', + 'summary', + 'content', + ] + + model = KnowledgeBase + + documentation: str = '' + + view_description = 'Information Management Knowledge Base Article(s)' + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + From b8cafeb99b9dada9566b52dfb2cd0f1d0930afc8 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 19:46:33 +0930 Subject: [PATCH 169/617] feat(assistance): Add Knowledge Base Category API v2 endpoint ref: #248 #348 --- app/api/urls.py | 4 +- app/assistance/models/knowledge_base.py | 27 ++-- app/assistance/serializers/knowledge_base.py | 8 +- .../serializers/knowledge_base_category.py | 146 ++++++++++++++++++ .../tests/unit/test_assistance_viewset.py | 2 +- .../viewsets/knowledge_base_category.py | 93 +++++++++++ app/settings/viewsets/index.py | 10 ++ 7 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 app/assistance/serializers/knowledge_base_category.py create mode 100644 app/assistance/viewsets/knowledge_base_category.py diff --git a/app/api/urls.py b/app/api/urls.py index 98a601820..3cc5989e7 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -49,7 +49,8 @@ from assistance.viewsets import ( index as assistance_index_v2, - knowledge_base as knowledge_base_v2 + knowledge_base as knowledge_base_v2, + knowledge_base_category as knowledge_base_category_v2 ) from config_management.viewset import ( @@ -133,6 +134,7 @@ router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') +router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') urlpatterns = [ diff --git a/app/assistance/models/knowledge_base.py b/app/assistance/models/knowledge_base.py index ef5f8cfd0..706e394bb 100644 --- a/app/assistance/models/knowledge_base.py +++ b/app/assistance/models/knowledge_base.py @@ -16,9 +16,9 @@ class Meta: 'name', ] - verbose_name = "Category" + verbose_name = "Knowledge Base Category" - verbose_name_plural = "Categorys" + verbose_name_plural = "Knowledge Base Categories" parent_category = models.ForeignKey( @@ -74,30 +74,20 @@ class Meta: { "layout": "double", "left": [ - 'title', + 'organization', 'parent_category', + 'name', 'target_user', 'target_team', - 'created', - 'modified', ], "right": [ 'model_notes', - 'organization', + 'created', + 'modified', ] } ] }, - { - "name": "Articles", - "slug": "article", - "sections": [ - { - "layout": "table", - "field": "articles", - } - ] - }, { "name": "Notes", "slug": "notes", @@ -106,8 +96,9 @@ class Meta: ] table_fields: list = [ - 'title', - 'parent', + 'name', + 'parent_category', + 'is_global', 'organization', ] diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py index e10e74d8c..00c2c3806 100644 --- a/app/assistance/serializers/knowledge_base.py +++ b/app/assistance/serializers/knowledge_base.py @@ -9,7 +9,7 @@ from app.serializers.user import UserBaseSerializer from assistance.models.knowledge_base import KnowledgeBase - +from assistance.serializers.knowledge_base_category import KnowledgeBaseCategoryBaseSerializer class KnowledgeBaseBaseSerializer(serializers.ModelSerializer): @@ -69,6 +69,10 @@ def get_url(self, item): 'pk': item.pk } ), + 'category': reverse( + 'API:_api_v2_knowledge_base_category-list', + request=self.context['view'].request, + ), 'organization': reverse( 'API:_api_v2_organization-list', request=self.context['view'].request, @@ -147,6 +151,8 @@ def is_valid(self, *, raise_exception=True) -> bool: class KnowledgeBaseViewSerializer(KnowledgeBaseModelSerializer): + category = KnowledgeBaseCategoryBaseSerializer( read_only = True ) + organization = OrganizationBaseSerializer( many=False, read_only=True ) responsible_teams = TeamBaseSerializer( read_only = True, many = True) diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py new file mode 100644 index 000000000..339193b61 --- /dev/null +++ b/app/assistance/serializers/knowledge_base_category.py @@ -0,0 +1,146 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers +from rest_framework.exceptions import ParseError, ValidationError + + +from access.serializers.organization import OrganizationBaseSerializer +from access.serializers.teams import TeamBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from assistance.models.knowledge_base import KnowledgeBaseCategory + + + +class KnowledgeBaseCategoryBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_knowledge_base_category-detail", + request=self.context['view'].request, + kwargs={ + 'pk': item.pk + } + ) + + + class Meta: + + model = KnowledgeBaseCategory + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class KnowledgeBaseCategoryModelSerializer(KnowledgeBaseCategoryBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + 'API:_api_v2_knowledge_base_category-detail', + request=self.context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + 'organization': reverse( + 'API:_api_v2_organization-list', + request=self.context['view'].request, + ), + 'team': reverse( + 'API:_api_v2_organization_team-list', + request=self.context['view'].request, + kwargs={ + 'organization_id': item.organization.id, + } + ), + 'user': reverse( + 'API:_api_v2_user-list', + request=self.context['view'].request, + ) + } + + + class Meta: + + model = KnowledgeBaseCategory + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'name', + 'parent_category', + 'target_user', + 'target_team', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=True) -> bool: + + is_valid = False + + is_valid = super().is_valid(raise_exception=raise_exception) + + + if self.validated_data['target_team'] and self.validated_data['target_user']: + + is_valid = False + + raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other or None') + + + return is_valid + + + +class KnowledgeBaseCategoryViewSerializer(KnowledgeBaseCategoryModelSerializer): + + organization = OrganizationBaseSerializer( many=False, read_only=True )\ + + parent_category = KnowledgeBaseCategoryBaseSerializer( many = False, read_only = True) + + target_team = TeamBaseSerializer( read_only = True, many = True) + + target_user = UserBaseSerializer( read_only = True ) diff --git a/app/assistance/tests/unit/test_assistance_viewset.py b/app/assistance/tests/unit/test_assistance_viewset.py index 061c01c2b..6b3b441d5 100644 --- a/app/assistance/tests/unit/test_assistance_viewset.py +++ b/app/assistance/tests/unit/test_assistance_viewset.py @@ -6,7 +6,7 @@ from api.tests.abstract.viewsets import ViewSetCommon -from assistance.viewset.index import Index +from assistance.viewsets.index import Index class AssistanceViewset( diff --git a/app/assistance/viewsets/knowledge_base_category.py b/app/assistance/viewsets/knowledge_base_category.py new file mode 100644 index 000000000..ccf8fd560 --- /dev/null +++ b/app/assistance/viewsets/knowledge_base_category.py @@ -0,0 +1,93 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from assistance.serializers.knowledge_base_category import ( + KnowledgeBaseCategory, + KnowledgeBaseCategoryModelSerializer, + KnowledgeBaseCategoryViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a knowledge base article', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=KnowledgeBaseCategoryViewSerializer), + 201: OpenApiResponse(description='Created', response=KnowledgeBaseCategoryViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a knowledge base article', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all knowledge base articles', + description='', + responses = { + 200: OpenApiResponse(description='', response=KnowledgeBaseCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single knowledge base article', + description='', + responses = { + 200: OpenApiResponse(description='', response=KnowledgeBaseCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a knowledge base article', + description = '', + responses = { + 200: OpenApiResponse(description='', response=KnowledgeBaseCategoryViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'name', + 'organization', + 'parent_category', + 'target_user', + 'target_team', + 'is_global', + ] + + search_fields = [ + 'name', + ] + + model = KnowledgeBaseCategory + + documentation: str = '' + + view_description = 'Settings, Knowledge Base Categories' + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 0801ed34c..d59841adc 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -17,6 +17,15 @@ class Index(CommonViewSet): ] page_layout: list = [ + { + "name": "Assistanace", + "links": [ + { + "name": "Knowledge Base Categories", + "model": "knowledge_base_category" + } + ] + }, { "name": "Core", "links": [ @@ -46,5 +55,6 @@ def list(self, request, pk=None): return Response( { + "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), } ) From 94f39251272596eb378a4b1bf0cb9c62590e1c9d Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 20:15:14 +0930 Subject: [PATCH 170/617] fix(assistance): Correct Knowledge Base serialaizer Validation ref: #248 #348 --- app/assistance/serializers/knowledge_base.py | 29 ++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py index 00c2c3806..705589580 100644 --- a/app/assistance/serializers/knowledge_base.py +++ b/app/assistance/serializers/knowledge_base.py @@ -132,15 +132,40 @@ def is_valid(self, *, raise_exception=True) -> bool: is_valid = super().is_valid(raise_exception=raise_exception) + target_team = None + target_user = None - if self.validated_data['target_team'] and self.validated_data['target_user']: + + if self.instance: + + if len(self.instance.target_team.filter()) > 0: + + target_team = self.instance.target_team.filter()[0] + + + if hasattr(self.instance, 'target_user_id'): + + target_user = self.instance.target_user_id + + + if 'target_team' in self.validated_data: + + target_team = self.validated_data['target_team'] + + + if 'target_user' in self.validated_data: + + target_user = self.validated_data['target_user'] + + + if target_team and target_user: is_valid = False raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other') - if not self.validated_data['target_team'] and not self.validated_data['target_user']: + if not target_team and not target_user: raise ValidationError('A Target Team or Target User must be assigned.') From 62ca58e820f1e3d5b1dcdd2da7e0301158fafbc3 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 20:15:53 +0930 Subject: [PATCH 171/617] refactor(api): Adjust viewset common so that page_layout is available for base ref: #248 #348 --- app/api/viewsets/common.py | 86 +++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index 631691179..eae3b646b 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -50,6 +50,18 @@ def allowed_methods(self): required to generate the UI. """ + model_documentation: str = None + """Model Documentation URL + + _Optional_, if specified will be add to detail view metadata""" + + page_layout: list = [] + """ Page layout class + + _Optional_, used by metadata to add the page layout to the HTTP/Options method + for detail view, Enables the UI can setup the page layout. + """ + permission_classes = [ OrganizationPermissionAPI ] """Permission Class @@ -61,6 +73,38 @@ def allowed_methods(self): view_name: str = None + def get_model_documentation(self): + + if not self.model_documentation: + + if hasattr(self.model, 'documentataion'): + + self.model_documentation = self.model.documentation + + else: + + self.model_documentation = '' + + return self.model_documentation + + + def get_page_layout(self): + + if len(self.page_layout) < 1: + + if hasattr(self, 'model'): + + if hasattr(self.model, 'page_layout'): + + self.page_layout = self.model.page_layout + + else: + + self.page_layout = [] + + return self.page_layout + + def get_view_description(self, html=False) -> str: if not self.view_description: @@ -111,18 +155,6 @@ class ModelViewSetBase( _Mandatory_, Django model used for this view. """ - model_documentation: str = None - """Model Documentation URL - - _Optional_, if specified will be add to detail view metadata""" - - page_layout: list = [] - """ Page layout class - - _Optional_, used by metadata to add the page layout to the HTTP/Options method - for detail view, Enables the UI can setup the page layout. - """ - queryset: object = None """View Queryset @@ -136,36 +168,6 @@ class ModelViewSetBase( """ - def get_model_documentation(self): - - if not self.model_documentation: - - if hasattr(self.model, 'documentataion'): - - self.model_documentation = self.model.documentation - - else: - - self.model_documentation = '' - - return self.model_documentation - - - def get_page_layout(self): - - if len(self.page_layout) < 1: - - if hasattr(self.model, 'page_layout'): - - self.page_layout = self.model.page_layout - - else: - - self.page_layout = [] - - return self.page_layout - - def get_queryset(self): if not self.queryset: From 308b5168d8d179d9dfe5be05691e4b8e31b0cb1d Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 20:16:37 +0930 Subject: [PATCH 172/617] test(assistance): Knowledge Base API ViewSet permission checks ref: #15 #248 #352 --- app/api/react_ui_metadata.py | 11 +- .../test_knowledge_base_viewset.py | 185 ++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 9eff7bb75..2419eee80 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -91,6 +91,10 @@ def determine_metadata(self, request, view): metadata['documentation'] = view.documentation + if hasattr(view, 'page_layout'): + + metadata['layout'] = view.get_page_layout() + metadata['navigation'] = [ { @@ -100,7 +104,6 @@ def determine_metadata(self, request, view): { "display_name": "Organization", "name": "organization", - "icon": "device", "link": "/access/organization" } ] @@ -114,6 +117,12 @@ def determine_metadata(self, request, view): "name": "request", "icon": "ticket", "link": "/assistance/ticket/request" + }, + { + "display_name": "Knowledge Base", + "name": "knowledge_base", + "icon": "kb", + "link": "/assistance/knowledge_base" } ] }, diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py new file mode 100644 index 000000000..920f3e71c --- /dev/null +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py @@ -0,0 +1,185 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from assistance.models.knowledge_base import KnowledgeBase + + + +class KnowledgeBasePermissionsAPI(TestCase, APIPermissions): + + model = KnowledgeBase + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base' + + change_data = {'title': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + + self.url_kwargs = {} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + title = 'one', + content = 'some text for body', + target_user = self.view_user + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'title': 'team_post', + 'organization': self.organization.id, + 'content': 'article text', + 'target_user': self.view_user.id + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From a32fe28a0d5f385b6a8cd38adf5894bd6e6a77c8 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 20:31:43 +0930 Subject: [PATCH 173/617] test(assistance): Knowledge Base Category API ViewSet permission checks ref: #15 #248 #352 --- .../test_knowledge_base_category_viewset.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py new file mode 100644 index 000000000..cbf0e9cb5 --- /dev/null +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py @@ -0,0 +1,179 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from assistance.models.knowledge_base import KnowledgeBaseCategory + + + +class KnowledgeBaseCategoryPermissionsAPI(TestCase, APIPermissions): + + model = KnowledgeBaseCategory + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base_category' + + change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + + # self.url_kwargs = {} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + target_user = self.view_user + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team_post', + 'organization': self.organization.id, + 'target_user': self.view_user.id + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 8b31a96508154c53abcdfc85427b3fc1d49a04f2 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 21:11:02 +0930 Subject: [PATCH 174/617] test(access): correct organization permission checks to have HTTP/403 not HTTP/405 ref: #15 #248 #352 --- .../unit/organization/test_organizaiton_permission_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/access/tests/unit/organization/test_organizaiton_permission_api.py b/app/access/tests/unit/organization/test_organizaiton_permission_api.py index 754a79993..dc96f60da 100644 --- a/app/access/tests/unit/organization/test_organizaiton_permission_api.py +++ b/app/access/tests/unit/organization/test_organizaiton_permission_api.py @@ -204,7 +204,7 @@ def test_add_is_prohibited_diff_org_user(self): client.force_login(self.different_organization_user) response = client.post(url, data={'name': 'should not create'}, content_type='application/json') - assert response.status_code == 405 + assert response.status_code == 403 def test_add_is_prohibited_super_user(self): @@ -220,7 +220,7 @@ def test_add_is_prohibited_super_user(self): client.force_login(self.super_user) response = client.post(url, data={'name': 'should not create'}, content_type='application/json') - assert response.status_code == 405 + assert response.status_code == 403 def test_add_is_prohibited_user_same_org(self): @@ -236,4 +236,4 @@ def test_add_is_prohibited_user_same_org(self): client.force_login(self.add_user) response = client.post(url, data={'name': 'should not create'}, content_type='application/json') - assert response.status_code == 405 + assert response.status_code == 403 From a87b313fdf7466e89042e553f29be8fc30a6deed Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 21:12:14 +0930 Subject: [PATCH 175/617] fix(assistance): correct KB category serializer validation ref: #15 #248 #352 --- .../serializers/knowledge_base_category.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py index 339193b61..1b795c456 100644 --- a/app/assistance/serializers/knowledge_base_category.py +++ b/app/assistance/serializers/knowledge_base_category.py @@ -123,8 +123,33 @@ def is_valid(self, *, raise_exception=True) -> bool: is_valid = super().is_valid(raise_exception=raise_exception) + target_team = None + target_user = None - if self.validated_data['target_team'] and self.validated_data['target_user']: + + if self.instance: + + if len(self.instance.target_team.filter()) > 0: + + target_team = self.instance.target_team.filter()[0] + + + if hasattr(self.instance, 'target_user_id'): + + target_user = self.instance.target_user_id + + + if 'target_team' in self.validated_data: + + target_team = self.validated_data['target_team'] + + + if 'target_user' in self.validated_data: + + target_user = self.validated_data['target_user'] + + + if target_team and target_user: is_valid = False From 6d58f33a6764d72d638b7961a243eb68d44bdee6 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 21:12:53 +0930 Subject: [PATCH 176/617] refactor(access): add name to modified field ref: #352 --- app/access/fields.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/access/fields.py b/app/access/fields.py index 715b88c89..c8b9e30c1 100644 --- a/app/access/fields.py +++ b/app/access/fields.py @@ -40,6 +40,14 @@ class AutoLastModifiedField(AutoCreatedField): verbose_name = 'Modified' + def __init__(self, *args, **kwargs): + + kwargs.setdefault("help_text", self.help_text) + + kwargs.setdefault("verbose_name", self.verbose_name) + + super().__init__(*args, **kwargs) + def pre_save(self, model_instance, add): value = now() From 80e981d40f7b75df1c737e0f1d34b4e42399b4a1 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 21:38:48 +0930 Subject: [PATCH 177/617] test(assistance): Knowledge Base API field checks ref: #15 #248 #352 --- .../test_knowledge_base_api_v2.py | 479 ++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py new file mode 100644 index 000000000..8dfb98800 --- /dev/null +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py @@ -0,0 +1,479 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from assistance.models.knowledge_base import KnowledgeBase, KnowledgeBaseCategory + + +class KnowledgeBaseAPI( + TestCase, + APITenancyObject +): + + model = KnowledgeBase + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create the object + 2. create view user + 4. make api request + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.view_team = Team.objects.create( + organization=organization, + team_name = 'teamone', + model_notes = 'random note' + ) + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + self.view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + + self.item = self.model.objects.create( + organization=organization, + title = 'teamone', + content = 'random note', + summary = 'a summary', + target_user = self.view_user, + release_date = '2024-01-01 12:00:00', + expiry_date = '2024-01-01 12:00:01', + responsible_user = self.view_user, + category = KnowledgeBaseCategory.objects.create( + name='cat', + target_user = self.view_user, + organization=organization, + ) + ) + + self.item.responsible_teams.set([self.view_team]) + + self.url_view_kwargs = {'pk': self.item.id} + + teamuser = TeamUsers.objects.create( + team = self.view_team, + user = self.view_user + ) + + organization.manager = self.view_user + + organization.save() + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + model_notes field does not exist for KB articles + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + model_notes does not exist for KB articles + """ + + pass + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_id(self): + """ Test for existance of API Field + + category.id field must exist + """ + + assert 'id' in self.api_data['category'] + + + def test_api_field_type_category_id(self): + """ Test for type for API Field + + category.id field must be int + """ + + assert type(self.api_data['category']['id']) is int + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be int + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be int + """ + + assert type(self.api_data['category']['url']) is str + + + + def test_api_field_exists_summary(self): + """ Test for existance of API Field + + summary field must exist + """ + + assert 'summary' in self.api_data + + + def test_api_field_type_summary(self): + """ Test for type for API Field + + summary field must be str + """ + + assert type(self.api_data['summary']) is str + + + + def test_api_field_exists_content(self): + """ Test for existance of API Field + + content field must exist + """ + + assert 'content' in self.api_data + + + def test_api_field_type_summary(self): + """ Test for type for API Field + + content field must be str + """ + + assert type(self.api_data['content']) is str + + + + def test_api_field_exists_release_date(self): + """ Test for existance of API Field + + release_date field must exist + """ + + assert 'release_date' in self.api_data + + + def test_api_field_type_release_date(self): + """ Test for type for API Field + + release_date field must be str + """ + + assert type(self.api_data['release_date']) is str + + + + def test_api_field_exists_expiry_date(self): + """ Test for existance of API Field + + expiry_date field must exist + """ + + assert 'expiry_date' in self.api_data + + + def test_api_field_type_expiry_date(self): + """ Test for type for API Field + + expiry_date field must be str + """ + + assert type(self.api_data['expiry_date']) is str + + + + def test_api_field_exists_public(self): + """ Test for existance of API Field + + public field must exist + """ + + assert 'public' in self.api_data + + + def test_api_field_type_public(self): + """ Test for type for API Field + + public field must be bool + """ + + assert type(self.api_data['public']) is bool + + + + def test_api_field_type_target_user(self): + """ Test for type for API Field + + target_user field must be dict + """ + + assert type(self.api_data['target_user']) is dict + + + def test_api_field_exists_target_user_id(self): + """ Test for existance of API Field + + target_user.id field must exist + """ + + assert 'id' in self.api_data['target_user'] + + + def test_api_field_type_target_user_id(self): + """ Test for type for API Field + + target_user.id field must be int + """ + + assert type(self.api_data['target_user']['id']) is int + + + def test_api_field_exists_target_user_display_name(self): + """ Test for existance of API Field + + target_user.display_name field must exist + """ + + assert 'display_name' in self.api_data['target_user'] + + + def test_api_field_type_target_user_display_name(self): + """ Test for type for API Field + + target_user.display_name field must be int + """ + + assert type(self.api_data['target_user']['display_name']) is str + + + def test_api_field_exists_target_user_url(self): + """ Test for existance of API Field + + target_user.url field must exist + """ + + assert 'url' in self.api_data['target_user'] + + + def test_api_field_type_target_user_url(self): + """ Test for type for API Field + + target_user.url field must be int + """ + + assert type(self.api_data['target_user']['url']) is Hyperlink + + + + def test_api_field_type_responsible_user(self): + """ Test for type for API Field + + responsible_user field must be dict + """ + + assert type(self.api_data['responsible_user']) is dict + + + def test_api_field_exists_responsible_user_id(self): + """ Test for existance of API Field + + responsible_user.id field must exist + """ + + assert 'id' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_id(self): + """ Test for type for API Field + + responsible_user.id field must be int + """ + + assert type(self.api_data['responsible_user']['id']) is int + + + def test_api_field_exists_responsible_user_display_name(self): + """ Test for existance of API Field + + responsible_user.display_name field must exist + """ + + assert 'display_name' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_display_name(self): + """ Test for type for API Field + + responsible_user.display_name field must be int + """ + + assert type(self.api_data['responsible_user']['display_name']) is str + + + def test_api_field_exists_responsible_user_url(self): + """ Test for existance of API Field + + responsible_user.url field must exist + """ + + assert 'url' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_url(self): + """ Test for type for API Field + + responsible_user.url field must be Hyperlink + """ + + assert type(self.api_data['responsible_user']['url']) is Hyperlink + + + + def test_api_field_type_responsible_teams(self): + """ Test for type for API Field + + responsible_teams field must be list + """ + + assert type(self.api_data['responsible_teams']) is list + + + def test_api_field_exists_responsible_teams_id(self): + """ Test for existance of API Field + + responsible_teams.id field must exist + """ + + assert 'id' in self.api_data['responsible_teams'][0] + + + def test_api_field_type_responsible_teams_id(self): + """ Test for type for API Field + + responsible_teams.id field must be int + """ + + assert type(self.api_data['responsible_teams'][0]['id']) is int + + + def test_api_field_exists_responsible_teams_display_name(self): + """ Test for existance of API Field + + responsible_teams.display_name field must exist + """ + + assert 'display_name' in self.api_data['responsible_teams'][0] + + + def test_api_field_type_responsible_teams_display_name(self): + """ Test for type for API Field + + responsible_teams.display_name field must be int + """ + + assert type(self.api_data['responsible_teams'][0]['display_name']) is str + + + def test_api_field_exists_responsible_teams_url(self): + """ Test for existance of API Field + + responsible_teams.url field must exist + """ + + assert 'url' in self.api_data['responsible_teams'][0] + + + def test_api_field_type_responsible_teams_url(self): + """ Test for type for API Field + + responsible_teams.url field must be str + """ + + assert type(self.api_data['responsible_teams'][0]['url']) is str From 073330015c42c3af6301c9e8475538d8aca8b8d5 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 21:55:26 +0930 Subject: [PATCH 178/617] test(assistance): Knowledge Base Category API field checks ref: #15 #248 #352 --- .../test_knowledge_base_category_api_v2.py | 475 ++++++++++++++++++ 1 file changed, 475 insertions(+) create mode 100644 app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py new file mode 100644 index 000000000..649a92f03 --- /dev/null +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py @@ -0,0 +1,475 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from assistance.models.knowledge_base import KnowledgeBase, KnowledgeBaseCategory + + +class KnowledgeBaseCategoryAPI( + TestCase, + APITenancyObject +): + + model = KnowledgeBaseCategory + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base_category' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create the object + 2. create view user + 4. make api request + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + self.view_team = Team.objects.create( + organization=organization, + team_name = 'teamone', + model_notes = 'random note' + ) + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + self.view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + parent_category = self.model.objects.create( + name='cat parent', + target_user = self.view_user, + organization=organization, + ) + + self.item = self.model.objects.create( + name='cat', + model_notes='dsa', + target_user = self.view_user, + organization=organization, + parent_category = parent_category, + ) + + # self.item.target_teams.set([self.view_team]) + + self.url_view_kwargs = {'pk': self.item.id} + + teamuser = TeamUsers.objects.create( + team = self.view_team, + user = self.view_user + ) + + organization.manager = self.view_user + + organization.save() + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + # def test_api_field_exists_model_notes(self): + # """ Test for existance of API Field + + # model_notes field does not exist for KB articles + # """ + + # assert 'model_notes' not in self.api_data + + + # def test_api_field_type_model_notes(self): + # """ Test for type for API Field + + # model_notes does not exist for KB articles + # """ + + # pass + + + + def test_api_field_exists_parent_category(self): + """ Test for existance of API Field + + parent_category field must exist + """ + + assert 'parent_category' in self.api_data + + + def test_api_field_type_parent_category(self): + """ Test for type for API Field + + parent_category field must be dict + """ + + assert type(self.api_data['parent_category']) is dict + + + def test_api_field_exists_parent_category_id(self): + """ Test for existance of API Field + + parent_category.id field must exist + """ + + assert 'id' in self.api_data['parent_category'] + + + def test_api_field_type_parent_category_id(self): + """ Test for type for API Field + + parent_category.id field must be int + """ + + assert type(self.api_data['parent_category']['id']) is int + + + def test_api_field_exists_parent_category_display_name(self): + """ Test for existance of API Field + + parent_category.display_name field must exist + """ + + assert 'display_name' in self.api_data['parent_category'] + + + def test_api_field_type_parent_category_display_name(self): + """ Test for type for API Field + + parent_category.display_name field must be int + """ + + assert type(self.api_data['parent_category']['display_name']) is str + + + def test_api_field_exists_parent_category_url(self): + """ Test for existance of API Field + + parent_category.url field must exist + """ + + assert 'url' in self.api_data['parent_category'] + + + def test_api_field_type_parent_category_url(self): + """ Test for type for API Field + + parent_category.url field must be int + """ + + assert type(self.api_data['parent_category']['url']) is str + + + + # def test_api_field_exists_summary(self): + # """ Test for existance of API Field + + # summary field must exist + # """ + + # assert 'summary' in self.api_data + + + # def test_api_field_type_summary(self): + # """ Test for type for API Field + + # summary field must be str + # """ + + # assert type(self.api_data['summary']) is str + + + + # def test_api_field_exists_content(self): + # """ Test for existance of API Field + + # content field must exist + # """ + + # assert 'content' in self.api_data + + + # def test_api_field_type_summary(self): + # """ Test for type for API Field + + # content field must be str + # """ + + # assert type(self.api_data['content']) is str + + + + # def test_api_field_exists_release_date(self): + # """ Test for existance of API Field + + # release_date field must exist + # """ + + # assert 'release_date' in self.api_data + + + # def test_api_field_type_release_date(self): + # """ Test for type for API Field + + # release_date field must be str + # """ + + # assert type(self.api_data['release_date']) is str + + + + # def test_api_field_exists_expiry_date(self): + # """ Test for existance of API Field + + # expiry_date field must exist + # """ + + # assert 'expiry_date' in self.api_data + + + # def test_api_field_type_expiry_date(self): + # """ Test for type for API Field + + # expiry_date field must be str + # """ + + # assert type(self.api_data['expiry_date']) is str + + + + # def test_api_field_exists_public(self): + # """ Test for existance of API Field + + # public field must exist + # """ + + # assert 'public' in self.api_data + + + # def test_api_field_type_public(self): + # """ Test for type for API Field + + # public field must be bool + # """ + + # assert type(self.api_data['public']) is bool + + + + def test_api_field_type_target_user(self): + """ Test for type for API Field + + target_user field must be dict + """ + + assert type(self.api_data['target_user']) is dict + + + def test_api_field_exists_target_user_id(self): + """ Test for existance of API Field + + target_user.id field must exist + """ + + assert 'id' in self.api_data['target_user'] + + + def test_api_field_type_target_user_id(self): + """ Test for type for API Field + + target_user.id field must be int + """ + + assert type(self.api_data['target_user']['id']) is int + + + def test_api_field_exists_target_user_display_name(self): + """ Test for existance of API Field + + target_user.display_name field must exist + """ + + assert 'display_name' in self.api_data['target_user'] + + + def test_api_field_type_target_user_display_name(self): + """ Test for type for API Field + + target_user.display_name field must be int + """ + + assert type(self.api_data['target_user']['display_name']) is str + + + def test_api_field_exists_target_user_url(self): + """ Test for existance of API Field + + target_user.url field must exist + """ + + assert 'url' in self.api_data['target_user'] + + + def test_api_field_type_target_user_url(self): + """ Test for type for API Field + + target_user.url field must be int + """ + + assert type(self.api_data['target_user']['url']) is Hyperlink + + + + # def test_api_field_type_responsible_user(self): + # """ Test for type for API Field + + # responsible_user field must be dict + # """ + + # assert type(self.api_data['responsible_user']) is dict + + + # def test_api_field_exists_responsible_user_id(self): + # """ Test for existance of API Field + + # responsible_user.id field must exist + # """ + + # assert 'id' in self.api_data['responsible_user'] + + + # def test_api_field_type_responsible_user_id(self): + # """ Test for type for API Field + + # responsible_user.id field must be int + # """ + + # assert type(self.api_data['responsible_user']['id']) is int + + + # def test_api_field_exists_responsible_user_display_name(self): + # """ Test for existance of API Field + + # responsible_user.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['responsible_user'] + + + # def test_api_field_type_responsible_user_display_name(self): + # """ Test for type for API Field + + # responsible_user.display_name field must be int + # """ + + # assert type(self.api_data['responsible_user']['display_name']) is str + + + # def test_api_field_exists_responsible_user_url(self): + # """ Test for existance of API Field + + # responsible_user.url field must exist + # """ + + # assert 'url' in self.api_data['responsible_user'] + + + # def test_api_field_type_responsible_user_url(self): + # """ Test for type for API Field + + # responsible_user.url field must be Hyperlink + # """ + + # assert type(self.api_data['responsible_user']['url']) is Hyperlink + + + + # def test_api_field_type_responsible_teams(self): + # """ Test for type for API Field + + # responsible_teams field must be list + # """ + + # assert type(self.api_data['responsible_teams']) is list + + + # def test_api_field_exists_responsible_teams_id(self): + # """ Test for existance of API Field + + # responsible_teams.id field must exist + # """ + + # assert 'id' in self.api_data['responsible_teams'][0] + + + # def test_api_field_type_responsible_teams_id(self): + # """ Test for type for API Field + + # responsible_teams.id field must be int + # """ + + # assert type(self.api_data['responsible_teams'][0]['id']) is int + + + # def test_api_field_exists_responsible_teams_display_name(self): + # """ Test for existance of API Field + + # responsible_teams.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['responsible_teams'][0] + + + # def test_api_field_type_responsible_teams_display_name(self): + # """ Test for type for API Field + + # responsible_teams.display_name field must be int + # """ + + # assert type(self.api_data['responsible_teams'][0]['display_name']) is str + + + # def test_api_field_exists_responsible_teams_url(self): + # """ Test for existance of API Field + + # responsible_teams.url field must exist + # """ + + # assert 'url' in self.api_data['responsible_teams'][0] + + + # def test_api_field_type_responsible_teams_url(self): + # """ Test for type for API Field + + # responsible_teams.url field must be str + # """ + + # assert type(self.api_data['responsible_teams'][0]['url']) is str From 42e38e26d93a1c9e9409598dc660bca38a6c6829 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 21:56:19 +0930 Subject: [PATCH 179/617] fix(assistance): Add missing fields `display_name` and `model_notes` to Knowledge Base Category serializer ref: #248 #352 --- app/assistance/serializers/knowledge_base_category.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py index 1b795c456..0088cd46c 100644 --- a/app/assistance/serializers/knowledge_base_category.py +++ b/app/assistance/serializers/knowledge_base_category.py @@ -98,6 +98,8 @@ class Meta: 'id', 'organization', 'name', + 'display_name', + 'model_notes', 'parent_category', 'target_user', 'target_team', From a4369c4a00b726675a2496dc527c3e141ffd85c1 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 15 Oct 2024 22:31:11 +0930 Subject: [PATCH 180/617] fix(itam): Correct inventory api upload to use API exceptions instead of django base ref: #352 --- app/api/views/itam/inventory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/api/views/itam/inventory.py b/app/api/views/itam/inventory.py index 1cc30392f..12ade69ce 100644 --- a/app/api/views/itam/inventory.py +++ b/app/api/views/itam/inventory.py @@ -1,11 +1,10 @@ import json import re -from django.core.exceptions import ValidationError, PermissionDenied - from drf_spectacular.utils import extend_schema, OpenApiResponse from rest_framework import generics, views +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.response import Response from api.views.mixin import OrganizationPermissionAPI @@ -91,12 +90,13 @@ def post(self, request, *args, **kwargs): if not self.permission_check(request=request, view=self, obj=device): - raise Http404 + raise PermissionDenied() task = process_inventory.delay(request.body, self.default_organization.id) response_data: dict = {"task_id": f"{task.id}"} + except PermissionDenied as e: status = Http.Status.FORBIDDEN From 138f8237fe9359c11f02a2c2f17a235ed8060dea Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 01:33:27 +0930 Subject: [PATCH 181/617] test(assistance): Knowledge Base Serializer Validation checks ref: #15 #248 #352 --- .../test_knowledge_base_serializer.py | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py new file mode 100644 index 000000000..395f2c557 --- /dev/null +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py @@ -0,0 +1,162 @@ +import json +import pytest + +from django.contrib.auth.models import User +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization, Team + +from assistance.models.knowledge_base import KnowledgeBase +from assistance.serializers.knowledge_base import KnowledgeBaseModelSerializer + + + +class KnowledgeBaseValidationAPI( + TestCase, +): + + model = KnowledgeBase + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create a team + 4. Add user to add team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.add_team = Team.objects.create( + organization=organization, + team_name = 'teamone', + model_notes = 'random note' + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + self.item_has_target_user = self.model.objects.create( + organization=organization, + title = 'random title', + content = 'random note', + summary = 'a summary', + target_user = self.add_user, + release_date = '2024-01-01 12:00:00', + expiry_date = '2024-01-01 12:00:01', + responsible_user = self.add_user, + ) + + self.item_has_target_team = self.model.objects.create( + organization=organization, + title = 'random title', + content = 'random note', + summary = 'a summary', + release_date = '2024-01-01 12:00:00', + expiry_date = '2024-01-01 12:00:01', + responsible_user = self.add_user, + ) + + self.item_has_target_team.target_team.set([ self.add_team ]) + + + + def test_serializer_validation_no_title(self): + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseModelSerializer(data={ + "organization": self.organization.id, + "content": "random note", + "target_user": self.add_user.id, + "target_team": [ + self.add_team.id + ], + "responsible_user": self.add_user.id, + }) + + serializer.is_valid() + + assert err.value.get_codes()['title'][0] == 'required' + + + + def test_serializer_validation_both_target_team_target_user(self): + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseModelSerializer(data={ + "organization": self.organization.id, + "title": "teamone", + "content": "random note", + "target_user": self.add_user.id, + "target_team": [ + self.add_team.id + ], + "responsible_user": self.add_user.id, + }) + + serializer.is_valid() + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' + + + + def test_serializer_validation_no_target_team_target_user(self): + + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseModelSerializer(data={ + "organization": self.organization.id, + "title": 'teamone', + "content": 'random note', + "responsible_user": self.add_user.id, + }) + + serializer.is_valid() + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_need_target_team_or_user' + + + + def test_serializer_validation_update_existing_target_user(self): + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseModelSerializer( + self.item_has_target_user, + data={ + "target_team": [ self.add_team.id ] + }, + partial=True, + ) + + serializer.is_valid() + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' + + + def test_serializer_validation_update_existing_target_team(self): + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseModelSerializer( + self.item_has_target_team, + data={ + "target_user": self.add_user.id + }, + partial=True, + ) + + serializer.is_valid() + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' From ea4952bd3e9de71ed5ceadf23bb6689331463e13 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 01:45:15 +0930 Subject: [PATCH 182/617] fix(itam): Correct inventory validation response data ref: #352 --- app/api/serializers/inventory.py | 5 ++++- app/api/views/itam/inventory.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/api/serializers/inventory.py b/app/api/serializers/inventory.py index b12370e9c..a2fac11f0 100644 --- a/app/api/serializers/inventory.py +++ b/app/api/serializers/inventory.py @@ -1,6 +1,9 @@ -from django.core.exceptions import ValidationError from django.utils.html import escape +from rest_framework.exceptions import ValidationError + + + class Inventory: """ Inventory Object diff --git a/app/api/views/itam/inventory.py b/app/api/views/itam/inventory.py index 12ade69ce..aee5c22a4 100644 --- a/app/api/views/itam/inventory.py +++ b/app/api/views/itam/inventory.py @@ -105,7 +105,7 @@ def post(self, request, *args, **kwargs): except ValidationError as e: status = Http.Status.BAD_REQUEST - response_data = e.message + response_data = e.detail except Exception as e: From df213c2015f589551fe7b9447db32ef79ab9e27a Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 01:53:08 +0930 Subject: [PATCH 183/617] feat(assistance): Knowledge Base Serializer Validation method added ref: #248 #352 --- app/assistance/serializers/knowledge_base.py | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py index 705589580..5b2745bef 100644 --- a/app/assistance/serializers/knowledge_base.py +++ b/app/assistance/serializers/knowledge_base.py @@ -125,12 +125,7 @@ class Meta: ] - - def is_valid(self, *, raise_exception=True) -> bool: - - is_valid = False - - is_valid = super().is_valid(raise_exception=raise_exception) + def validate(self, attrs): target_team = None target_user = None @@ -148,29 +143,41 @@ def is_valid(self, *, raise_exception=True) -> bool: target_user = self.instance.target_user_id - if 'target_team' in self.validated_data: + if 'target_team' in self.initial_data: - target_team = self.validated_data['target_team'] + target_team = self.initial_data['target_team'] - if 'target_user' in self.validated_data: + if 'target_user' in self.initial_data: - target_user = self.validated_data['target_user'] + target_user = self.initial_data['target_user'] if target_team and target_user: is_valid = False - raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other') + raise ValidationError( + detail = [ + 'Both a Target Team or Target User Cant be assigned at the same time. Use one or the other' + ], + code = 'invalid_not_both_target_team_user' + ) if not target_team and not target_user: - raise ValidationError('A Target Team or Target User must be assigned.') + is_valid = False + raise ValidationError( + detail = [ + 'A Target Team or Target User must be assigned.' + ], + code='invalid_need_target_team_or_user' + ) - return is_valid + + return super().validate(attrs) From ed6cf305ae027f49d11bf9c8d3c61d17771332be Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 02:05:38 +0930 Subject: [PATCH 184/617] test(assistance): ensure is_valid raises exceptions for Knowledge Base Serializer Validation checks ref: #15 #248 #352 --- .../knowledge_base/test_knowledge_base_serializer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py index 395f2c557..fa6dae6f7 100644 --- a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py @@ -83,7 +83,7 @@ def test_serializer_validation_no_title(self): "responsible_user": self.add_user.id, }) - serializer.is_valid() + serializer.is_valid(raise_exception = True) assert err.value.get_codes()['title'][0] == 'required' @@ -104,7 +104,7 @@ def test_serializer_validation_both_target_team_target_user(self): "responsible_user": self.add_user.id, }) - serializer.is_valid() + serializer.is_valid(raise_exception = True) assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' @@ -122,7 +122,7 @@ def test_serializer_validation_no_target_team_target_user(self): "responsible_user": self.add_user.id, }) - serializer.is_valid() + serializer.is_valid(raise_exception = True) assert err.value.get_codes()['non_field_errors'][0] == 'invalid_need_target_team_or_user' @@ -140,7 +140,7 @@ def test_serializer_validation_update_existing_target_user(self): partial=True, ) - serializer.is_valid() + serializer.is_valid(raise_exception = True) assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' @@ -157,6 +157,6 @@ def test_serializer_validation_update_existing_target_team(self): partial=True, ) - serializer.is_valid() + serializer.is_valid(raise_exception = True) assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' From 56235e6ffe0bfdc82726a7e0838a5aafcbe8225e Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 11:28:20 +0930 Subject: [PATCH 185/617] fix(assistance): Correct Knowledge Base Category serializer Validation ref: #248 #348 --- app/assistance/serializers/knowledge_base.py | 5 --- .../serializers/knowledge_base_category.py | 33 ++++++++++++------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py index 5b2745bef..71d4ff952 100644 --- a/app/assistance/serializers/knowledge_base.py +++ b/app/assistance/serializers/knowledge_base.py @@ -155,8 +155,6 @@ def validate(self, attrs): if target_team and target_user: - is_valid = False - raise ValidationError( detail = [ 'Both a Target Team or Target User Cant be assigned at the same time. Use one or the other' @@ -167,8 +165,6 @@ def validate(self, attrs): if not target_team and not target_user: - is_valid = False - raise ValidationError( detail = [ 'A Target Team or Target User must be assigned.' @@ -176,7 +172,6 @@ def validate(self, attrs): code='invalid_need_target_team_or_user' ) - return super().validate(attrs) diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py index 0088cd46c..27f68d2e2 100644 --- a/app/assistance/serializers/knowledge_base_category.py +++ b/app/assistance/serializers/knowledge_base_category.py @@ -119,11 +119,7 @@ class Meta: - def is_valid(self, *, raise_exception=True) -> bool: - - is_valid = False - - is_valid = super().is_valid(raise_exception=raise_exception) + def validate(self, attrs): target_team = None target_user = None @@ -141,24 +137,37 @@ def is_valid(self, *, raise_exception=True) -> bool: target_user = self.instance.target_user_id - if 'target_team' in self.validated_data: + if 'target_team' in self.initial_data: - target_team = self.validated_data['target_team'] + target_team = self.initial_data['target_team'] - if 'target_user' in self.validated_data: + if 'target_user' in self.initial_data: - target_user = self.validated_data['target_user'] + target_user = self.initial_data['target_user'] if target_team and target_user: - is_valid = False + raise ValidationError( + detail = [ + 'Both a Target Team or Target User Cant be assigned at the same time. Use one or the other' + ], + code = 'invalid_not_both_target_team_user' + ) + + + if not target_team and not target_user: - raise ValidationError('Both a Target Team or Target User Cant be assigned at the same time. Use one or the other or None') + raise ValidationError( + detail = [ + 'A Target Team or Target User must be assigned.' + ], + code='invalid_need_target_team_or_user' + ) - return is_valid + return super().validate(attrs) From 9278668e587b3a30a85df31ffe37f87c806b0c1d Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 11:34:37 +0930 Subject: [PATCH 186/617] test(assistance): Knowledge Base Category Serializer Validation checks ref: #15 #248 #352 --- .../test_knowledge_base_serializer.py | 22 +++ ...test_knowledge_base_category_serializer.py | 166 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py index fa6dae6f7..2de29d392 100644 --- a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_serializer.py @@ -70,6 +70,10 @@ def setUpTestData(self): def test_serializer_validation_no_title(self): + """Serializer Validation Check + + Ensure that if creating and no title is provided a validation error occurs + """ with pytest.raises(ValidationError) as err: @@ -90,6 +94,10 @@ def test_serializer_validation_no_title(self): def test_serializer_validation_both_target_team_target_user(self): + """Serializer Validation Check + + Ensure that both target user and target team raises a validation error + """ with pytest.raises(ValidationError) as err: @@ -111,6 +119,10 @@ def test_serializer_validation_both_target_team_target_user(self): def test_serializer_validation_no_target_team_target_user(self): + """Serializer Validation Check + + Ensure that if either target user and target team is missing it raises validation error + """ with pytest.raises(ValidationError) as err: @@ -129,6 +141,11 @@ def test_serializer_validation_no_target_team_target_user(self): def test_serializer_validation_update_existing_target_user(self): + """Serializer Validation Check + + Ensure that if an existing item with target user is updated to include a target_team + it raises a validation error + """ with pytest.raises(ValidationError) as err: @@ -146,6 +163,11 @@ def test_serializer_validation_update_existing_target_user(self): def test_serializer_validation_update_existing_target_team(self): + """Serializer Validation Check + + Ensure that if an existing item with target team is updated to include a target_user + it raises a validation error + """ with pytest.raises(ValidationError) as err: diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py new file mode 100644 index 000000000..7477a179d --- /dev/null +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py @@ -0,0 +1,166 @@ +import json +import pytest + +from django.contrib.auth.models import User +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization, Team + +from assistance.serializers.knowledge_base_category import KnowledgeBaseCategory, KnowledgeBaseCategoryModelSerializer + + + +class KnowledgeBaseCategoryValidationAPI( + TestCase, +): + + model = KnowledgeBaseCategory + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create a team + 4. Add user to add team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.add_team = Team.objects.create( + organization=organization, + team_name = 'teamone', + model_notes = 'random note' + ) + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + self.item_has_target_user = self.model.objects.create( + organization=organization, + name = 'random title', + target_user = self.add_user, + ) + + self.item_has_target_team = self.model.objects.create( + organization=organization, + name = 'random title0', + ) + + self.item_has_target_team.target_team.set([ self.add_team ]) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseCategoryModelSerializer(data={ + "organization": self.organization.id, + "target_user": self.add_user.id, + "target_team": [ + self.add_team.id + ] + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_both_target_team_target_user(self): + """Serializer Validation Check + + Ensure that both target user and target team raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseCategoryModelSerializer(data={ + "organization": self.organization.id, + "name": "teamone", + "target_user": self.add_user.id, + "target_team": [ + self.add_team.id + ] + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' + + + + def test_serializer_validation_no_target_team_target_user(self): + """Serializer Validation Check + + Ensure that if either target user and target team is missing it raises validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseCategoryModelSerializer(data={ + "organization": self.organization.id, + "name": 'teamone' + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_need_target_team_or_user' + + + + def test_serializer_validation_update_existing_target_user(self): + """Serializer Validation Check + + Ensure that if an existing item with target user is updated to include a target_team + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseCategoryModelSerializer( + self.item_has_target_user, + data={ + "target_team": [ self.add_team.id ] + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' + + + def test_serializer_validation_update_existing_target_team(self): + """Serializer Validation Check + + Ensure that if an existing item with target team is updated to include a target_user + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseCategoryModelSerializer( + self.item_has_target_team, + data={ + "target_user": self.add_user.id + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['non_field_errors'][0] == 'invalid_not_both_target_team_user' From 47d5e4031502b67d8bb86da6e9beefefae720e66 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 11:46:32 +0930 Subject: [PATCH 187/617] feat(assistance): Ensure Knowledge Base Category cant assign self as parent category ref: #15 #248 #352 --- .../serializers/knowledge_base_category.py | 12 ++++++++++ ...test_knowledge_base_category_serializer.py | 22 +++++++++++++++++++ .../centurion_erp/development/testing.md | 9 +++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py index 27f68d2e2..9519a2a31 100644 --- a/app/assistance/serializers/knowledge_base_category.py +++ b/app/assistance/serializers/knowledge_base_category.py @@ -137,6 +137,18 @@ def validate(self, attrs): target_user = self.instance.target_user_id + if 'parent_category' in self.initial_data: + + if self.instance.id == self.initial_data['parent_category']: + + raise ValidationError( + detail = { + 'parent_category': 'Can not assign self as parent caategory' + }, + code = 'parent_category_not_self' + ) + + if 'target_team' in self.initial_data: target_team = self.initial_data['target_team'] diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py index 7477a179d..34ec5049c 100644 --- a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_serializer.py @@ -80,6 +80,28 @@ def test_serializer_validation_no_name(self): + def test_serializer_validation_parent_category_not_self(self): + """Serializer Validation Check + + Ensure that you cant assisgn self as parent category + """ + + with pytest.raises(ValidationError) as err: + + serializer = KnowledgeBaseCategoryModelSerializer( + self.item_has_target_user, + data={ + "parent_category": self.item_has_target_user.id + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['parent_category'][0] == 'parent_category_not_self' + + + def test_serializer_validation_both_target_team_target_user(self): """Serializer Validation Check diff --git a/docs/projects/centurion_erp/development/testing.md b/docs/projects/centurion_erp/development/testing.md index 46e946231..628a7ee20 100644 --- a/docs/projects/centurion_erp/development/testing.md +++ b/docs/projects/centurion_erp/development/testing.md @@ -107,12 +107,13 @@ example file system structure showing the layout of the tests directory for a mo │   └── unit │   ├── __init__.py │   └── +│      ├── test_.py │      ├── test__api.py -│      ├── test__permission_api.py -│      ├── test__permission.py │      ├── test__core_history.py │      ├── test__history_permission.py -│      ├── test_.py +│      ├── test__permission_api.py +│      ├── test__permission.py +│      ├── test__serializer.py │      └── test__viewsets.py ``` @@ -159,6 +160,8 @@ Items to test include, and are not limited to: _Field(s) exists, Type is checked_ +- Serializer Validations + ## Running Tests From 5b9ed1db2ca42bc3fbdb4b2301d210ed5859b71e Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 12:09:13 +0930 Subject: [PATCH 188/617] refactor(config_management): update serializer dir name ref: #248 --- app/api/urls.py | 6 +++--- .../tests/unit/test_config_management_viewset.py | 2 +- app/config_management/{viewset => viewsets}/index.py | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename app/config_management/{viewset => viewsets}/index.py (100%) diff --git a/app/api/urls.py b/app/api/urls.py index 3cc5989e7..696514a2b 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -53,7 +53,7 @@ knowledge_base_category as knowledge_base_category_v2 ) -from config_management.viewset import ( +from config_management.viewsets import ( index as config_management_v2 ) @@ -125,12 +125,12 @@ router.register('v2/base/permission', permission_v2.ViewSet, basename='_api_v2_permission') router.register('v2/base/user', user_v2.ViewSet, basename='_api_v2_user') +router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') + router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') -router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') - router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') diff --git a/app/config_management/tests/unit/test_config_management_viewset.py b/app/config_management/tests/unit/test_config_management_viewset.py index 0cca53167..c3b8768c2 100644 --- a/app/config_management/tests/unit/test_config_management_viewset.py +++ b/app/config_management/tests/unit/test_config_management_viewset.py @@ -6,7 +6,7 @@ from api.tests.abstract.viewsets import ViewSetCommon -from config_management.viewset.index import Index +from config_management.viewsets.index import Index class ConfigManagementViewset( diff --git a/app/config_management/viewset/index.py b/app/config_management/viewsets/index.py similarity index 100% rename from app/config_management/viewset/index.py rename to app/config_management/viewsets/index.py From 10704457b02fa5a2092df72b009c4e9078641d06 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 14:49:33 +0930 Subject: [PATCH 189/617] feat(config_management): Add Config Group API v2 endpoint ref: #248 #348 --- app/api/urls.py | 5 +- .../serializers/config_group.py | 147 ++++++++++++++++++ .../viewsets/config_group.py | 104 +++++++++++++ app/config_management/viewsets/index.py | 2 +- 4 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 app/config_management/serializers/config_group.py create mode 100644 app/config_management/viewsets/config_group.py diff --git a/app/api/urls.py b/app/api/urls.py index 696514a2b..507aa5697 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -54,7 +54,8 @@ ) from config_management.viewsets import ( - index as config_management_v2 + index as config_management_v2, + config_group as config_group_v2 ) from itam.viewset import ( @@ -126,6 +127,8 @@ router.register('v2/base/user', user_v2.ViewSet, basename='_api_v2_user') router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') +router.register('v2/config_management/group', config_group_v2.ViewSet, basename='_api_v2_config_group') +router.register('v2/config_management/group/(?P[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child') router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py new file mode 100644 index 000000000..dfc6e8ba2 --- /dev/null +++ b/app/config_management/serializers/config_group.py @@ -0,0 +1,147 @@ +from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.reverse import reverse + +from access.serializers.organization import OrganizationBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from config_management.models.groups import ConfigGroups + + + +class ConfigGroupBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_config_group-detail", + request=self.context['view'].request, + kwargs={ + 'pk': item.pk + } + ) + + + class Meta: + + model = ConfigGroups + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class ConfigGroupModelSerializer(ConfigGroupBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + 'API:_api_v2_config_group-detail', + request = self.context['view'].request, + kwargs = { + 'pk': item.pk + } + ), + 'configgroups': reverse( + 'API:_api_v2_config_group-list', + request = self.context['view'].request, + ), + 'organization': reverse( + 'API:_api_v2_organization-list', + request=self.context['view'].request, + ), + 'parent': reverse( + 'API:_api_v2_config_group-list', + request=self.context['view'].request, + ), + } + + + class Meta: + + model = ConfigGroups + + fields = [ + 'id', + 'display_name', + 'organization', + 'parent', + 'name', + 'model_notes', + 'config', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + def get_field_names(self, declared_fields, info): + + fields = self.Meta.fields + + if 'view' in self._context: + + if 'parent_group' in self._context['view'].kwargs: + + self.Meta.read_only_fields += [ + 'organization', + 'parent' + ] + + return fields + + + def is_valid(self, *, raise_exception=True) -> bool: + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'parent_group' in self._context['view'].kwargs: + + self.validated_data['parent_id'] = int(self._context['view'].kwargs['parent_group']) + + organization = self.Meta.model.objects.get(pk = int(self._context['view'].kwargs['parent_group'])) + + self.validated_data['organization_id'] = organization.id + + return is_valid + + +class ConfigGroupViewSerializer(ConfigGroupModelSerializer): + + parent = ConfigGroupBaseSerializer( read_only = True ) + + organization = OrganizationBaseSerializer( many=False, read_only=True ) diff --git a/app/config_management/viewsets/config_group.py b/app/config_management/viewsets/config_group.py new file mode 100644 index 000000000..73fcf1ec6 --- /dev/null +++ b/app/config_management/viewsets/config_group.py @@ -0,0 +1,104 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from config_management.serializers.config_group import ( + ConfigGroups, + ConfigGroupModelSerializer, + ConfigGroupViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a config group', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=ConfigGroupViewSerializer), + 201: OpenApiResponse(description='Created', response=ConfigGroupViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a config group', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all config groups', + description='', + responses = { + 200: OpenApiResponse(description='', response=ConfigGroupViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single config group', + description='', + responses = { + 200: OpenApiResponse(description='', response=ConfigGroupViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update aconfig group', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ConfigGroupViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'parent', + 'is_global', + ] + + search_fields = [ + 'name', + 'config', + ] + + model = ConfigGroups + + documentation: str = '' + + view_description = 'Information Management Knowledge Base Article(s)' + + + def get_queryset(self): + + if 'parent_group' in self.kwargs: + + self.queryset = super().get_queryset().filter(parent = self.kwargs['parent_group']) + + else: + + self.queryset = super().get_queryset() + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + diff --git a/app/config_management/viewsets/index.py b/app/config_management/viewsets/index.py index 8ceb7051b..3674cea9f 100644 --- a/app/config_management/viewsets/index.py +++ b/app/config_management/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "group": "ToDo", + "group": reverse('API:_api_v2_config_group-list', request=request), } ) From 8b091e3c798120f5268f2f37c7686240ac5d170b Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 14:50:49 +0930 Subject: [PATCH 190/617] test(assistance): Config Group API field checks ref: #15 #248 #353 --- .../test_config_groups_api_v2.py | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py b/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py new file mode 100644 index 000000000..3d9d1b7fa --- /dev/null +++ b/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py @@ -0,0 +1,172 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from config_management.models.groups import ConfigGroups + + + +class ConfigGroupsAPI( + TestCase, + APITenancyObject +): + + model = ConfigGroups + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + config = dict({"key": "one", "existing": "dont_over_write"}) + ) + + self.second_item = self.model.objects.create( + organization = self.organization, + name = 'one_two', + model_notes = 'stuff', + config = dict({"key": "two"}), + parent = self.item + ) + + self.url_view_kwargs = {'pk': self.second_item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_config_group-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is dict + + + + def test_api_field_exists_parent(self): + """ Test for existance of API Field + + parent field must exist + """ + + assert 'parent' in self.api_data + + + def test_api_field_type_parent(self): + """ Test for type for API Field + + parent field must be dict + """ + + assert type(self.api_data['parent']) is dict + + + def test_api_field_exists_parent_id(self): + """ Test for existance of API Field + + parent.id field must exist + """ + + assert 'id' in self.api_data['parent'] + + + def test_api_field_type_parent_id(self): + """ Test for type for API Field + + parent.id field must be int + """ + + assert type(self.api_data['parent']['id']) is int + + + def test_api_field_exists_parent_display_name(self): + """ Test for existance of API Field + + parent.display_name field must exist + """ + + assert 'display_name' in self.api_data['parent'] + + + def test_api_field_type_parent_display_name(self): + """ Test for type for API Field + + parent.display_name field must be str + """ + + assert type(self.api_data['parent']['display_name']) is str + + + def test_api_field_exists_parent_url(self): + """ Test for existance of API Field + + parent.url field must exist + """ + + assert 'url' in self.api_data['parent'] + + + def test_api_field_type_parent_url(self): + """ Test for type for API Field + + parent.url field must be str + """ + + assert type(self.api_data['parent']['url']) is str From cc6770278ff56dee830112f9999ae98c21865ffa Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 15:12:18 +0930 Subject: [PATCH 191/617] test(config_management): Config Groups API ViewSet permission checks ref: #15 #248 #353 --- Release-Notes.md | 10 + .../test_config_groups_viewset.py | 181 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups/test_config_groups_viewset.py diff --git a/Release-Notes.md b/Release-Notes.md index 3566af2ed..cb973f061 100644 --- a/Release-Notes.md +++ b/Release-Notes.md @@ -2,6 +2,16 @@ API redesign in preparation for moving the UI out of centurion to it's [own project](https://github.com/nofusscomputing/centurion_erp_ui). This release introduces a **Feature freeze** to the current UI. Only bug fixes will be done for the current UI. +- A large emphasis is being placed upon API stability. This is being achieved by ensuring the following: + + - Actions can only be carried out by users whom have the correct permissions + + - fields are of the correct type and visible when required as part of the API response + + - Data validations work and notify the user of any issue + + We are make the above possible by ensuring a more stringent test policy. + - New API will be at path `api/v2` and will remain until v2.0.0 release of Centurion on which the `api/v2` path will be moved to `api` - API v1 is now **Feature frozen** with only bug fixes being completed. It's recommended that you move to and start using API v2 as this has feature parity with API v1. diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py b/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py new file mode 100644 index 000000000..d948a1e6c --- /dev/null +++ b/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py @@ -0,0 +1,181 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from config_management.models.groups import ConfigGroups + + + +class ConfigGroupsPermissionsAPI(TestCase, APIPermissions): + + model = ConfigGroups + + app_namespace = 'API' + + url_name = '_api_v2_config_group' + + change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + + # self.url_kwargs = {} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team_post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 518142492faccb076f27c0eb1b5cb292c6b3e5f8 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 16:04:20 +0930 Subject: [PATCH 192/617] test(config_management): Config Groups Serializer Validation checks ref: #15 #248 #353 --- .../test_config_groups_serializer.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups/test_config_groups_serializer.py diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py b/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py new file mode 100644 index 000000000..3f3b7fd5f --- /dev/null +++ b/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py @@ -0,0 +1,111 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from config_management.serializers.config_group import ConfigGroups, ConfigGroupModelSerializer + + + +class ConfigGroupsValidationAPI( + TestCase, +): + + model = ConfigGroups + + app_namespace = 'API' + + url_name = '_api_v2_knowledge_base' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item_no_parent = self.model.objects.create( + organization=organization, + name = 'random title', + config = { 'config_key': 'a value' } + ) + + self.item_has_parent = self.model.objects.create( + organization=organization, + name = 'random title two', + parent = self.item_no_parent + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ConfigGroupModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_update_existing_parnet_not_self(self): + """Serializer Validation Check + + Ensure that if an existing item is assigned itself as it's parent group + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = ConfigGroupModelSerializer( + self.item_has_parent, + data={ + "parent": self.item_has_parent.id + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['parent'][0] == 'self_not_parent' + + + def test_serializer_validation_update_existing_invalid_config_key(self): + """Serializer Validation Check + + Ensure that if an existing item has it's config updated with an invalid config key + a validation exception is raised. + """ + + invalid_config = self.item_no_parent.config.copy() + invalid_config.update({ 'software': 'is invalid' }) + + with pytest.raises(ValidationError) as err: + + serializer = ConfigGroupModelSerializer( + self.item_no_parent, + data={ + "config": invalid_config + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['config'][0] == 'invalid' From fae48f2c5af1bc25fef712754c4958fbb51b5ff5 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 16:05:34 +0930 Subject: [PATCH 193/617] fix(config_management): Config Groups Serializer Validation checks ref: #15 #248 #353 --- app/api/react_ui_metadata.py | 12 ++++++++ app/config_management/models/groups.py | 16 +++++----- .../serializers/config_group.py | 29 ++++++++++++++++--- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 2419eee80..07f326e2c 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -138,6 +138,18 @@ def determine_metadata(self, request, view): } ] }, + { + "display_name": "Config Management", + "name": "config_management", + "icon": "ansible", + "pages": [ + { + "display_name": "Groups", + "name": "config_group", + "link": "/config_management/group" + } + ] + }, { "display_name": "Settings", diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index b42572173..b9d78a6ad 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -56,12 +56,14 @@ class Meta: def validate_config_keys_not_reserved(self): - value: dict = self + if self is not None: - for invalid_key in ConfigGroups.reserved_config_keys: + value: dict = self - if invalid_key in value.keys(): - raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key') + for invalid_key in ConfigGroups.reserved_config_keys: + + if invalid_key in value.keys(): + raise ValidationError(f'json key "{invalid_key}" is a reserved configuration key') parent = models.ForeignKey( @@ -103,14 +105,14 @@ def validate_config_keys_not_reserved(self): "layout": "double", "left": [ 'organization', - 'name' + 'name', 'parent', - 'is_global', + 'is_global' ], "right": [ 'model_notes', 'created', - 'modified', + 'modified' ] }, { diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index dfc6e8ba2..3a154c0d6 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -129,17 +129,38 @@ def is_valid(self, *, raise_exception=True) -> bool: is_valid = super().is_valid(raise_exception=raise_exception) - if 'parent_group' in self._context['view'].kwargs: + if 'view' in self._context: + + if 'parent_group' in self._context['view'].kwargs: - self.validated_data['parent_id'] = int(self._context['view'].kwargs['parent_group']) + self.validated_data['parent_id'] = int(self._context['view'].kwargs['parent_group']) - organization = self.Meta.model.objects.get(pk = int(self._context['view'].kwargs['parent_group'])) + organization = self.Meta.model.objects.get(pk = int(self._context['view'].kwargs['parent_group'])) - self.validated_data['organization_id'] = organization.id + self.validated_data['organization_id'] = organization.id return is_valid + def validate(self, attrs): + + if self.instance: + + if hasattr(self.instance, 'parent_id') and 'parent' in self.initial_data: + + if self.initial_data['parent'] == self.instance.id: + + raise serializers.ValidationError( + detail = { + 'parent': 'Can not assign self as parent' + }, + code = 'self_not_parent' + ) + + return super().validate(attrs) + + + class ConfigGroupViewSerializer(ConfigGroupModelSerializer): parent = ConfigGroupBaseSerializer( read_only = True ) From a0ac0e4839a241558b9c43937549a8d532a126d5 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 18:22:49 +0930 Subject: [PATCH 194/617] refactor: update model fields ref: #353 --- ...ptions_alter_teamusers_options_and_more.py | 44 +++++++++++++++++++ ...03_alter_knowledgebase_options_and_more.py | 21 +++++++++ .../migrations/0007_configgroups_hosts.py | 19 ++++++++ 3 files changed, 84 insertions(+) create mode 100644 app/access/migrations/0004_alter_organization_options_alter_teamusers_options_and_more.py create mode 100644 app/assistance/migrations/0003_alter_knowledgebase_options_and_more.py create mode 100644 app/config_management/migrations/0007_configgroups_hosts.py diff --git a/app/access/migrations/0004_alter_organization_options_alter_teamusers_options_and_more.py b/app/access/migrations/0004_alter_organization_options_alter_teamusers_options_and_more.py new file mode 100644 index 000000000..51a3c7f3f --- /dev/null +++ b/app/access/migrations/0004_alter_organization_options_alter_teamusers_options_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.1.2 on 2024-10-16 06:54 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('access', '0003_alter_organization_id_alter_organization_manager_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name='organization', + options={'ordering': ['name'], 'verbose_name': 'Organization', 'verbose_name_plural': 'Organizations'}, + ), + migrations.AlterModelOptions( + name='teamusers', + options={'ordering': ['user'], 'verbose_name': 'Team User', 'verbose_name_plural': 'Team Users'}, + ), + migrations.AlterField( + model_name='teamusers', + name='id', + field=models.AutoField(help_text='ID of this Team User', primary_key=True, serialize=False, unique=True, verbose_name='ID'), + ), + migrations.AlterField( + model_name='teamusers', + name='manager', + field=models.BooleanField(blank=True, default=False, help_text='Is this user to be a manager of this team', verbose_name='manager'), + ), + migrations.AlterField( + model_name='teamusers', + name='team', + field=models.ForeignKey(help_text='Team user belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='team', to='access.team', verbose_name='Team'), + ), + migrations.AlterField( + model_name='teamusers', + name='user', + field=models.ForeignKey(help_text='User who will be added to the team', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/app/assistance/migrations/0003_alter_knowledgebase_options_and_more.py b/app/assistance/migrations/0003_alter_knowledgebase_options_and_more.py new file mode 100644 index 000000000..5c9f65ea8 --- /dev/null +++ b/app/assistance/migrations/0003_alter_knowledgebase_options_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2024-10-16 06:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assistance', '0002_remove_knowledgebasecategory_slug_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='knowledgebase', + options={'ordering': ['title'], 'verbose_name': 'Knowledge Base', 'verbose_name_plural': 'Knowledge Base Articles'}, + ), + migrations.AlterModelOptions( + name='knowledgebasecategory', + options={'ordering': ['name'], 'verbose_name': 'Knowledge Base Category', 'verbose_name_plural': 'Knowledge Base Categories'}, + ), + ] diff --git a/app/config_management/migrations/0007_configgroups_hosts.py b/app/config_management/migrations/0007_configgroups_hosts.py new file mode 100644 index 000000000..d2215e7ca --- /dev/null +++ b/app/config_management/migrations/0007_configgroups_hosts.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2024-10-16 06:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config_management', '0006_alter_configgrouphosts_group_and_more'), + ('itam', '0015_alter_device_device_model_alter_device_device_type_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='configgroups', + name='hosts', + field=models.ManyToManyField(blank=True, help_text='Hosts that are part of this group', to='itam.device', verbose_name='Hosts'), + ), + ] From 8219bf6c9dc5280cf5eb3e615c00e7ac89f039a7 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 18:23:12 +0930 Subject: [PATCH 195/617] chore(vscode): add debug for migrations ref: #353 --- .vscode/launch.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 40ec49b21..773849209 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug: Django", + "name": "Centurion", "type": "debugpy", "request": "launch", "args": [ @@ -17,6 +17,7 @@ "program": "${workspaceFolder}/app/manage.py" }, { + "name": "Debug: Gunicorn", "type": "debugpy", "request": "launch", @@ -35,9 +36,21 @@ "autoStartBrowser": false, "cwd": "${workspaceFolder}/app" }, + { + "name": "Migrate", + "type": "debugpy", + "request": "launch", + "args": [ + "migrate" + ], + "django": true, + "autoStartBrowser": false, + "program": "${workspaceFolder}/app/manage.py" + + }, { "name": "Debug: Celery", - "type": "python", + "type": "debugpy", "request": "launch", "module": "celery", "console": "integratedTerminal", From 0c9a9f5ae1bf542f49f61c8219baa31f1dcdf772 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 18:49:08 +0930 Subject: [PATCH 196/617] refactor(config_management): move config_group_hosts to related table ref: #353 --- app/api/serializers/config.py | 1 + app/api/serializers/itam/device.py | 8 +- app/config_management/forms/group_hosts.py | 21 ----- .../0008_move_data_configgroup_hosts.py | 75 +++++++++++++++++ app/config_management/models/groups.py | 7 ++ .../serializers/config_group.py | 7 +- .../templates/config_management/group.html.j2 | 12 ++- app/config_management/urls.py | 5 -- app/config_management/views/groups/groups.py | 84 +------------------ app/itam/templates/itam/device.html.j2 | 10 +-- app/itam/tests/unit/device/test_device_api.py | 75 ----------------- app/itam/views/device.py | 4 - 12 files changed, 101 insertions(+), 208 deletions(-) delete mode 100644 app/config_management/forms/group_hosts.py create mode 100644 app/config_management/migrations/0008_move_data_configgroup_hosts.py diff --git a/app/api/serializers/config.py b/app/api/serializers/config.py index f190a1bb2..84b74325f 100644 --- a/app/api/serializers/config.py +++ b/app/api/serializers/config.py @@ -74,6 +74,7 @@ class Meta: 'parent', 'name', 'config', + 'hosts', 'url', ] read_only_fields = [ diff --git a/app/api/serializers/itam/device.py b/app/api/serializers/itam/device.py index 1001c4872..3c0760ecb 100644 --- a/app/api/serializers/itam/device.py +++ b/app/api/serializers/itam/device.py @@ -4,7 +4,7 @@ from api.serializers.config import ParentGroupSerializer -from config_management.models.groups import ConfigGroupHosts +from config_management.models.groups import ConfigGroups from itam.models.device import Device @@ -12,15 +12,13 @@ class DeviceConfigGroupsSerializer(serializers.ModelSerializer): - name = serializers.CharField(source='group.name', read_only=True) - url = serializers.HyperlinkedIdentityField( view_name="API:_api_config_group", format="html" ) class Meta: - model = ConfigGroupHosts + model = ConfigGroups fields = [ 'id', @@ -43,7 +41,7 @@ class DeviceSerializer(serializers.ModelSerializer): config = serializers.SerializerMethodField('get_device_config') - groups = DeviceConfigGroupsSerializer(source='configgrouphosts_set', many=True, read_only=True) + groups = DeviceConfigGroupsSerializer(source='configgroups_set', many=True, read_only=True) def get_device_config(self, device): diff --git a/app/config_management/forms/group_hosts.py b/app/config_management/forms/group_hosts.py deleted file mode 100644 index 765091cc0..000000000 --- a/app/config_management/forms/group_hosts.py +++ /dev/null @@ -1,21 +0,0 @@ -from itam.models.device import Device - -from config_management.models.groups import ConfigGroups, ConfigGroupHosts - -from core.forms.common import CommonModelForm - - - -class ConfigGroupHostsForm(CommonModelForm): - - __name__ = 'asdsa' - - class Meta: - - fields = [ - 'host' - ] - - model = ConfigGroupHosts - - prefix = 'config_group_hosts' diff --git a/app/config_management/migrations/0008_move_data_configgroup_hosts.py b/app/config_management/migrations/0008_move_data_configgroup_hosts.py new file mode 100644 index 000000000..2a3bafe8a --- /dev/null +++ b/app/config_management/migrations/0008_move_data_configgroup_hosts.py @@ -0,0 +1,75 @@ +# Generated by Django 5.1.2 on 2024-10-16 06:54 + +from django.db import migrations, models + + +def migrate_to_configgroups_hosts(apps, schema_editor): + + if schema_editor.connection.alias != "default": + + return + + print('') + + ConfigGroups = apps.get_model('config_management', 'ConfigGroups') + ConfigGroupHosts = apps.get_model('config_management', 'ConfigGroupHosts') + + current_data = ConfigGroupHosts.objects.all() + + for host in current_data: + + print(f'Begin migrating host {host.host} in group {host.group}:') + + config_group = ConfigGroups.objects.get(pk = host.group.id) + + print(f' migrate {host.host} in group {config_group}') + + config_group.hosts.add( host.host ) + + try: + + was_migrated = ConfigGroups.objects.get(pk = host.group.id) + + if host.host in was_migrated.hosts.all(): + + print(f' successfully migrated {host.id} {host.host} to new table') + + ConfigGroupHosts.objects.get(pk = host.id).delete() + + try: + + ConfigGroupHosts.objects.get(pk = host.id) + + print(f' Error Failed to remove old data for host {host.host}') + + except ConfigGroupHosts.DoesNotExist: + + print(f' Old data removed') + + except ConfigGroupHosts.DoesNotExist: + + print(f' Error, {host.host} was not migrated to new table') + + + old_data = ConfigGroupHosts.objects.all() + + if len(old_data) == 0: + + print(f'Successfully migrated data to new table, removing old table') + + migrations.DeleteModel("ConfigGroupHosts") + + + + + + +class Migration(migrations.Migration): + + dependencies = [ + ('config_management', '0007_configgroups_hosts'), + ] + + operations = [ + migrations.RunPython(migrate_to_configgroups_hosts), + ] diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index b9d78a6ad..ca8908457 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -95,6 +95,13 @@ def validate_config_keys_not_reserved(self): verbose_name = 'Configuration' ) + hosts = models.ManyToManyField( + to = Device, + blank = True, + help_text = 'Hosts that are part of this group', + verbose_name = 'Hosts' + ) + page_layout: dict = [ { diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 3a154c0d6..70b86bcde 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -4,11 +4,9 @@ from access.serializers.organization import OrganizationBaseSerializer -from app.serializers.user import UserBaseSerializer - from config_management.models.groups import ConfigGroups - +from api.v2.serializers.itam.device import DeviceModelBaseSerializer class ConfigGroupBaseSerializer(serializers.ModelSerializer): @@ -94,6 +92,7 @@ class Meta: 'name', 'model_notes', 'config', + 'hosts', 'is_global', 'created', 'modified', @@ -163,6 +162,8 @@ def validate(self, attrs): class ConfigGroupViewSerializer(ConfigGroupModelSerializer): + hosts = DeviceModelBaseSerializer(read_only = True, many = True) + parent = ConfigGroupBaseSerializer( read_only = True ) organization = OrganizationBaseSerializer( many=False, read_only=True ) diff --git a/app/config_management/templates/config_management/group.html.j2 b/app/config_management/templates/config_management/group.html.j2 index 6c9000980..a5c3ce99c 100644 --- a/app/config_management/templates/config_management/group.html.j2 +++ b/app/config_management/templates/config_management/group.html.j2 @@ -48,20 +48,18 @@ {% include 'content/section.html.j2' with tab=form.tabs.hosts %} - - - {% if config_group_hosts %} - {% for host in config_group_hosts %} + {% if group.hosts %} + {% for host in group.hosts.all %} - - - + + + {% endfor %} {% else %} diff --git a/app/config_management/urls.py b/app/config_management/urls.py index 287720d2c..0af94aa8f 100644 --- a/app/config_management/urls.py +++ b/app/config_management/urls.py @@ -1,10 +1,8 @@ from django.urls import path from config_management.views.groups import groups -from config_management.views.groups.groups import GroupHostAdd, GroupHostDelete from config_management.views.groups import software -# from config_management.views.groups.software import GroupSoftwareAdd, GroupSoftwareChange, GroupSoftwareDelete app_name = "Config Management" @@ -21,7 +19,4 @@ path("group//software/", software.Change.as_view(), name="_group_software_change"), path("group//software//delete", software.Delete.as_view(), name="_group_software_delete"), - path('group//host', GroupHostAdd.as_view(), name='_group_add_host'), - path('group//host//delete', GroupHostDelete.as_view(), name='_group_delete_host'), - ] diff --git a/app/config_management/views/groups/groups.py b/app/config_management/views/groups/groups.py index 7d22c6383..9c14a5afa 100644 --- a/app/config_management/views/groups/groups.py +++ b/app/config_management/views/groups/groups.py @@ -13,9 +13,8 @@ from settings.models.user_settings import UserSettings -from config_management.forms.group_hosts import ConfigGroupHostsForm from config_management.forms.group.group import ConfigGroupForm, DetailForm -from config_management.models.groups import ConfigGroups, ConfigGroupHosts, ConfigGroupSoftware +from config_management.models.groups import ConfigGroups, ConfigGroupSoftware @@ -158,7 +157,6 @@ def get_context_data(self, **kwargs): context['config'] = json.dumps(json.loads(self.object.render_config()), indent=4, sort_keys=True) - context['config_group_hosts'] = ConfigGroupHosts.objects.filter(group_id = self.kwargs['pk']).order_by('-host') context['tickets'] = TicketLinkedItem.objects.filter( item = int(self.kwargs['pk']), @@ -245,83 +243,3 @@ def get_context_data(self, **kwargs): def get_success_url(self, **kwargs): return reverse('Config Management:Groups') - - - -class GroupHostAdd(AddView): - - model = ConfigGroupHosts - - parent_model = ConfigGroups - - permission_required = [ - 'config_management.add_configgrouphosts', - ] - - template_name = 'form.html.j2' - - form_class = ConfigGroupHostsForm - - - def form_valid(self, form): - - form.instance.group_id = self.kwargs['pk'] - - form.instance.organization = self.parent_model.objects.get(pk=form.instance.group_id).organization - - return super().form_valid(form) - - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - context['content_title'] = 'Add Host to Group' - - return context - - - def get_form(self, form_class=None): - - form_class = super().get_form(form_class=None) - - group = ConfigGroups.objects.get(pk=self.kwargs['pk']) - - exsting_group_hosts = ConfigGroupHosts.objects.filter(group=group) - - form_class.fields["host"].queryset = form_class.fields["host"].queryset.filter( - ).exclude( - id__in=exsting_group_hosts.values_list('host', flat=True) - ) - - - return form_class - - - def get_success_url(self, **kwargs): - - return reverse('Config Management:_group_view', args=[self.kwargs['pk'],]) - - - -class GroupHostDelete(DeleteView): - - model = ConfigGroupHosts - - permission_required = [ - 'config_management.delete_configgrouphosts', - ] - - template_name = 'form.html.j2' - - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - context['content_title'] = 'Delete ' + self.object.host.name - - return context - - - def get_success_url(self, **kwargs): - - return reverse('Config Management:_group_view', args=[self.kwargs['group_id'],]) diff --git a/app/itam/templates/itam/device.html.j2 b/app/itam/templates/itam/device.html.j2 index 6cc499e93..fffbd281c 100644 --- a/app/itam/templates/itam/device.html.j2 +++ b/app/itam/templates/itam/device.html.j2 @@ -196,17 +196,17 @@ - {% if config_groups %} - {% for group in config_groups %} + {% if device.configgroups_set %} + {% for group in device.configgroups_set.all %} - + - + {% endfor %} {% else %} - + {% endif %}
Name Organization  
{{ host.host }}{{ host.host.organization }}Delete{{ host }}{{ host.organization }} 
Added  
{{ group.group }}{{ group }} {{ group.created }}Delete 
Nothing FoundNothing Found.
diff --git a/app/itam/tests/unit/device/test_device_api.py b/app/itam/tests/unit/device/test_device_api.py index a340f2025..34c03bb9d 100644 --- a/app/itam/tests/unit/device/test_device_api.py +++ b/app/itam/tests/unit/device/test_device_api.py @@ -374,24 +374,6 @@ def test_api_field_type_modified(self): assert type(self.api_data['modified']) is str - def test_api_field_exists_groups(self): - """ Test for existance of API Field - - groups field must exist - """ - - assert 'groups' in self.api_data - - - def test_api_field_type_groups(self): - """ Test for type for API Field - - groups field must be list - """ - - assert type(self.api_data['groups']) is list - - def test_api_field_exists_organization(self): """ Test for existance of API Field @@ -428,63 +410,6 @@ def test_api_field_type_url(self): assert type(self.api_data['url']) is Hyperlink - - - def test_api_field_exists_groups_id(self): - """ Test for existance of API Field - - groups.id field must exist - """ - - assert 'id' in self.api_data['groups'][0] - - - def test_api_field_type_groups_id(self): - """ Test for type for API Field - - groups.id field must be int - """ - - assert type(self.api_data['groups'][0]['id']) is int - - - def test_api_field_exists_groups_name(self): - """ Test for existance of API Field - - groups.name field must exist - """ - - assert 'name' in self.api_data['groups'][0] - - - def test_api_field_type_groups_name(self): - """ Test for type for API Field - - groups.name field must be str - """ - - assert type(self.api_data['groups'][0]['name']) is str - - - def test_api_field_exists_groups_url(self): - """ Test for existance of API Field - - groups.url field must exist - """ - - assert 'url' in self.api_data['groups'][0] - - - def test_api_field_type_groups_url(self): - """ Test for type for API Field - - groups.url field must be str - """ - - assert type(self.api_data['groups'][0]['url']) is Hyperlink - - - def test_api_create_device_existing_uuid_matches_status_200(self): """Creation of existing device diff --git a/app/itam/views/device.py b/app/itam/views/device.py index d2f8fb3ea..d29a74ec1 100644 --- a/app/itam/views/device.py +++ b/app/itam/views/device.py @@ -8,9 +8,6 @@ from access.models import Organization -from config_management.models.groups import ConfigGroupHosts - - from ..models.device import Device, DeviceSoftware, DeviceOperatingSystem from ..models.software import Software @@ -143,7 +140,6 @@ def get_context_data(self, **kwargs): config = self.object.get_configuration context['config'] = json.dumps(config, indent=4, sort_keys=True) - context['config_groups'] = ConfigGroupHosts.objects.filter(host = self.object.id) context['model_pk'] = self.kwargs['pk'] context['model_name'] = self.model._meta.verbose_name.replace(' ', '') From 7d35db030c98695e835bbba18c0b793331423d1f Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 16 Oct 2024 18:54:04 +0930 Subject: [PATCH 197/617] refactor(itam): rename dir viewset -> viewsets ref: #248 #353 --- app/api/urls.py | 2 +- app/config_management/serializers/config_group.py | 4 ---- app/itam/tests/unit/test_itam_viewset.py | 2 +- app/itam/{viewset => viewsets}/index.py | 0 4 files changed, 2 insertions(+), 6 deletions(-) rename app/itam/{viewset => viewsets}/index.py (100%) diff --git a/app/api/urls.py b/app/api/urls.py index 507aa5697..1a05df302 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -58,7 +58,7 @@ config_group as config_group_v2 ) -from itam.viewset import ( +from itam.viewsets import ( index as itam_index_v2, ) diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 70b86bcde..bafd8dcec 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -6,8 +6,6 @@ from config_management.models.groups import ConfigGroups -from api.v2.serializers.itam.device import DeviceModelBaseSerializer - class ConfigGroupBaseSerializer(serializers.ModelSerializer): @@ -162,8 +160,6 @@ def validate(self, attrs): class ConfigGroupViewSerializer(ConfigGroupModelSerializer): - hosts = DeviceModelBaseSerializer(read_only = True, many = True) - parent = ConfigGroupBaseSerializer( read_only = True ) organization = OrganizationBaseSerializer( many=False, read_only=True ) diff --git a/app/itam/tests/unit/test_itam_viewset.py b/app/itam/tests/unit/test_itam_viewset.py index 1387f33c2..ae28710fd 100644 --- a/app/itam/tests/unit/test_itam_viewset.py +++ b/app/itam/tests/unit/test_itam_viewset.py @@ -6,7 +6,7 @@ from api.tests.abstract.viewsets import ViewSetCommon -from itam.viewset.index import Index +from itam.viewsets.index import Index class ItamViewset( diff --git a/app/itam/viewset/index.py b/app/itam/viewsets/index.py similarity index 100% rename from app/itam/viewset/index.py rename to app/itam/viewsets/index.py From 976a5f0706f2273832e7880a2491df66259eba35 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 13:09:41 +0930 Subject: [PATCH 198/617] refactor(itam): Software Action field changed char -> integer ref: #248 #353 --- .../0009_alter_configgroupsoftware_action.py | 18 ++++++++++++++++++ app/config_management/models/groups.py | 3 +-- ...test_config_groups_software_core_history.py | 2 +- .../0016_alter_devicesoftware_action.py | 18 ++++++++++++++++++ app/itam/models/device.py | 9 ++++----- 5 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 app/config_management/migrations/0009_alter_configgroupsoftware_action.py create mode 100644 app/itam/migrations/0016_alter_devicesoftware_action.py diff --git a/app/config_management/migrations/0009_alter_configgroupsoftware_action.py b/app/config_management/migrations/0009_alter_configgroupsoftware_action.py new file mode 100644 index 000000000..48e3a9170 --- /dev/null +++ b/app/config_management/migrations/0009_alter_configgroupsoftware_action.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2024-10-17 03:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config_management', '0008_move_data_configgroup_hosts'), + ] + + operations = [ + migrations.AlterField( + model_name='configgroupsoftware', + name='action', + field=models.IntegerField(blank=True, choices=[(1, 'Install'), (0, 'Remove')], default=None, help_text='ACtion to perform with this software', null=True, verbose_name='Action'), + ), + ] diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index ca8908457..d49eabb12 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -417,12 +417,11 @@ class Meta: ) - action = models.CharField( + action = models.IntegerField( blank = True, choices=DeviceSoftware.Actions, default=None, help_text = 'ACtion to perform with this software', - max_length=1, null=True, verbose_name = 'Action' ) diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py index bce034a8c..d233b7231 100644 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py @@ -62,7 +62,7 @@ def setUpTestData(self): self.item_change.action = DeviceSoftware.Actions.REMOVE self.item_change.save() - self.field_after_expected_value = '{"action": "' + DeviceSoftware.Actions.REMOVE + '"}' + self.field_after_expected_value = '{"action": "' + str(DeviceSoftware.Actions.REMOVE) + '"}' self.history_change = History.objects.get( action = History.Actions.UPDATE[0], diff --git a/app/itam/migrations/0016_alter_devicesoftware_action.py b/app/itam/migrations/0016_alter_devicesoftware_action.py new file mode 100644 index 000000000..8c305ccff --- /dev/null +++ b/app/itam/migrations/0016_alter_devicesoftware_action.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2024-10-17 03:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0015_alter_device_device_model_alter_device_device_type_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='devicesoftware', + name='action', + field=models.IntegerField(blank=True, choices=[(1, 'Install'), (0, 'Remove')], default=None, help_text='Action to perform', null=True, verbose_name='Action'), + ), + ] diff --git a/app/itam/models/device.py b/app/itam/models/device.py index db4f72013..5c07b0a48 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -499,9 +499,9 @@ class Meta: - class Actions(models.TextChoices): - INSTALL = '1', 'Install' - REMOVE = '0', 'Remove' + class Actions(models.IntegerChoices): + INSTALL = 1, 'Install' + REMOVE = 0, 'Remove' device = models.ForeignKey( @@ -522,12 +522,11 @@ class Actions(models.TextChoices): verbose_name = 'Software' ) - action = models.CharField( + action = models.IntegerField( blank = True, choices=Actions, default=None, help_text = 'Action to perform', - max_length=1, null=True, verbose_name = 'Action', ) From cb6f15b9334f0fec422cca6bba6f7cf3ef32729a Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 13:55:38 +0930 Subject: [PATCH 199/617] refactor(config_management): Adjust rendered config str -> dict ref: #248 #353 --- .vscode/settings.json | 1 + app/config_management/models/groups.py | 6 +++--- .../tests/unit/config_groups/test_config_groups.py | 2 +- app/config_management/views/groups/groups.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 83a6c5114..4687599b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,4 +17,5 @@ "ITSM" ], "cSpell.language": "en-AU", + "jest.enable": false, } \ No newline at end of file diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index d49eabb12..7b324b308 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -245,13 +245,13 @@ def parent_object(self): return self.parent - def render_config(self) -> str: + def render_config(self): config: dict = dict() if self.parent: - config.update(json.loads(ConfigGroups.objects.get(id=self.parent.id).render_config())) + config.update(ConfigGroups.objects.get(id=self.parent.id).render_config()) if self.config: @@ -294,7 +294,7 @@ def render_config(self) -> str: config['software'] = merge_software(config['software'], software_actions['software']) - return json.dumps(config) + return config diff --git a/app/config_management/tests/unit/config_groups/test_config_groups.py b/app/config_management/tests/unit/config_groups/test_config_groups.py index 697e63356..916f0706a 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups.py @@ -61,7 +61,7 @@ def test_config_groups_rendered_config_not_empty(self): def test_config_groups_rendered_config_is_dict(self): """ Rendered Config is a string """ - assert type(self.item.render_config()) is str + assert type(self.item.render_config()) is dict def test_config_groups_rendered_config_is_correct(self): diff --git a/app/config_management/views/groups/groups.py b/app/config_management/views/groups/groups.py index 9c14a5afa..1d8750cd5 100644 --- a/app/config_management/views/groups/groups.py +++ b/app/config_management/views/groups/groups.py @@ -155,7 +155,7 @@ def get_context_data(self, **kwargs): context['child_groups'] = ConfigGroups.objects.filter(parent=self.kwargs['pk']) - context['config'] = json.dumps(json.loads(self.object.render_config()), indent=4, sort_keys=True) + context['config'] = json.dumps(self.object.render_config(), indent=4, sort_keys=True) context['tickets'] = TicketLinkedItem.objects.filter( From 3482b7dd0ac305dc2514f2f661ac6d960dd303be Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 15:09:23 +0930 Subject: [PATCH 200/617] feat(config_management): Add Config Group Software API v2 endpoint ref: #248 #348 --- app/api/urls.py | 4 +- app/config_management/models/groups.py | 1 - .../serializers/config_group.py | 7 + .../serializers/config_group_software.py | 211 ++++++++++++++++++ .../viewsets/config_group_software.py | 100 +++++++++ 5 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 app/config_management/serializers/config_group_software.py create mode 100644 app/config_management/viewsets/config_group_software.py diff --git a/app/api/urls.py b/app/api/urls.py index 1a05df302..3f4feaf02 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -55,7 +55,8 @@ from config_management.viewsets import ( index as config_management_v2, - config_group as config_group_v2 + config_group as config_group_v2, + config_group_software as config_group_software_v2 ) from itam.viewsets import ( @@ -129,6 +130,7 @@ router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') router.register('v2/config_management/group', config_group_v2.ViewSet, basename='_api_v2_config_group') router.register('v2/config_management/group/(?P[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child') +router.register('v2/config_management/group/(?P[0-9]+)/software', config_group_software_v2.ViewSet, basename='_api_v2_config_group_software') router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 7b324b308..060414d39 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -1,4 +1,3 @@ -import json import re from django.db import models diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index bafd8dcec..32805af6d 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -67,6 +67,13 @@ def get_url(self, item): 'API:_api_v2_config_group-list', request = self.context['view'].request, ), + 'group_software': reverse( + 'API:_api_v2_config_group_software-list', + request=self.context['view'].request, + kwargs = { + 'group_id': item.pk + } + ), 'organization': reverse( 'API:_api_v2_organization-list', request=self.context['view'].request, diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py new file mode 100644 index 000000000..e9f7c6e77 --- /dev/null +++ b/app/config_management/serializers/config_group_software.py @@ -0,0 +1,211 @@ +from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.reverse import reverse + +from access.serializers.organization import OrganizationBaseSerializer + +from config_management.models.groups import ConfigGroupSoftware +from config_management.serializers.config_group import ConfigGroups, ConfigGroupBaseSerializer + + + +class ConfigGroupSoftwareBaseSerializer(serializers.ModelSerializer): + + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_config_group-detail", + request=self.context['view'].request, + kwargs={ + 'group_id': item.config_group.pk, + 'pk': item.pk + } + ) + + + class Meta: + + model = ConfigGroupSoftware + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class ConfigGroupSoftwareModelSerializer(ConfigGroupSoftwareBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + 'API:_api_v2_config_group_software-detail', + request = self.context['view'].request, + kwargs = { + 'group_id': item.config_group.pk, + 'pk': item.pk + } + ), + 'organization': reverse( + 'API:_api_v2_organization-list', + request=self.context['view'].request, + ), + } + + + class Meta: + + model = ConfigGroupSoftware + + fields = '__all__' + + fields = [ + 'id', + 'display_name', + 'organization', + 'config_group', + 'software', + 'action', + 'version', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + def get_field_names(self, declared_fields, info): + + fields = self.Meta.fields + + if 'view' in self._context: + + if 'group_id' in self._context['view'].kwargs: + + self.Meta.read_only_fields += [ + 'organization', + 'config_group' + ] + + return fields + + + def is_valid(self, *, raise_exception=False) -> bool: + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'view' in self._context: + + if 'group_id' in self._context['view'].kwargs: + + self.validated_data['config_group_id'] = int(self._context['view'].kwargs['group_id']) + + parent_item = ConfigGroups.objects.get(pk = int(self._context['view'].kwargs['group_id'])) + + self.validated_data['organization_id'] = parent_item.id + + return is_valid + + + def validate(self, attrs): + + if 'software' in self.initial_data: + + try: + + try: + + current_object = self.Meta.model.objects.get( software_id = self.initial_data['software'] ) + + except self.Meta.model.MultipleObjectsReturned: + + pass # Although an exception, the item still exists + + raise serializers.ValidationError( + detail = { + 'software': 'This software is already assigned to this group' + }, + code = 'software_exists' + ) + + except self.Meta.model.DoesNotExist as exc: + + pass + + + if 'version' in self.initial_data: + + if self.initial_data['version']: + + try: + + current_object = SoftwareVersion.objects.get( pk = self.initial_data['version'] ) + + + if 'software' in self.initial_data: + + software = self.initial_data['software'] + + elif self.instance: + + software = self.instance.software + + + if software != current_object.software: + + raise serializers.ValidationError( + detail = { + 'version': 'This version does not belong to selected software' + }, + code = 'software_not_own_version' + ) + + except self.Meta.model.DoesNotExist as exc: + + raise serializers.ValidationError( + detail = { + 'version': 'Software version does not exist' + }, + code = 'version_absent' + ) + + + return super().validate(attrs) + + + +class ConfigGroupSoftwareViewSerializer(ConfigGroupSoftwareModelSerializer): + + config_group = ConfigGroupBaseSerializer(read_only = True ) + + organization = OrganizationBaseSerializer( many=False, read_only=True ) diff --git a/app/config_management/viewsets/config_group_software.py b/app/config_management/viewsets/config_group_software.py new file mode 100644 index 000000000..9aac78dac --- /dev/null +++ b/app/config_management/viewsets/config_group_software.py @@ -0,0 +1,100 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from config_management.serializers.config_group_software import ( + ConfigGroupSoftware, + ConfigGroupSoftwareModelSerializer, + ConfigGroupSoftwareViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a config group software', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=ConfigGroupSoftwareViewSerializer), + 201: OpenApiResponse(description='Created', response=ConfigGroupSoftwareViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a config group software', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all config group softwares', + description='', + responses = { + 200: OpenApiResponse(description='', response=ConfigGroupSoftwareViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single config group software', + description='', + responses = { + 200: OpenApiResponse(description='', response=ConfigGroupSoftwareViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a config group software', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ConfigGroupSoftwareViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'software', + 'is_global', + ] + + + model = ConfigGroupSoftware + + documentation: str = '' + + view_description = 'Software for a config group' + + + def get_queryset(self): + + if 'group_id' in self.kwargs: + + self.queryset = super().get_queryset().filter(config_group = self.kwargs['group_id']) + + else: + + self.queryset = super().get_queryset() + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + From bd11d82107e15d218a0b3acd75f6f618c6a481a7 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 15:11:19 +0930 Subject: [PATCH 201/617] test(config_management): Config Groups Software Serializer Validation checks ref: #15 #248 #353 --- .../test_config_groups_serializer.py | 4 - .../test_config_groups_software_serializer.py | 192 ++++++++++++++++++ 2 files changed, 192 insertions(+), 4 deletions(-) create mode 100644 app/config_management/tests/unit/config_groups_software/test_config_groups_software_serializer.py diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py b/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py index 3f3b7fd5f..3950cfbe7 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups_serializer.py @@ -16,10 +16,6 @@ class ConfigGroupsValidationAPI( model = ConfigGroups - app_namespace = 'API' - - url_name = '_api_v2_knowledge_base' - @classmethod def setUpTestData(self): """Setup Test diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_serializer.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_serializer.py new file mode 100644 index 000000000..54bc9193d --- /dev/null +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_serializer.py @@ -0,0 +1,192 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from config_management.serializers.config_group_software import ConfigGroupSoftware, ConfigGroupSoftwareModelSerializer +from config_management.models.groups import ConfigGroups + +from itam.models.software import Software, SoftwareVersion + + + +class ConfigGroupSoftwareValidationAPI( + TestCase, +): + + model = ConfigGroupSoftware + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. create software + 3. create software version + 4. create config group + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.software = Software.objects.create( + organization=organization, + name = 'software config' + ) + + self.software_version = SoftwareVersion.objects.create( + organization=organization, + software = self.software, + name = '1.2.2' + ) + + + + self.software_two = Software.objects.create( + organization=organization, + name = 'software config two' + ) + + self.software_version_two = SoftwareVersion.objects.create( + organization=organization, + software = self.software_two, + name = '1.2.3' + ) + + + + self.config_group = ConfigGroups.objects.create( + organization=organization, + name = 'random title', + config = { 'config_key': 'a value' } + ) + + self.config_group_already_has_software = ConfigGroups.objects.create( + organization=organization, + name = 'random title', + config = { 'config_key': 'a value' } + ) + + self.config_group_software = ConfigGroupSoftware.objects.create( + organization=organization, + config_group = self.config_group_already_has_software, + software = self.software, + version = self.software_version + ) + + + + # def test_serializer_validation_no_name(self): + # """Serializer Validation Check + + # Ensure that if creating and no name is provided a validation error occurs + # """ + + # with pytest.raises(ValidationError) as err: + + # serializer = ConfigGroupModelSerializer(data={ + # "organization": self.organization.id, + # }) + + # serializer.is_valid(raise_exception = True) + + # assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_update_existing_software_add_same(self): + """Serializer Validation Check + + Ensure that if an existing item is already assigned a piece of software + and an attempt to reassign the same software it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = ConfigGroupSoftwareModelSerializer( + self.config_group_software, + data={ + "software": self.software.id + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['software'][0] == 'software_exists' + + + + def test_serializer_validation_update_version_not_exist(self): + """Serializer Validation Check + + Ensure that if an existing item is assigned a piece of software that doesn't + exist, it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = ConfigGroupSoftwareModelSerializer( + self.config_group_software, + data={ + "version": 55 + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['version'][0] == 'does_not_exist' + + + + def test_serializer_validation_update_version_from_other_software(self): + """Serializer Validation Check + + Ensure that if an existing item is assigned a piece of software is assigned a + software version from a different piece of software, it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = ConfigGroupSoftwareModelSerializer( + self.config_group_software, + data={ + "version": self.software_version_two.id + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['version'][0] == 'software_not_own_version' + + + # def test_serializer_validation_update_existing_invalid_config_key(self): + # """Serializer Validation Check + + # Ensure that if an existing item has it's config updated with an invalid config key + # a validation exception is raised. + # """ + + # invalid_config = self.item_no_parent.config.copy() + # invalid_config.update({ 'software': 'is invalid' }) + + # with pytest.raises(ValidationError) as err: + + # serializer = ConfigGroupModelSerializer( + # self.item_no_parent, + # data={ + # "config": invalid_config + # }, + # partial=True, + # ) + + # serializer.is_valid(raise_exception = True) + + # assert err.value.get_codes()['config'][0] == 'invalid' From 99550e7ab36afb5945d4229c64f9196b4685946b Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 15:29:00 +0930 Subject: [PATCH 202/617] feat(itam): Add Software Base Serializer ref: #248 #348 --- app/app/settings.py | 4 +-- .../serializers/config_group_software.py | 4 +++ app/itam/serializers/software.py | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 app/itam/serializers/software.py diff --git a/app/app/settings.py b/app/app/settings.py index 404871920..ed682d021 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -191,7 +191,7 @@ }, ] -LOGIN_REDIRECT_URL = "home" +LOGIN_REDIRECT_URL = "http://127.0.0.1:3000" LOGOUT_REDIRECT_URL = "login" LOGIN_URL = '/account/login' @@ -309,7 +309,7 @@ """, 'VERSION': '1.0.0', - 'SCHEMA_PATH_PREFIX': '/api/v2/([a-z]+)|/api', + 'SCHEMA_PATH_PREFIX': '/api/v2/([a-z_]+)/|/api/', 'SERVE_INCLUDE_SCHEMA': False, 'SWAGGER_UI_DIST': 'SIDECAR', diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py index e9f7c6e77..2b2531e67 100644 --- a/app/config_management/serializers/config_group_software.py +++ b/app/config_management/serializers/config_group_software.py @@ -7,6 +7,8 @@ from config_management.models.groups import ConfigGroupSoftware from config_management.serializers.config_group import ConfigGroups, ConfigGroupBaseSerializer +from itam.serializers.software import SoftwareBaseSerializer + class ConfigGroupSoftwareBaseSerializer(serializers.ModelSerializer): @@ -209,3 +211,5 @@ class ConfigGroupSoftwareViewSerializer(ConfigGroupSoftwareModelSerializer): config_group = ConfigGroupBaseSerializer(read_only = True ) organization = OrganizationBaseSerializer( many=False, read_only=True ) + + software = SoftwareBaseSerializer( read_only = True ) diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py new file mode 100644 index 000000000..0d6318a0a --- /dev/null +++ b/app/itam/serializers/software.py @@ -0,0 +1,32 @@ +from rest_framework import serializers +from rest_framework.reverse import reverse + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.models.software import Software + + + +class SoftwareBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + class Meta: + + model = Software + + fields = [ + 'id', + 'display_name', + 'name', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + ] From 5ab7ce05bc4c0e9115aec6df3138b61ca421b18a Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 15:30:19 +0930 Subject: [PATCH 203/617] feat(itam): Add Software Version Base Serializer ref: #248 #348 --- .../serializers/config_group_software.py | 3 ++ app/itam/serializers/software_version.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 app/itam/serializers/software_version.py diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py index 2b2531e67..96bcf722b 100644 --- a/app/config_management/serializers/config_group_software.py +++ b/app/config_management/serializers/config_group_software.py @@ -8,6 +8,7 @@ from config_management.serializers.config_group import ConfigGroups, ConfigGroupBaseSerializer from itam.serializers.software import SoftwareBaseSerializer +from itam.serializers.software_version import SoftwareVersion, SoftwareVersionBaseSerializer @@ -213,3 +214,5 @@ class ConfigGroupSoftwareViewSerializer(ConfigGroupSoftwareModelSerializer): organization = OrganizationBaseSerializer( many=False, read_only=True ) software = SoftwareBaseSerializer( read_only = True ) + + version = SoftwareVersionBaseSerializer( read_only = True ) diff --git a/app/itam/serializers/software_version.py b/app/itam/serializers/software_version.py new file mode 100644 index 000000000..db58aea7c --- /dev/null +++ b/app/itam/serializers/software_version.py @@ -0,0 +1,31 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer +from itam.models.software import SoftwareVersion + + + +class SoftwareVersionBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + class Meta: + + model = SoftwareVersion + + fields = [ + 'id', + 'display_name', + 'name', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + ] From 7d3576a879ea6934f61dc53ca1746f0e7aa5dcf1 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 16:41:51 +0930 Subject: [PATCH 204/617] feat(config_management): Add Device Base Serializer ref: #248 #353 --- app/config_management/models/groups.py | 9 ++++-- .../serializers/config_group.py | 4 +++ app/itam/serializers/device.py | 31 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 app/itam/serializers/device.py diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 060414d39..dc9b24c1c 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -155,7 +155,7 @@ def validate_config_keys_not_reserved(self): "sections": [ { "layout": "table", - "field": "hosts", + "field": "group_software", } ] }, @@ -164,8 +164,10 @@ def validate_config_keys_not_reserved(self): "slug": "configuration", "sections": [ { - "layout": "table", - "field": "rendered_configuration", + "layout": "single", + "fields": [ + "rendered_config" + ], } ] }, @@ -189,6 +191,7 @@ def validate_config_keys_not_reserved(self): table_fields: list = [ 'name', + 'parent', 'count_children', 'organization' ] diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 32805af6d..b64e050ba 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -6,6 +6,8 @@ from config_management.models.groups import ConfigGroups +from itam.serializers.device import DeviceBaseSerializer + class ConfigGroupBaseSerializer(serializers.ModelSerializer): @@ -167,6 +169,8 @@ def validate(self, attrs): class ConfigGroupViewSerializer(ConfigGroupModelSerializer): + hosts = DeviceBaseSerializer(read_only = True, many = True) + parent = ConfigGroupBaseSerializer( read_only = True ) organization = OrganizationBaseSerializer( many=False, read_only=True ) diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py new file mode 100644 index 000000000..cbcec85a9 --- /dev/null +++ b/app/itam/serializers/device.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.models.device import Device + + + +class DeviceBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + class Meta: + + model = Device + + fields = [ + 'id', + 'display_name', + 'name', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + ] From 114272471ed3adcd48ea2a4a04d6a840c611ebd5 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 16:43:34 +0930 Subject: [PATCH 205/617] test(config_management): Config Groups Software Serializer Validation checks ref: #15 #248 #353 --- .../test_config_groups_software_api_v2.py | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py new file mode 100644 index 000000000..0ec03979b --- /dev/null +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py @@ -0,0 +1,362 @@ +import pytest +# import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from config_management.models.groups import DeviceSoftware, ConfigGroups, ConfigGroupSoftware, Software, SoftwareVersion + + + +class ConfigGroupsAPI( + TestCase, + APITenancyObject +): + + model = ConfigGroupSoftware + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.config_group = ConfigGroups.objects.create( + organization = self.organization, + name = 'one', + config = dict({"key": "one", "existing": "dont_over_write"}) + ) + + self.software = Software.objects.create( + organization = self.organization, + name = 'conf grp soft' + ) + + self.software_version = SoftwareVersion.objects.create( + organization = self.organization, + name = '1.1.1', + software = self.software + + ) + + self.item = self.model.objects.create( + organization = self.organization, + config_group = self.config_group, + action = DeviceSoftware.Actions.INSTALL, + software = self.software, + version = self.software_version + ) + + self.url_view_kwargs = {'group_id': self.config_group.id, 'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_config_group_software-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_action(self): + """ Test for existance of API Field + + action field must exist + """ + + assert 'action' in self.api_data + + + def test_api_field_type_action(self): + """ Test for type for API Field + + action field must be int + """ + + assert type(self.api_data['action']) is int + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + model_notes field must not exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + model_notes does not exist for this model + """ + + assert True + + + def test_api_field_exists_config_group(self): + """ Test for existance of API Field + + config_group field must exist + """ + + assert 'config_group' in self.api_data + + + def test_api_field_type_config_group(self): + """ Test for type for API Field + + config_group field must be dict + """ + + assert type(self.api_data['config_group']) is dict + + + + def test_api_field_exists_config_group_id(self): + """ Test for existance of API Field + + config_group.id field must exist + """ + + assert 'id' in self.api_data['config_group'] + + + def test_api_field_type_config_group_id(self): + """ Test for type for API Field + + config_group.id field must be int + """ + + assert type(self.api_data['config_group']['id']) is int + + + def test_api_field_exists_config_group_display_name(self): + """ Test for existance of API Field + + config_group.display_name field must exist + """ + + assert 'display_name' in self.api_data['config_group'] + + + def test_api_field_type_config_group_display_name(self): + """ Test for type for API Field + + config_group.display_name field must be str + """ + + assert type(self.api_data['config_group']['display_name']) is str + + + def test_api_field_exists_config_group_url(self): + """ Test for existance of API Field + + config_group.url field must exist + """ + + assert 'url' in self.api_data['config_group'] + + + def test_api_field_type_config_group_url(self): + """ Test for type for API Field + + config_group.url field must be str + """ + + assert type(self.api_data['config_group']['url']) is str + + + + + + + + def test_api_field_exists_software(self): + """ Test for existance of API Field + + software field must exist + """ + + assert 'software' in self.api_data + + + def test_api_field_type_software(self): + """ Test for type for API Field + + software field must be dict + """ + + assert type(self.api_data['software']) is dict + + + + def test_api_field_exists_software_id(self): + """ Test for existance of API Field + + software.id field must exist + """ + + assert 'id' in self.api_data['software'] + + + def test_api_field_type_software_id(self): + """ Test for type for API Field + + software.id field must be int + """ + + assert type(self.api_data['software']['id']) is int + + + def test_api_field_exists_software_display_name(self): + """ Test for existance of API Field + + software.display_name field must exist + """ + + assert 'display_name' in self.api_data['software'] + + + def test_api_field_type_software_display_name(self): + """ Test for type for API Field + + software.display_name field must be str + """ + + assert type(self.api_data['software']['display_name']) is str + + + def test_api_field_exists_software_url(self): + """ Test for existance of API Field + + software.url field must exist + """ + + assert 'url' in self.api_data['software'] + + + def test_api_field_type_software_url(self): + """ Test for type for API Field + + software.url field must be str + """ + + assert type(self.api_data['software']['url']) is Hyperlink + + + + + + + + + def test_api_field_exists_version(self): + """ Test for existance of API Field + + version field must exist + """ + + assert 'version' in self.api_data + + + def test_api_field_type_version(self): + """ Test for type for API Field + + version field must be dict + """ + + assert type(self.api_data['version']) is dict + + + + def test_api_field_exists_version_id(self): + """ Test for existance of API Field + + version.id field must exist + """ + + assert 'id' in self.api_data['version'] + + + def test_api_field_type_version_id(self): + """ Test for type for API Field + + version.id field must be int + """ + + assert type(self.api_data['version']['id']) is int + + + def test_api_field_exists_version_display_name(self): + """ Test for existance of API Field + + version.display_name field must exist + """ + + assert 'display_name' in self.api_data['version'] + + + def test_api_field_type_version_display_name(self): + """ Test for type for API Field + + version.display_name field must be str + """ + + assert type(self.api_data['version']['display_name']) is str + + + def test_api_field_exists_version_url(self): + """ Test for existance of API Field + + version.url field must exist + """ + + assert 'url' in self.api_data['version'] + + + def test_api_field_type_version_url(self): + """ Test for type for API Field + + version.url field must be Hyperlink + """ + + assert type(self.api_data['version']['url']) is Hyperlink From 99313d7a8d22e18cbea534eb452702b300966ec5 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 16:57:39 +0930 Subject: [PATCH 206/617] feat(config_management): Add config groups to config api endpoint ref: #248 #353 --- app/config_management/serializers/config_group.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index b64e050ba..8fbb6229c 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -65,6 +65,13 @@ def get_url(self, item): 'pk': item.pk } ), + 'child_groups': reverse( + 'API:_api_v2_config_group_child-list', + request = self.context['view'].request, + kwargs = { + 'parent_group': item.pk + } + ), 'configgroups': reverse( 'API:_api_v2_config_group-list', request = self.context['view'].request, @@ -86,6 +93,8 @@ def get_url(self, item): ), } + rendered_config = serializers.JSONField( source = 'render_config', read_only=True ) + class Meta: @@ -100,6 +109,7 @@ class Meta: 'model_notes', 'config', 'hosts', + 'rendered_config', 'is_global', 'created', 'modified', From f019791fa4f0e63fa21cbd871304afc02bd8f6fa Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 17:32:48 +0930 Subject: [PATCH 207/617] test(config_management): Config Groups Software API ViewSet permission checks ref: #15 #248 #353 --- .../test_config_groups_software_viewset.py | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py new file mode 100644 index 000000000..c8df61dfe --- /dev/null +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py @@ -0,0 +1,214 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from config_management.models.groups import ConfigGroups, ConfigGroupSoftware, Software, SoftwareVersion + + + +class ConfigGroupSoftwarePermissionsAPI(TestCase, APIPermissions): + + model = ConfigGroupSoftware + + app_namespace = 'API' + + url_name = '_api_v2_config_group_software' + + change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.config_group = ConfigGroups.objects.create( + organization = self.organization, + name = 'one' + ) + + self.url_kwargs = { 'group_id': self.config_group.id } + + self.software = Software.objects.create( + organization = self.organization, + name = 'random name' + ) + + self.software_two = Software.objects.create( + organization = self.organization, + name = 'random name two' + ) + + self.software_version = SoftwareVersion.objects.create( + organization = self.organization, + software = self.software, + name = '1.1.1' + ) + + + self.software_version_two = SoftwareVersion.objects.create( + organization = self.organization, + software = self.software_two, + name = '2.2.2' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + config_group = self.config_group, + software = self.software, + version = self.software_version + ) + + + self.url_view_kwargs = {'group_id': self.config_group.id, 'pk': self.item.id} + + self.add_data = { + 'organization': self.organization.id, + 'software': self.software_two.id, + 'config_group': self.config_group.id, + 'version': self.software_version_two.id + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 71f746dba6a2fc64b7995e6083b761077404ff8b Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 18:20:50 +0930 Subject: [PATCH 208/617] feat(config_management): Depreciate API v1 config endpoint ref: #248 #353 --- app/api/views/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/api/views/config.py b/app/api/views/config.py index 728fbe877..aadb40afa 100644 --- a/app/api/views/config.py +++ b/app/api/views/config.py @@ -8,7 +8,7 @@ from config_management.models.groups import ConfigGroups - +@extend_schema( deprecated = True ) @extend_schema_view( get=extend_schema( summary = "Fetch Config groups", @@ -31,6 +31,7 @@ def get_view_name(self): +@extend_schema( deprecated = True ) @extend_schema_view( get=extend_schema( summary = "Get A Config Group", From b9b2142cc1d3c58bc62b67064ae27c24dd315a77 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 17 Oct 2024 18:26:12 +0930 Subject: [PATCH 209/617] fix(config_management): ensure validation uses software.id for config group software serializer ref: #248 #353 --- app/config_management/serializers/config_group_software.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py index 96bcf722b..1a8cda9e1 100644 --- a/app/config_management/serializers/config_group_software.py +++ b/app/config_management/serializers/config_group_software.py @@ -177,14 +177,14 @@ def validate(self, attrs): if 'software' in self.initial_data: - software = self.initial_data['software'] + software = int(self.initial_data['software']) elif self.instance: software = self.instance.software - if software != current_object.software: + if software != current_object.software.id: raise serializers.ValidationError( detail = { From 40f110e7e5af49d286f81fefc64f4817e9d50164 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 12:16:31 +0930 Subject: [PATCH 210/617] fix(core): Add missing attributes name to history model ref: #248 #354 --- app/core/models/history.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/core/models/history.py b/app/core/models/history.py index e68a96188..720aa64e6 100644 --- a/app/core/models/history.py +++ b/app/core/models/history.py @@ -30,6 +30,10 @@ class Meta: '-created' ] + verbose_name = 'History' + + verbose_name_plural = 'History' + class Actions(models.TextChoices): ADD = '1', 'Create' From 2dd01111d962253197c96335c6308ea61cde504a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 13:11:45 +0930 Subject: [PATCH 211/617] feat(config_management): Add History API v2 endpoint ref: #248 #354 --- app/api/urls.py | 6 ++ app/core/serializers/history.py | 104 ++++++++++++++++++++++++++++++++ app/core/viewsets/history.py | 65 ++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 app/core/serializers/history.py create mode 100644 app/core/viewsets/history.py diff --git a/app/api/urls.py b/app/api/urls.py index 3f4feaf02..8fc3021ea 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -59,6 +59,10 @@ config_group_software as config_group_software_v2 ) +from core.viewsets import ( + history as history_v2 +) + from itam.viewsets import ( index as itam_index_v2, ) @@ -132,6 +136,8 @@ router.register('v2/config_management/group/(?P[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child') router.register('v2/config_management/group/(?P[0-9]+)/software', config_group_software_v2.ViewSet, basename='_api_v2_config_group_software') +router.register('v2/core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') + router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') diff --git a/app/core/serializers/history.py b/app/core/serializers/history.py new file mode 100644 index 000000000..a6722fa8d --- /dev/null +++ b/app/core/serializers/history.py @@ -0,0 +1,104 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from app.serializers.user import UserBaseSerializer + +from core.models.history import History + + + +class HistoryBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_my_url') + + def get_my_url(self, item): + + return reverse("API:_api_v2_model_history-detail", + request=self._context['view'].request, + kwargs={ + 'model_class': self._kwargs['context']['view'].kwargs['model_class'], + 'model_id': self._kwargs['context']['view'].kwargs['model_id'], + 'pk': item.pk + } + ) + + + class Meta: + + model = History + + fields = [ + 'id', + 'display_name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url', + ] + + +class HistoryModelSerializer(HistoryBaseSerializer): + + + after = serializers.JSONField(read_only=True) + + before = serializers.JSONField(read_only=True) + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_model_history-detail", + request=self._context['view'].request, + kwargs={ + 'model_class': self._kwargs['context']['view'].kwargs['model_class'], + 'model_id': self._kwargs['context']['view'].kwargs['model_id'], + 'pk': item.pk + } + ), + } + + + class Meta: + + model = History + + fields = [ + 'id', + 'display_name', + 'before', + 'after', + 'action', + 'user', + 'item_pk', + 'item_class', + 'item_parent_pk', + 'item_parent_class', + 'created', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + '_urls', + ] + + + +class HistoryViewSerializer(HistoryModelSerializer): + + user = UserBaseSerializer( read_only = True ) + + pass diff --git a/app/core/viewsets/history.py b/app/core/viewsets/history.py new file mode 100644 index 000000000..be7fcd097 --- /dev/null +++ b/app/core/viewsets/history.py @@ -0,0 +1,65 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from core.serializers.history import ( + History, + HistoryModelSerializer, + HistoryViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( exclude = True ), + destroy = extend_schema( exclude = True ), + list = extend_schema( + summary = 'Fetch entire history for a model', + description='', + responses = { + 200: OpenApiResponse(description='', response=HistoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( exclude = True ), + update = extend_schema( exclude = True ), + partial_update = extend_schema( exclude = True ) +) +class ViewSet(ModelViewSet): + + allowed_methods = [ + 'GET', + 'OPTIONS' + ] + + filterset_fields = [ + 'item_parent_pk', + 'item_parent_class' + ] + + + model = History + + + def get_queryset(self): + + queryset = super().get_queryset() + + self.queryset = queryset.filter( + item_parent_class = self.kwargs['model_class'], + item_parent_pk = self.kwargs['model_id'] + ).order_by('-created') + + return self.queryset + + + def get_serializer_class(self): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + def get_view_name(self): + if self.detail: + return "History" + + return 'History' From 0ed61336982affbdfae53875a978ce9f14d4beee Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 13:56:40 +0930 Subject: [PATCH 212/617] test(config_management): History API ViewSet permission checks ref: #15 #248 #354 --- .../unit/test_history/test_history_viewset.py | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 app/core/tests/unit/test_history/test_history_viewset.py diff --git a/app/core/tests/unit/test_history/test_history_viewset.py b/app/core/tests/unit/test_history/test_history_viewset.py new file mode 100644 index 000000000..975cad517 --- /dev/null +++ b/app/core/tests/unit/test_history/test_history_viewset.py @@ -0,0 +1,317 @@ +import pytest +# import unittest +# import requests + + +# from django.contrib.auth import get_user_model +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.history import History + +from itam.models.device import Device + + + +class HistoryPermissionsAPI(TestCase, APIPermissions): + + model = History + + app_namespace = 'API' + + url_name = '_api_v2_model_history' + + # change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + # self.config_group = ConfigGroups.objects.create( + # organization = self.organization, + # name = 'one' + # ) + + # self.software = Software.objects.create( + # organization = self.organization, + # name = 'random name' + # ) + + # self.software_two = Software.objects.create( + # organization = self.organization, + # name = 'random name two' + # ) + + # self.software_version = SoftwareVersion.objects.create( + # organization = self.organization, + # software = self.software, + # name = '1.1.1' + # ) + + + # self.software_version_two = SoftwareVersion.objects.create( + # organization = self.organization, + # software = self.software_two, + # name = '2.2.2' + # ) + + self.device = Device.objects.create( + organization = self.organization, + name = 'history-device' + ) + + + # self.item = self.model.objects.create( + # organization = self.organization, + # config_group = self.config_group, + # software = self.software, + # version = self.software_version + # ) + + self.item = History.objects.get(item_class = 'device', item_pk = self.device.id) + + + self.url_kwargs = {'model_class': 'device', 'model_id': self.device.id} + + self.url_view_kwargs = {'model_class': 'device', 'model_id': self.device.id, 'pk': self.item.pk } + + self.add_data = {} + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + def test_view_list_has_permission(self): + """ Check correct permission for view + + Attempt to view as user with view permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs=self.url_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + assert response.status_code == 200 + + + def test_view_has_permission(self): + """ Check correct permission for view + + Custom permission of test case with same name. + This test ensures that the user is denied + + Attempt to view as user with view permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + assert response.status_code == 403 + + + def test_add_has_permission(self): + """ Check correct permission for add + + Custom permission of test case with same name. + This test ensures that the user is denied + + Attempt to add as user with permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 403 + + + def test_change_has_permission(self): + """ Check correct permission for change + + Custom permission of test case with same name. + This test ensures that the user is denied + + Make change with user who has change permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 403 + + + def test_delete_has_permission(self): + """ Check correct permission for delete + + Custom permission of test case with same name. + This test ensures that the user is denied + + Delete item as user with delete permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.delete_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + From e8279dd3639781dd043c6d85e666e1f61bf120c5 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 16:23:43 +0930 Subject: [PATCH 213/617] refactor(core): Adjust action choices to be integer ref: #248 #354 --- ...er_history_options_alter_history_action.py | 22 +++++++++++++++++++ app/core/models/history.py | 8 +++---- 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 app/core/migrations/0011_alter_history_options_alter_history_action.py diff --git a/app/core/migrations/0011_alter_history_options_alter_history_action.py b/app/core/migrations/0011_alter_history_options_alter_history_action.py new file mode 100644 index 000000000..711e181bd --- /dev/null +++ b/app/core/migrations/0011_alter_history_options_alter_history_action.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2024-10-18 03:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_alter_history_action_alter_history_after_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='history', + options={'ordering': ['-created'], 'verbose_name': 'History', 'verbose_name_plural': 'History'}, + ), + migrations.AlterField( + model_name='history', + name='action', + field=models.IntegerField(choices=[(1, 'Create'), (2, 'Update'), (3, 'Delete')], default=None, help_text='History action performed', null=True, verbose_name='Action'), + ), + ] diff --git a/app/core/models/history.py b/app/core/models/history.py index 720aa64e6..3f77899e2 100644 --- a/app/core/models/history.py +++ b/app/core/models/history.py @@ -35,10 +35,10 @@ class Meta: verbose_name_plural = 'History' - class Actions(models.TextChoices): - ADD = '1', 'Create' - UPDATE = '2', 'Update' - DELETE = '3', 'Delete' + class Actions(models.IntegerChoices): + ADD = 1, 'Create' + UPDATE = 2, 'Update' + DELETE = 3, 'Delete' before = models.JSONField( From c7d4f2b4963d2a5bed5aed8ea8ae3a443b61c9ba Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 16:37:59 +0930 Subject: [PATCH 214/617] feat(config_management): Add Notes API v2 endpoint ref: #248 #354 --- app/api/urls.py | 2 + .../serializers/config_group.py | 5 + .../serializers/config_group_software.py | 3 +- app/core/serializers/notes.py | 174 ++++++++++++++++ app/core/tests/abstract/test_notes_viewset.py | 187 ++++++++++++++++++ app/core/viewsets/notes.py | 105 ++++++++++ 6 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 app/core/serializers/notes.py create mode 100644 app/core/tests/abstract/test_notes_viewset.py create mode 100644 app/core/viewsets/notes.py diff --git a/app/api/urls.py b/app/api/urls.py index 8fc3021ea..ad3986b58 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -60,6 +60,7 @@ ) from core.viewsets import ( + notes as notes_v2, history as history_v2 ) @@ -134,6 +135,7 @@ router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') router.register('v2/config_management/group', config_group_v2.ViewSet, basename='_api_v2_config_group') router.register('v2/config_management/group/(?P[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child') +router.register('v2/config_management/group/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_config_group_notes') router.register('v2/config_management/group/(?P[0-9]+)/software', config_group_software_v2.ViewSet, basename='_api_v2_config_group_software') router.register('v2/core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 8fbb6229c..11dac2616 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -83,6 +83,11 @@ def get_url(self, item): 'group_id': item.pk } ), + 'notes': reverse( + "API:_api_v2_config_group_notes-list", + request=self._context['view'].request, + kwargs={'group_id': item.pk} + ), 'organization': reverse( 'API:_api_v2_organization-list', request=self.context['view'].request, diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py index 1a8cda9e1..bef851c63 100644 --- a/app/config_management/serializers/config_group_software.py +++ b/app/config_management/serializers/config_group_software.py @@ -75,6 +75,7 @@ def get_url(self, item): 'API:_api_v2_organization-list', request=self.context['view'].request, ), + 'softwareversion': 'ToDo', } @@ -135,7 +136,7 @@ def is_valid(self, *, raise_exception=False) -> bool: parent_item = ConfigGroups.objects.get(pk = int(self._context['view'].kwargs['group_id'])) - self.validated_data['organization_id'] = parent_item.id + self.validated_data['organization_id'] = parent_item.organization.id return is_valid diff --git a/app/core/serializers/notes.py b/app/core/serializers/notes.py new file mode 100644 index 000000000..d431700fe --- /dev/null +++ b/app/core/serializers/notes.py @@ -0,0 +1,174 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from config_management.serializers.config_group import ConfigGroupBaseSerializer + +from core.models.notes import Notes + +from itam.serializers.device import DeviceBaseSerializer +from itam.serializers.operating_system import OperatingSystemBaseSerializer +from itam.serializers.software import SoftwareBaseSerializer + +from itim.serializers.service import ServiceBaseSerializer + + + +class NoteBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device-detail", format="html" + ) + + class Meta: + + model = Notes + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class NoteModelSerializer(NoteBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + + if 'group_id' in self._kwargs['context']['view'].kwargs: + + _self = reverse("API:_api_v2_config_group_notes-detail", + request=self._context['view'].request, + kwargs={ + 'group_id': self._kwargs['context']['view'].kwargs['group_id'], + 'pk': item.pk + } + ) + + elif 'device_id' in self._kwargs['context']['view'].kwargs: + + _self = reverse("API:_api_v2_device_notes-detail", + request=self._context['view'].request, + kwargs={ + 'device_id': self._kwargs['context']['view'].kwargs['device_id'], + 'pk': item.pk + } + ) + + + return { + '_self': _self, + } + + + class Meta: + + model = Notes + + fields = [ + 'id', + 'organization', + 'display_name', + 'note', + 'usercreated', + 'usermodified', + 'config_group', + 'device', + 'service', + 'software', + 'operatingsystem', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'organization', + 'display_name', + 'usercreated', + 'usermodified', + 'config_group', + 'device', + 'service', + 'software', + 'operatingsystem', + 'created', + 'modified', + '_urls', + ] + + + def is_valid(self, *, raise_exception=False) -> bool: + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'view' in self._context: + + if 'group_id' in self._kwargs['context']['view'].kwargs: + + from config_management.models.groups import ConfigGroups as model + + key = 'group_id' + + self.validated_data['config_group_id'] = int(self._context['view'].kwargs['group_id']) + + + elif 'device_id' in self._kwargs['context']['view'].kwargs: + + from itam.models.device import Device as model + + key = 'device_id' + + self.validated_data['device_id'] = int(self._context['view'].kwargs['device_id']) + + + item = model.objects.get(pk = int(self._context['view'].kwargs[key])) + + self.validated_data['organization_id'] = item.organization.id + + self.validated_data['usercreated_id'] = self._kwargs['context']['view'].request.user.id + + return is_valid + + + +class NoteViewSerializer(NoteModelSerializer): + + config_group = ConfigGroupBaseSerializer( many = False, read_only = True ) + + device = DeviceBaseSerializer( many = False, read_only = True ) + + service = ServiceBaseSerializer( many = False, read_only = True ) + + software = SoftwareBaseSerializer( many = False, read_only = True ) + + operatingsystem = OperatingSystemBaseSerializer( many = False, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + usercreated = UserBaseSerializer( many = False, read_only = True ) + + usermodified = UserBaseSerializer( many = False, read_only = True ) diff --git a/app/core/tests/abstract/test_notes_viewset.py b/app/core/tests/abstract/test_notes_viewset.py new file mode 100644 index 000000000..7ebdadb88 --- /dev/null +++ b/app/core/tests/abstract/test_notes_viewset.py @@ -0,0 +1,187 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.notes import Notes + +from config_management.models.groups import ConfigGroups + + + +class NoteViewSetCommon( + APIPermissions +): + + app_namespace: str = None + + url_name: str = None + + change_data = {'note': 'a changed note'} + + delete_data = {} + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + Notes._meta.model_name, + content_type = ContentType.objects.get( + app_label = Notes._meta.app_label, + model = Notes._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + Notes._meta.model_name, + content_type = ContentType.objects.get( + app_label = Notes._meta.app_label, + model = Notes._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + Notes._meta.model_name, + content_type = ContentType.objects.get( + app_label = Notes._meta.app_label, + model = Notes._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + Notes._meta.model_name, + content_type = ContentType.objects.get( + app_label = Notes._meta.app_label, + model = Notes._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + + + + # self.note_item = ConfigGroups.objects.create( + # organization = self.organization, + # name = 'history-device' + # ) + + + # self.item = Notes.objects.create( + # organization = self.organization, + # note = 'a note', + # usercreated = self.view_user, + # config_group = self.note_item + # ) + + + # self.url_kwargs = {'group_id': self.note_item.id} + + # self.url_view_kwargs = {'group_id': self.note_item.id, 'pk': self.item.pk } + + # self.add_data = {'note': 'a note added', 'organization': self.organization.id} diff --git a/app/core/viewsets/notes.py b/app/core/viewsets/notes.py new file mode 100644 index 000000000..5cdb6e437 --- /dev/null +++ b/app/core/viewsets/notes.py @@ -0,0 +1,105 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from core.serializers.notes import ( + Notes, + NoteModelSerializer, + NoteViewSerializer +) + +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a note', + description = '', + responses = { + 201: OpenApiResponse(description='created', response=NoteViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a note', + description = '' + ), + list = extend_schema( + summary = 'Fetch all notes', + description='', + ), + retrieve = extend_schema( + summary = 'Fetch a single note', + description='', + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a note', + description = '' + ), +) +class ViewSet(ModelViewSet): + + filterset_fields = [ + 'config_group', + 'device', + ] + + search_fields = [ + 'note', + ] + + model = Notes + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return ViewSerializer + + + return ModelSerializer + + + def get_queryset(self): + + queryset = super().get_queryset() + + if 'group_id' in self.kwargs: + + self.queryset = queryset.filter(config_group_id=self.kwargs['group_id']).order_by('-created') + + elif 'device_id' in self.kwargs: + + self.queryset = queryset.filter(device_id=self.kwargs['device_id']).order_by('-created') + + else: + + self.queryset = queryset + + + return self.queryset + + + def get_view_name(self): + if self.detail: + return "Note" + + return 'Notes' + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] From 4a2ad114d7bedd5c9d34c74d661e355182f5f2cc Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 16:38:38 +0930 Subject: [PATCH 215/617] test(config_management): Config Groups Note API ViewSet permission checks ref: #15 #49 #248 #354 --- .../test_config_groups_notes_viewset.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups/test_config_groups_notes_viewset.py diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_notes_viewset.py b/app/config_management/tests/unit/config_groups/test_config_groups_notes_viewset.py new file mode 100644 index 000000000..7a74f3dcc --- /dev/null +++ b/app/config_management/tests/unit/config_groups/test_config_groups_notes_viewset.py @@ -0,0 +1,55 @@ +import pytest + +from django.test import TestCase + +from core.tests.abstract.test_notes_viewset import NoteViewSetCommon + +from core.models.notes import Notes + +from config_management.models.groups import ConfigGroups + + + +class NotePermissionsAPI( + NoteViewSetCommon, + TestCase, +): + + app_namespace = 'API' + + url_name = '_api_v2_config_group_notes' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + super().setUpTestData() + + + + self.note_item = ConfigGroups.objects.create( + organization = self.organization, + name = 'history-device' + ) + + self.item = Notes.objects.create( + organization = self.organization, + note = 'a note', + usercreated = self.view_user, + config_group = self.note_item + ) + + + self.url_kwargs = {'group_id': self.note_item.id} + + self.url_view_kwargs = {'group_id': self.note_item.id, 'pk': self.item.pk } + + self.add_data = {'note': 'a note added', 'organization': self.organization.id} From 419725f13fbe70d1944e140b40a0ee0dd263cca7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 16:46:19 +0930 Subject: [PATCH 216/617] feat(itam): Add operating system Base Serializer ref: #248 #354 --- app/itam/serializers/operating_system.py | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/itam/serializers/operating_system.py diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py new file mode 100644 index 000000000..d7a081199 --- /dev/null +++ b/app/itam/serializers/operating_system.py @@ -0,0 +1,33 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer +from itam.models.operating_system import OperatingSystem + + + +class OperatingSystemBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + class Meta: + + model = OperatingSystem + + fields = '__all__' + fields = [ + 'id', + 'display_name', + 'name', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + ] From 2363064b1a80133a4da868f70ac083877957beb2 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 16:47:49 +0930 Subject: [PATCH 217/617] feat(itim): Add Service base serializer ref: #248 #354 --- app/itim/serializers/service.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 app/itim/serializers/service.py diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py new file mode 100644 index 000000000..2f051166f --- /dev/null +++ b/app/itim/serializers/service.py @@ -0,0 +1,32 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itim.models.services import Service + + + +class ServiceBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + class Meta: + + model = Service + + fields = [ + 'id', + 'display_name', + 'name', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + ] From 9f81c4911930e2b514f9efba140f0b68fa199356 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 17:34:36 +0930 Subject: [PATCH 218/617] test: Adjust tests to cater for action choices now being an integer ref: #15 #46 #248 #354 --- .../unit/organization/test_organization_core_history.py | 8 ++++---- app/access/tests/unit/team/test_team_core_history.py | 6 +++--- .../tests/unit/team_user/test_team_user_core_history.py | 6 +++--- .../knowledge_base/test_knowledge_base_core_history.py | 4 ++-- .../test_knowledge_base_category_core_history.py | 4 ++-- .../unit/config_groups/test_config_groups_core_history.py | 6 +++--- .../test_config_groups_software_core_history.py | 6 +++--- app/core/tests/abstract/history_entry.py | 4 ++-- app/core/tests/abstract/history_entry_child_model.py | 2 +- .../unit/manufacturer/test_manufacturer_core_history.py | 4 ++-- .../ticket_category/test_ticket_category_core_history.py | 4 ++-- .../test_ticket_comment_category_core_history.py | 4 ++-- app/itam/tests/unit/device/test_device_core_history.py | 4 ++-- .../unit/device_model/test_device_model_core_history.py | 4 ++-- .../test_device_operating_system_core_history.py | 6 +++--- .../device_software/test_device_software_core_history.py | 6 +++--- .../unit/device_type/test_device_type_core_history.py | 4 ++-- .../test_operating_system_core_history.py | 4 ++-- .../test_operating_system_version_core_history.py | 6 +++--- .../tests/unit/software/test_software_core_history.py | 4 ++-- .../test_software_category_core_history.py | 4 ++-- app/itim/tests/unit/cluster/test_cluster_core_history.py | 4 ++-- .../unit/cluster_types/test_cluster_type_core_history.py | 4 ++-- app/itim/tests/unit/port/test_port_core_history.py | 4 ++-- app/itim/tests/unit/service/test_service_core_history.py | 4 ++-- .../tests/unit/project/test_project_core_history.py | 4 ++-- .../test_project_milestone_core_history.py | 6 +++--- .../unit/project_state/test_project_state_core_history.py | 4 ++-- .../unit/project_type/test_project_type_core_history.py | 4 ++-- .../external_links/test_external_links_core_history.py | 4 ++-- 30 files changed, 69 insertions(+), 69 deletions(-) diff --git a/app/access/tests/unit/organization/test_organization_core_history.py b/app/access/tests/unit/organization/test_organization_core_history.py index 7b57736a0..a14724563 100644 --- a/app/access/tests/unit/organization/test_organization_core_history.py +++ b/app/access/tests/unit/organization/test_organization_core_history.py @@ -34,7 +34,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -44,7 +44,7 @@ def setUpTestData(self): self.item_change.save() self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -72,7 +72,7 @@ def test_history_entry_item_add_field_action(self): history = self.history_create.__dict__ - assert history['action'] == int(History.Actions.ADD[0]) + assert history['action'] == int(History.Actions.ADD) # assert type(history['action']) is int @@ -125,7 +125,7 @@ def test_history_entry_item_change_field_action(self): history = self.history_change.__dict__ - assert history['action'] == int(History.Actions.UPDATE[0]) + assert history['action'] == int(History.Actions.UPDATE) # assert type(history['action']) is int diff --git a/app/access/tests/unit/team/test_team_core_history.py b/app/access/tests/unit/team/test_team_core_history.py index b8f9ce3f5..d8abfe677 100644 --- a/app/access/tests/unit/team/test_team_core_history.py +++ b/app/access/tests/unit/team/test_team_core_history.py @@ -39,7 +39,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -51,7 +51,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_org_' + self.item_change.team_name + '", "team_name": "' + self.item_change.team_name + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -68,7 +68,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/access/tests/unit/team_user/test_team_user_core_history.py b/app/access/tests/unit/team_user/test_team_user_core_history.py index 2de2e6912..489721713 100644 --- a/app/access/tests/unit/team_user/test_team_user_core_history.py +++ b/app/access/tests/unit/team_user/test_team_user_core_history.py @@ -48,7 +48,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -60,7 +60,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"manager": true}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -81,7 +81,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_core_history.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_core_history.py index eaf858b92..fbe6c698c 100644 --- a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_core_history.py +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_core_history.py @@ -41,7 +41,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -53,7 +53,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"title": "' + self.item_change.title + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_core_history.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_core_history.py index 3b3813480..debcd0939 100644 --- a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_core_history.py +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_core_history.py @@ -37,7 +37,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -49,7 +49,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_core_history.py b/app/config_management/tests/unit/config_groups/test_config_groups_core_history.py index 51cf9d5f2..8d9688414 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups_core_history.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups_core_history.py @@ -42,7 +42,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -54,7 +54,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -70,7 +70,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py index d233b7231..94f8c7cdf 100644 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_core_history.py @@ -53,7 +53,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name ) @@ -65,7 +65,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"action": "' + str(DeviceSoftware.Actions.REMOVE) + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -89,7 +89,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/core/tests/abstract/history_entry.py b/app/core/tests/abstract/history_entry.py index 5dcb18564..a34931251 100644 --- a/app/core/tests/abstract/history_entry.py +++ b/app/core/tests/abstract/history_entry.py @@ -15,7 +15,7 @@ def test_history_entry_item_add_field_action(self): history = self.history_create.__dict__ - assert history['action'] == int(History.Actions.ADD[0]) + assert history['action'] == int(History.Actions.ADD) # assert type(history['action']) is int @@ -69,7 +69,7 @@ def test_history_entry_item_change_field_action(self): history = self.history_change.__dict__ - assert history['action'] == int(History.Actions.UPDATE[0]) + assert history['action'] == int(History.Actions.UPDATE) # assert type(history['action']) is int diff --git a/app/core/tests/abstract/history_entry_child_model.py b/app/core/tests/abstract/history_entry_child_model.py index 2db4d073f..ca619c1df 100644 --- a/app/core/tests/abstract/history_entry_child_model.py +++ b/app/core/tests/abstract/history_entry_child_model.py @@ -21,7 +21,7 @@ def test_history_entry_item_delete_field_action(self): history = self.history_delete.__dict__ - assert history['action'] == int(History.Actions.DELETE[0]) + assert history['action'] == int(History.Actions.DELETE) # assert type(history['action']) is int diff --git a/app/core/tests/unit/manufacturer/test_manufacturer_core_history.py b/app/core/tests/unit/manufacturer/test_manufacturer_core_history.py index 856d1da12..2a21b1c6b 100644 --- a/app/core/tests/unit/manufacturer/test_manufacturer_core_history.py +++ b/app/core/tests/unit/manufacturer/test_manufacturer_core_history.py @@ -37,7 +37,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -50,7 +50,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_core_history.py b/app/core/tests/unit/ticket_category/test_ticket_category_core_history.py index ee1d322e1..243d458fc 100644 --- a/app/core/tests/unit/ticket_category/test_ticket_category_core_history.py +++ b/app/core/tests/unit/ticket_category/test_ticket_category_core_history.py @@ -37,7 +37,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -50,7 +50,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py index 2b17b0906..64291b824 100644 --- a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_core_history.py @@ -37,7 +37,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -50,7 +50,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/device/test_device_core_history.py b/app/itam/tests/unit/device/test_device_core_history.py index 79e840dd2..03b989221 100644 --- a/app/itam/tests/unit/device/test_device_core_history.py +++ b/app/itam/tests/unit/device/test_device_core_history.py @@ -38,7 +38,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -51,7 +51,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/device_model/test_device_model_core_history.py b/app/itam/tests/unit/device_model/test_device_model_core_history.py index 3c767c888..d663daad3 100644 --- a/app/itam/tests/unit/device_model/test_device_model_core_history.py +++ b/app/itam/tests/unit/device_model/test_device_model_core_history.py @@ -38,7 +38,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -50,7 +50,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/device_operating_system/test_device_operating_system_core_history.py b/app/itam/tests/unit/device_operating_system/test_device_operating_system_core_history.py index 252e3ee16..72e685f89 100644 --- a/app/itam/tests/unit/device_operating_system/test_device_operating_system_core_history.py +++ b/app/itam/tests/unit/device_operating_system/test_device_operating_system_core_history.py @@ -55,7 +55,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -74,7 +74,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"operating_system_version_id": ' + str(self.item_operating_system_version_changed.pk) + '}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -102,7 +102,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/device_software/test_device_software_core_history.py b/app/itam/tests/unit/device_software/test_device_software_core_history.py index 3dc454a6c..ca4d4fc1a 100644 --- a/app/itam/tests/unit/device_software/test_device_software_core_history.py +++ b/app/itam/tests/unit/device_software/test_device_software_core_history.py @@ -54,7 +54,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -73,7 +73,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"installedversion_id": ' + str(self.item_software_version_changed.pk) + '}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -102,7 +102,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/device_type/test_device_type_core_history.py b/app/itam/tests/unit/device_type/test_device_type_core_history.py index 99ffe1404..e38c3c6a5 100644 --- a/app/itam/tests/unit/device_type/test_device_type_core_history.py +++ b/app/itam/tests/unit/device_type/test_device_type_core_history.py @@ -37,7 +37,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -49,7 +49,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/operating_system/test_operating_system_core_history.py b/app/itam/tests/unit/operating_system/test_operating_system_core_history.py index 0c28675ab..22058116d 100644 --- a/app/itam/tests/unit/operating_system/test_operating_system_core_history.py +++ b/app/itam/tests/unit/operating_system/test_operating_system_core_history.py @@ -38,7 +38,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -50,7 +50,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_core_history.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_core_history.py index 4b48c25ab..9a439a88f 100644 --- a/app/itam/tests/unit/operating_system_version/test_operating_system_version_core_history.py +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version_core_history.py @@ -42,7 +42,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -54,7 +54,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -70,7 +70,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/software/test_software_core_history.py b/app/itam/tests/unit/software/test_software_core_history.py index 0a25e4829..83de8b8f0 100644 --- a/app/itam/tests/unit/software/test_software_core_history.py +++ b/app/itam/tests/unit/software/test_software_core_history.py @@ -36,7 +36,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -48,7 +48,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itam/tests/unit/software_category/test_software_category_core_history.py b/app/itam/tests/unit/software_category/test_software_category_core_history.py index 286709d7b..a6fb12860 100644 --- a/app/itam/tests/unit/software_category/test_software_category_core_history.py +++ b/app/itam/tests/unit/software_category/test_software_category_core_history.py @@ -36,7 +36,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -48,7 +48,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itim/tests/unit/cluster/test_cluster_core_history.py b/app/itim/tests/unit/cluster/test_cluster_core_history.py index 4de46dcfb..0b38e88ff 100644 --- a/app/itim/tests/unit/cluster/test_cluster_core_history.py +++ b/app/itim/tests/unit/cluster/test_cluster_core_history.py @@ -41,7 +41,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -53,7 +53,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itim/tests/unit/cluster_types/test_cluster_type_core_history.py b/app/itim/tests/unit/cluster_types/test_cluster_type_core_history.py index 3fe81cb97..6fb166cb3 100644 --- a/app/itim/tests/unit/cluster_types/test_cluster_type_core_history.py +++ b/app/itim/tests/unit/cluster_types/test_cluster_type_core_history.py @@ -41,7 +41,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -53,7 +53,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itim/tests/unit/port/test_port_core_history.py b/app/itim/tests/unit/port/test_port_core_history.py index 29109f3c7..33e5d742f 100644 --- a/app/itim/tests/unit/port/test_port_core_history.py +++ b/app/itim/tests/unit/port/test_port_core_history.py @@ -41,7 +41,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -53,7 +53,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"number": ' + str(self.item_change.number) + '}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/itim/tests/unit/service/test_service_core_history.py b/app/itim/tests/unit/service/test_service_core_history.py index 8447aa702..106c341d8 100644 --- a/app/itim/tests/unit/service/test_service_core_history.py +++ b/app/itim/tests/unit/service/test_service_core_history.py @@ -41,7 +41,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -53,7 +53,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "' + self.item_change.name + '"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/project_management/tests/unit/project/test_project_core_history.py b/app/project_management/tests/unit/project/test_project_core_history.py index 799874fd3..7fd0d5c5b 100644 --- a/app/project_management/tests/unit/project/test_project_core_history.py +++ b/app/project_management/tests/unit/project/test_project_core_history.py @@ -36,7 +36,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -49,7 +49,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_core_history.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_core_history.py index d8b73d91f..ed7b50cae 100644 --- a/app/project_management/tests/unit/project_milestone/test_project_milestone_core_history.py +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_core_history.py @@ -61,7 +61,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -80,7 +80,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "project milestone item change name"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) @@ -108,7 +108,7 @@ def setUpTestData(self): self.item_delete.delete() self.history_delete = History.objects.get( - action = History.Actions.DELETE[0], + action = int(History.Actions.DELETE), item_pk = self.deleted_pk, item_class = self.model._meta.model_name, ) diff --git a/app/project_management/tests/unit/project_state/test_project_state_core_history.py b/app/project_management/tests/unit/project_state/test_project_state_core_history.py index bd62443f8..a9010e0bc 100644 --- a/app/project_management/tests/unit/project_state/test_project_state_core_history.py +++ b/app/project_management/tests/unit/project_state/test_project_state_core_history.py @@ -36,7 +36,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -49,7 +49,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/project_management/tests/unit/project_type/test_project_type_core_history.py b/app/project_management/tests/unit/project_type/test_project_type_core_history.py index 0e344f1b6..8554fdae6 100644 --- a/app/project_management/tests/unit/project_type/test_project_type_core_history.py +++ b/app/project_management/tests/unit/project_type/test_project_type_core_history.py @@ -36,7 +36,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -49,7 +49,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) diff --git a/app/settings/tests/unit/external_links/test_external_links_core_history.py b/app/settings/tests/unit/external_links/test_external_links_core_history.py index a2cc1317d..3bfe95146 100644 --- a/app/settings/tests/unit/external_links/test_external_links_core_history.py +++ b/app/settings/tests/unit/external_links/test_external_links_core_history.py @@ -34,7 +34,7 @@ def setUpTestData(self): self.history_create = History.objects.get( - action = History.Actions.ADD[0], + action = int(History.Actions.ADD), item_pk = self.item_create.pk, item_class = self.model._meta.model_name, ) @@ -47,7 +47,7 @@ def setUpTestData(self): self.field_after_expected_value = '{"name": "test_item_' + self.model._meta.model_name + '_changed"}' self.history_change = History.objects.get( - action = History.Actions.UPDATE[0], + action = int(History.Actions.UPDATE), item_pk = self.item_change.pk, item_class = self.model._meta.model_name, ) From a0b013d44e25144f1c7822d0c938c1566418c8de Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 18:05:19 +0930 Subject: [PATCH 219/617] test(config_management): Device Note API ViewSet permission checks ref: #15 #49 #248 #354 --- app/api/urls.py | 1 + .../unit/device/test_device_notes_viewset.py | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 app/itam/tests/unit/device/test_device_notes_viewset.py diff --git a/app/api/urls.py b/app/api/urls.py index ad3986b58..b92c32bf8 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -141,6 +141,7 @@ router.register('v2/core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') +router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') diff --git a/app/itam/tests/unit/device/test_device_notes_viewset.py b/app/itam/tests/unit/device/test_device_notes_viewset.py new file mode 100644 index 000000000..a999da8b4 --- /dev/null +++ b/app/itam/tests/unit/device/test_device_notes_viewset.py @@ -0,0 +1,64 @@ +import pytest +# import unittest +# import requests + + +# from django.contrib.auth import get_user_model +# from django.contrib.auth.models import User +# from django.contrib.contenttypes.models import ContentType +# from django.shortcuts import reverse +from django.test import TestCase + +# from access.models import Organization, Team, TeamUsers, Permission + +from core.tests.abstract.test_notes_viewset import NoteViewSetCommon + +from core.models.notes import Notes + +from itam.models.device import Device + + + +class DeviceNotePermissionsAPI( + NoteViewSetCommon, + TestCase, +): + + app_namespace = 'API' + + url_name = '_api_v2_device_notes' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + super().setUpTestData() + + + + self.note_item = Device.objects.create( + organization = self.organization, + name = 'history-device' + ) + + self.item = Notes.objects.create( + organization = self.organization, + note = 'a note', + usercreated = self.view_user, + device = self.note_item + ) + + + self.url_kwargs = {'device_id': self.note_item.id} + + self.url_view_kwargs = {'device_id': self.note_item.id, 'pk': self.item.pk } + + self.add_data = {'note': 'a note added', 'organization': self.organization.id} From 0ce5b0d98c974fa7a22e01cba4884c248ada116c Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 18:07:11 +0930 Subject: [PATCH 220/617] test(itam): Operating System Note API ViewSet permission checks ref: #15 #49 #248 #354 --- app/api/urls.py | 1 + .../unit/device/test_device_notes_viewset.py | 9 --- .../test_operating_system_notes_viewset.py | 55 +++++++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 app/itam/tests/unit/operating_system/test_operating_system_notes_viewset.py diff --git a/app/api/urls.py b/app/api/urls.py index b92c32bf8..e45d07e9c 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -142,6 +142,7 @@ router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') +router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') diff --git a/app/itam/tests/unit/device/test_device_notes_viewset.py b/app/itam/tests/unit/device/test_device_notes_viewset.py index a999da8b4..22e1cebbc 100644 --- a/app/itam/tests/unit/device/test_device_notes_viewset.py +++ b/app/itam/tests/unit/device/test_device_notes_viewset.py @@ -1,16 +1,7 @@ import pytest -# import unittest -# import requests - -# from django.contrib.auth import get_user_model -# from django.contrib.auth.models import User -# from django.contrib.contenttypes.models import ContentType -# from django.shortcuts import reverse from django.test import TestCase -# from access.models import Organization, Team, TeamUsers, Permission - from core.tests.abstract.test_notes_viewset import NoteViewSetCommon from core.models.notes import Notes diff --git a/app/itam/tests/unit/operating_system/test_operating_system_notes_viewset.py b/app/itam/tests/unit/operating_system/test_operating_system_notes_viewset.py new file mode 100644 index 000000000..586140769 --- /dev/null +++ b/app/itam/tests/unit/operating_system/test_operating_system_notes_viewset.py @@ -0,0 +1,55 @@ +import pytest + +from django.test import TestCase + +from core.tests.abstract.test_notes_viewset import NoteViewSetCommon + +from core.models.notes import Notes + +from itam.models.operating_system import OperatingSystem + + + +class OperatingSystemNotePermissionsAPI( + NoteViewSetCommon, + TestCase, +): + + app_namespace = 'API' + + url_name = '_api_v2_operating_system_notes' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + super().setUpTestData() + + + + self.note_item = OperatingSystem.objects.create( + organization = self.organization, + name = 'history-device' + ) + + self.item = Notes.objects.create( + organization = self.organization, + note = 'a note', + usercreated = self.view_user, + operatingsystem = self.note_item + ) + + + self.url_kwargs = {'operating_system_id': self.note_item.id} + + self.url_view_kwargs = {'operating_system_id': self.note_item.id, 'pk': self.item.pk } + + self.add_data = {'note': 'a note added', 'organization': self.organization.id} From 3e210ed217f4f34c22ff2f630812b663b923f5ab Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 18:07:50 +0930 Subject: [PATCH 221/617] test(itam): Softwaare Note API ViewSet permission checks ref: #15 #49 #248 #354 --- .../software/test_software_notes_viewset.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/itam/tests/unit/software/test_software_notes_viewset.py diff --git a/app/itam/tests/unit/software/test_software_notes_viewset.py b/app/itam/tests/unit/software/test_software_notes_viewset.py new file mode 100644 index 000000000..a8e4be414 --- /dev/null +++ b/app/itam/tests/unit/software/test_software_notes_viewset.py @@ -0,0 +1,55 @@ +import pytest + +from django.test import TestCase + +from core.tests.abstract.test_notes_viewset import NoteViewSetCommon + +from core.models.notes import Notes + +from itam.models.software import Software + + + +class SoftwareNotePermissionsAPI( + NoteViewSetCommon, + TestCase, +): + + app_namespace = 'API' + + url_name = '_api_v2_software_notes' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + super().setUpTestData() + + + + self.note_item = Software.objects.create( + organization = self.organization, + name = 'history-device' + ) + + self.item = Notes.objects.create( + organization = self.organization, + note = 'a note', + usercreated = self.view_user, + software = self.note_item + ) + + + self.url_kwargs = {'software_id': self.note_item.id} + + self.url_view_kwargs = {'software_id': self.note_item.id, 'pk': self.item.pk } + + self.add_data = {'note': 'a note added', 'organization': self.organization.id} From df5a185986c20592fd265ca8e8417ea9ff6805c1 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 18 Oct 2024 18:27:14 +0930 Subject: [PATCH 222/617] test(itim): Service Note API ViewSet permission checks ref: #15 #49 #248 #354 --- app/core/serializers/notes.py | 54 ++++++++++++++++++ app/core/viewsets/notes.py | 18 +++++- .../service/test_service_notes_viewset.py | 55 +++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 app/itim/tests/unit/service/test_service_notes_viewset.py diff --git a/app/core/serializers/notes.py b/app/core/serializers/notes.py index d431700fe..9df686375 100644 --- a/app/core/serializers/notes.py +++ b/app/core/serializers/notes.py @@ -77,6 +77,36 @@ def get_url(self, item): } ) + elif 'operating_system_id' in self._kwargs['context']['view'].kwargs: + + _self = reverse("API:_api_v2_operating_system_notes-detail", + request=self._context['view'].request, + kwargs={ + 'operating_system_id': self._kwargs['context']['view'].kwargs['operating_system_id'], + 'pk': item.pk + } + ) + + elif 'service_id' in self._kwargs['context']['view'].kwargs: + + _self = reverse("API:_api_v2_service_notes-detail", + request=self._context['view'].request, + kwargs={ + 'service_id': self._kwargs['context']['view'].kwargs['service_id'], + 'pk': item.pk + } + ) + + elif 'software_id' in self._kwargs['context']['view'].kwargs: + + _self = reverse("API:_api_v2_software_notes-detail", + request=self._context['view'].request, + kwargs={ + 'software_id': self._kwargs['context']['view'].kwargs['software_id'], + 'pk': item.pk + } + ) + return { '_self': _self, @@ -144,6 +174,30 @@ def is_valid(self, *, raise_exception=False) -> bool: self.validated_data['device_id'] = int(self._context['view'].kwargs['device_id']) + elif 'operating_system_id' in self._kwargs['context']['view'].kwargs: + + from itam.models.operating_system import OperatingSystem as model + + key = 'operating_system_id' + + self.validated_data['operatingsystem_id'] = int(self._context['view'].kwargs['operating_system_id']) + + elif 'service_id' in self._kwargs['context']['view'].kwargs: + + from itim.models.services import Service as model + + key = 'service_id' + + self.validated_data['service_id'] = int(self._context['view'].kwargs['service_id']) + + elif 'software_id' in self._kwargs['context']['view'].kwargs: + + from itam.models.software import Software as model + + key = 'software_id' + + self.validated_data['software_id'] = int(self._context['view'].kwargs['software_id']) + item = model.objects.get(pk = int(self._context['view'].kwargs[key])) diff --git a/app/core/viewsets/notes.py b/app/core/viewsets/notes.py index 5cdb6e437..ea4e33169 100644 --- a/app/core/viewsets/notes.py +++ b/app/core/viewsets/notes.py @@ -69,13 +69,25 @@ def get_queryset(self): queryset = super().get_queryset() - if 'group_id' in self.kwargs: + if 'device_id' in self.kwargs: + + self.queryset = queryset.filter(device_id=self.kwargs['device_id']).order_by('-created') + + elif 'group_id' in self.kwargs: self.queryset = queryset.filter(config_group_id=self.kwargs['group_id']).order_by('-created') - elif 'device_id' in self.kwargs: + elif 'operating_system_id' in self.kwargs: - self.queryset = queryset.filter(device_id=self.kwargs['device_id']).order_by('-created') + self.queryset = queryset.filter(operatingsystem_id=self.kwargs['operating_system_id']).order_by('-created') + + elif 'service_id' in self.kwargs: + + self.queryset = queryset.filter(service_id=self.kwargs['service_id']).order_by('-created') + + elif 'software_id' in self.kwargs: + + self.queryset = queryset.filter(software_id=self.kwargs['software_id']).order_by('-created') else: diff --git a/app/itim/tests/unit/service/test_service_notes_viewset.py b/app/itim/tests/unit/service/test_service_notes_viewset.py new file mode 100644 index 000000000..19582234c --- /dev/null +++ b/app/itim/tests/unit/service/test_service_notes_viewset.py @@ -0,0 +1,55 @@ +import pytest + +from django.test import TestCase + +from core.tests.abstract.test_notes_viewset import NoteViewSetCommon + +from core.models.notes import Notes + +from itim.models.services import Service + + + +class ServiceNotePermissionsAPI( + NoteViewSetCommon, + TestCase, +): + + app_namespace = 'API' + + url_name = '_api_v2_service_notes' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + super().setUpTestData() + + + + self.note_item = Service.objects.create( + organization = self.organization, + name = 'history-device' + ) + + self.item = Notes.objects.create( + organization = self.organization, + note = 'a note', + usercreated = self.view_user, + service = self.note_item + ) + + + self.url_kwargs = {'service_id': self.note_item.id} + + self.url_view_kwargs = {'service_id': self.note_item.id, 'pk': self.item.pk } + + self.add_data = {'note': 'a note added', 'organization': self.organization.id} From 319eaadbc252ead7c5bfb8c15545e51eaf260712 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 12:21:00 +0930 Subject: [PATCH 223/617] test(core): Notes Serializer Validation checks ref: #15 #49 #248 #354 --- .../unit/test_notes/test_notes_serializer.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/core/tests/unit/test_notes/test_notes_serializer.py diff --git a/app/core/tests/unit/test_notes/test_notes_serializer.py b/app/core/tests/unit/test_notes/test_notes_serializer.py new file mode 100644 index 000000000..4b47ac672 --- /dev/null +++ b/app/core/tests/unit/test_notes/test_notes_serializer.py @@ -0,0 +1,66 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.notes import Notes, NoteModelSerializer + +from itam.models.device import Device + + + +class NotesValidationAPI( + TestCase, +): + + # model = ConfigGroups + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.device = Device.objects.create( + organization = self.organization, + name = 'notes-test' + ) + + # self.item_no_parent = self.model.objects.create( + # organization=organization, + # name = 'random title', + # config = { 'config_key': 'a value' } + # ) + + # self.item_has_parent = self.model.objects.create( + # organization=organization, + # name = 'random title two', + # parent = self.item_no_parent + # ) + + + + def test_serializer_validation_no_note(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = NoteModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['note'][0] == 'required' From 4841a36968985ae14f68fcc1094057cd49cdcdf1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 13:04:10 +0930 Subject: [PATCH 224/617] test(assistance): Notes API field checks ref: #15 #49 #248 #354 --- .../unit/test_notes/test_notes_api_v2.py | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 app/core/tests/unit/test_notes/test_notes_api_v2.py diff --git a/app/core/tests/unit/test_notes/test_notes_api_v2.py b/app/core/tests/unit/test_notes/test_notes_api_v2.py new file mode 100644 index 000000000..b0301a84d --- /dev/null +++ b/app/core/tests/unit/test_notes/test_notes_api_v2.py @@ -0,0 +1,517 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.notes import Notes + +from itam.models.device import Device +from itam.models.operating_system import OperatingSystem +from itam.models.software import Software + + + +class NotesAPI( + TestCase, + APITenancyObject +): + + model = Notes + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + self.device = Device.objects.create( + organization = self.organization, + name = 'notes-device' + ) + + self.operating_system = OperatingSystem.objects.create( + organization = self.organization, + name = 'notes-os' + ) + + self.software = Software.objects.create( + organization = self.organization, + name = 'notes-software' + ) + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + note = 'one', + device = self.device, + operatingsystem = self.operating_system, + software = self.software, + usercreated = self.view_user, + usermodified = self.view_user + ) + + self.second_item = self.model.objects.create( + organization = self.organization, + note = 'one_two', + device = self.device + ) + + self.url_view_kwargs = {'device_id': self.device.id, 'pk': self.item.id} + + + client = Client() + url = reverse('API:_api_v2_device_notes-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This test is a custome test case from a test with the same name. It + exists as this model does not have a model_notes field. + + model_notes field must exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This test is a custome test case from a test with the same name. It + exists as this model does not have a model_notes field. + + model_notes field must be str + """ + + pass + + + + def test_api_field_exists_note(self): + """ Test for existance of API Field + + model_nonotetes field must exist + """ + + assert 'note' in self.api_data + + + def test_api_field_type_note(self): + """ Test for type for API Field + + note field must be str + """ + + assert type(self.api_data['note']) is str + + + + def test_api_field_exists_usercreated(self): + """ Test for existance of API Field + + usercreated field must exist + """ + + assert 'usercreated' in self.api_data + + + def test_api_field_type_usercreated(self): + """ Test for type for API Field + + usercreated field must be dict + """ + + assert type(self.api_data['usercreated']) is dict + + + def test_api_field_exists_usercreated_id(self): + """ Test for existance of API Field + + usercreated.id field must exist + """ + + assert 'id' in self.api_data['usercreated'] + + + def test_api_field_type_usercreated_id(self): + """ Test for type for API Field + + usercreated.id field must be int + """ + + assert type(self.api_data['usercreated']['id']) is int + + + def test_api_field_exists_usercreated_display_name(self): + """ Test for existance of API Field + + usercreated.display_name field must exist + """ + + assert 'display_name' in self.api_data['usercreated'] + + + def test_api_field_type_usercreated_display_name(self): + """ Test for type for API Field + + usercreated.display_name field must be str + """ + + assert type(self.api_data['usercreated']['display_name']) is str + + + def test_api_field_exists_usercreated_url(self): + """ Test for existance of API Field + + usercreated.url field must exist + """ + + assert 'url' in self.api_data['usercreated'] + + + def test_api_field_type_usercreated_url(self): + """ Test for type for API Field + + usercreated.url field must be Hyperlink + """ + + assert type(self.api_data['usercreated']['url']) is Hyperlink + + + + def test_api_field_exists_usermodified(self): + """ Test for existance of API Field + + usermodified field must exist + """ + + assert 'usermodified' in self.api_data + + + def test_api_field_type_usermodified(self): + """ Test for type for API Field + + usermodified field must be dict + """ + + assert type(self.api_data['usermodified']) is dict + + + def test_api_field_exists_usermodified_id(self): + """ Test for existance of API Field + + usermodified.id field must exist + """ + + assert 'id' in self.api_data['usermodified'] + + + def test_api_field_type_usermodified_id(self): + """ Test for type for API Field + + usermodified.id field must be int + """ + + assert type(self.api_data['usermodified']['id']) is int + + + def test_api_field_exists_usermodified_display_name(self): + """ Test for existance of API Field + + usermodified.display_name field must exist + """ + + assert 'display_name' in self.api_data['usermodified'] + + + def test_api_field_type_usermodified_display_name(self): + """ Test for type for API Field + + usermodified.display_name field must be str + """ + + assert type(self.api_data['usermodified']['display_name']) is str + + + def test_api_field_exists_usermodified_url(self): + """ Test for existance of API Field + + usermodified.url field must exist + """ + + assert 'url' in self.api_data['usermodified'] + + + def test_api_field_type_usermodified_url(self): + """ Test for type for API Field + + usermodified.url field must be Hyperlink + """ + + assert type(self.api_data['usermodified']['url']) is Hyperlink + + + + def test_api_field_exists_device(self): + """ Test for existance of API Field + + device field must exist + """ + + assert 'device' in self.api_data + + + def test_api_field_type_device(self): + """ Test for type for API Field + + device field must be dict + """ + + assert type(self.api_data['device']) is dict + + + def test_api_field_exists_device_id(self): + """ Test for existance of API Field + + device.id field must exist + """ + + assert 'id' in self.api_data['device'] + + + def test_api_field_type_device_id(self): + """ Test for type for API Field + + device.id field must be int + """ + + assert type(self.api_data['device']['id']) is int + + + def test_api_field_exists_device_display_name(self): + """ Test for existance of API Field + + device.display_name field must exist + """ + + assert 'display_name' in self.api_data['device'] + + + def test_api_field_type_device_display_name(self): + """ Test for type for API Field + + device.display_name field must be str + """ + + assert type(self.api_data['device']['display_name']) is str + + + def test_api_field_exists_device_url(self): + """ Test for existance of API Field + + device.url field must exist + """ + + assert 'url' in self.api_data['device'] + + + def test_api_field_type_device_url(self): + """ Test for type for API Field + + device.url field must be str + """ + + assert type(self.api_data['device']['url']) is str + + + + def test_api_field_exists_operatingsystem(self): + """ Test for existance of API Field + + operatingsystemfield must exist + """ + + assert 'operatingsystem' in self.api_data + + + def test_api_field_type_operatingsystem(self): + """ Test for type for API Field + + operatingsystemfield must be dict + """ + + assert type(self.api_data['operatingsystem']) is dict + + + def test_api_field_exists_operatingsystem_id(self): + """ Test for existance of API Field + + operatingsystem.id field must exist + """ + + assert 'id' in self.api_data['operatingsystem'] + + + def test_api_field_type_operatingsystem_id(self): + """ Test for type for API Field + + operatingsystem.id field must be int + """ + + assert type(self.api_data['operatingsystem']['id']) is int + + + def test_api_field_exists_operatingsystem_display_name(self): + """ Test for existance of API Field + + operatingsystem.display_name field must exist + """ + + assert 'display_name' in self.api_data['operatingsystem'] + + + def test_api_field_type_operatingsystem_display_name(self): + """ Test for type for API Field + + operatingsystem.display_name field must be str + """ + + assert type(self.api_data['operatingsystem']['display_name']) is str + + + def test_api_field_exists_operatingsystem_url(self): + """ Test for existance of API Field + + operatingsystem.url field must exist + """ + + assert 'url' in self.api_data['operatingsystem'] + + + def test_api_field_type_operatingsystem_url(self): + """ Test for type for API Field + + operatingsystem.url field must be Hyperlink + """ + + assert type(self.api_data['operatingsystem']['url']) is Hyperlink + + + + + + def test_api_field_exists_software(self): + """ Test for existance of API Field + + software field must exist + """ + + assert 'software' in self.api_data + + + def test_api_field_type_software(self): + """ Test for type for API Field + + softwarefield must be dict + """ + + assert type(self.api_data['software']) is dict + + + def test_api_field_exists_software_id(self): + """ Test for existance of API Field + + software.id field must exist + """ + + assert 'id' in self.api_data['software'] + + + def test_api_field_type_software_id(self): + """ Test for type for API Field + + software.id field must be int + """ + + assert type(self.api_data['software']['id']) is int + + + def test_api_field_exists_software_display_name(self): + """ Test for existance of API Field + + software.display_name field must exist + """ + + assert 'display_name' in self.api_data['software'] + + + def test_api_field_type_software_display_name(self): + """ Test for type for API Field + + software.display_name field must be str + """ + + assert type(self.api_data['software']['display_name']) is str + + + def test_api_field_exists_software_url(self): + """ Test for existance of API Field + + software.url field must exist + """ + + assert 'url' in self.api_data['software'] + + + def test_api_field_type_software_url(self): + """ Test for type for API Field + + software.url field must be Hyperlink + """ + + assert type(self.api_data['software']['url']) is Hyperlink + From 94119b1f9fca434661e2d1654f5886f626af2e1f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 13:07:43 +0930 Subject: [PATCH 225/617] fix(core): notes field must be mandatory ref: #354 --- app/core/migrations/0012_alter_notes_note.py | 19 +++++++++++++++++++ app/core/models/notes.py | 5 ++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 app/core/migrations/0012_alter_notes_note.py diff --git a/app/core/migrations/0012_alter_notes_note.py b/app/core/migrations/0012_alter_notes_note.py new file mode 100644 index 000000000..c9e7c7c7e --- /dev/null +++ b/app/core/migrations/0012_alter_notes_note.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2024-10-19 02:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_alter_history_options_alter_history_action'), + ] + + operations = [ + migrations.AlterField( + model_name='notes', + name='note', + field=models.TextField(default='-', help_text='The tid bit you wish to add', verbose_name='Note'), + preserve_default=False, + ), + ] diff --git a/app/core/models/notes.py b/app/core/models/notes.py index af2252bbc..dcdd1b027 100644 --- a/app/core/models/notes.py +++ b/app/core/models/notes.py @@ -55,10 +55,9 @@ class Meta: note = models.TextField( - blank = True, - default = None, + blank = False, help_text = 'The tid bit you wish to add', - null = True, + null = False, verbose_name = 'Note', ) From 5d953771d78684fada6484f6b8e825ca86d262fb Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 13:46:56 +0930 Subject: [PATCH 226/617] feat(core): Add Manufacturer API v2 endpoint ref: #248 #354 --- app/api/urls.py | 5 +- .../0013_alter_manufacturer_modified.py | 20 ++++ app/core/models/manufacturer.py | 4 +- app/core/serializers/manufacturer.py | 98 +++++++++++++++++++ app/core/viewsets/manufacturer.py | 84 ++++++++++++++++ app/settings/viewsets/index.py | 10 ++ 6 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 app/core/migrations/0013_alter_manufacturer_modified.py create mode 100644 app/core/serializers/manufacturer.py create mode 100644 app/core/viewsets/manufacturer.py diff --git a/app/api/urls.py b/app/api/urls.py index e45d07e9c..1a56b2b44 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -60,8 +60,9 @@ ) from core.viewsets import ( + history as history_v2, notes as notes_v2, - history as history_v2 + manufacturer as manufacturer_v2 ) from itam.viewsets import ( @@ -150,6 +151,8 @@ router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') +router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') +router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') urlpatterns = [ diff --git a/app/core/migrations/0013_alter_manufacturer_modified.py b/app/core/migrations/0013_alter_manufacturer_modified.py new file mode 100644 index 000000000..7bc6c3b74 --- /dev/null +++ b/app/core/migrations/0013_alter_manufacturer_modified.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2024-10-19 03:58 + +import access.fields +import django.utils.timezone +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_alter_notes_note'), + ] + + operations = [ + migrations.AlterField( + model_name='manufacturer', + name='modified', + field=access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified'), + ), + ] diff --git a/app/core/models/manufacturer.py b/app/core/models/manufacturer.py index da08bf186..b62cff8ac 100644 --- a/app/core/models/manufacturer.py +++ b/app/core/models/manufacturer.py @@ -23,7 +23,7 @@ class Meta: created = AutoCreatedField() - modified = AutoCreatedField() + modified = AutoLastModifiedField() @@ -61,7 +61,7 @@ class Meta: "layout": "double", "left": [ 'organization', - 'name' + 'name', 'is_global', ], "right": [ diff --git a/app/core/serializers/manufacturer.py b/app/core/serializers/manufacturer.py new file mode 100644 index 000000000..e4e369a7a --- /dev/null +++ b/app/core/serializers/manufacturer.py @@ -0,0 +1,98 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from core.models.manufacturer import Manufacturer + + + +class ManufacturerBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_manufacturer-detail", format="html" + ) + + + class Meta: + + model = Manufacturer + + fields = [ + 'id', + 'display_name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url', + ] + + +class ManufacturerModelSerializer(ManufacturerBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_manufacturer-detail", + request=self._context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + # 'notes': reverse("API:_api_v2_manufacturer_notes-list", request=self._context['view'].request, kwargs={'manufacturer_id': item.pk}), + } + + + class Meta: + + model = Manufacturer + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class ManufacturerViewSerializer(ManufacturerModelSerializer): + + organization = OrganizationBaseSerializer( read_only = True ) diff --git a/app/core/viewsets/manufacturer.py b/app/core/viewsets/manufacturer.py new file mode 100644 index 000000000..86175c6ae --- /dev/null +++ b/app/core/viewsets/manufacturer.py @@ -0,0 +1,84 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from core.serializers.manufacturer import ( + Manufacturer, + ManufacturerModelSerializer, + ManufacturerViewSerializer +) + +from api.viewsets.common import ModelViewSet + + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a manufacturer', + description='', + responses = { + # 200: OpenApiResponse(description='Allready exists', response=ConfigGroupViewSerializer), + 201: OpenApiResponse(description='Created', response=ManufacturerViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a manufacturer', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all manufacturer', + description='', + responses = { + 200: OpenApiResponse(description='', response=ManufacturerViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single manufacturer', + description='', + responses = { + 200: OpenApiResponse(description='', response=ManufacturerViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a manufacturer', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ManufacturerViewSerializer), + # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), + # # 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelViewSet): + + filterset_fields = [ + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = Manufacturer + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index d59841adc..76cd2b1ff 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -26,6 +26,15 @@ class Index(CommonViewSet): } ] }, + { + "name": "Common", + "links": [ + { + "name": "Manufacturers", + "model": "manufacturer" + } + ] + }, { "name": "Core", "links": [ @@ -56,5 +65,6 @@ def list(self, request, pk=None): return Response( { "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), + "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), } ) From 3f41fc19d261bd2627033f8b80c66d8db0df825a Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 14:04:02 +0930 Subject: [PATCH 227/617] feat(core): Add Software Notes API v2 endpoint ref: #248 #354 --- app/api/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/urls.py b/app/api/urls.py index 1a56b2b44..785b62305 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -144,6 +144,7 @@ router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') +router.register('v2/itim/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') From 9884312d47cfc310c1003d1bea659a0593d39aa5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 14:04:30 +0930 Subject: [PATCH 228/617] feat(itim): Add Service Notes API v2 endpoint ref: #248 #354 --- app/api/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/urls.py b/app/api/urls.py index 785b62305..7edd5d230 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -147,6 +147,7 @@ router.register('v2/itim/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') +router.register('v2/itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') From 32ac01dc556e890fa981bb94f7ae1e6a550e1888 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 14:21:04 +0930 Subject: [PATCH 229/617] test(assistance): Manufacturer API field checks ref: #15 #248 #354 --- .../manufacturer/test_manufacturer_api_v2.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py diff --git a/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py b/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py new file mode 100644 index 000000000..fee76eaf7 --- /dev/null +++ b/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py @@ -0,0 +1,110 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + + + +class ManufacturerAPI( + TestCase, + APITenancyObject +): + + model = Manufacturer + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + model_notes = 'text' + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_manufacturer-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + + def test_api_field_exists_url_history(self): + """ Test for existance of API Field + + _urls.history field must exist + """ + + assert 'history' in self.api_data['_urls'] + + + def test_api_field_type_url_history(self): + """ Test for type for API Field + + _urls.history field must be str + """ + + assert type(self.api_data['_urls']['history']) is str From 5f4a09da25d9453828e1436592f89d533793bbad Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 14:31:27 +0930 Subject: [PATCH 230/617] test(core): Manufacturer Serializer Validation checks ref: #15 #248 #353 --- .../test_manufacturer_serializer.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/core/tests/unit/manufacturer/test_manufacturer_serializer.py diff --git a/app/core/tests/unit/manufacturer/test_manufacturer_serializer.py b/app/core/tests/unit/manufacturer/test_manufacturer_serializer.py new file mode 100644 index 000000000..5f57b839d --- /dev/null +++ b/app/core/tests/unit/manufacturer/test_manufacturer_serializer.py @@ -0,0 +1,74 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.manufacturer import Manufacturer, ManufacturerModelSerializer + + + +class ManufacturerValidationAPI( + TestCase, +): + + model = Manufacturer + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'random title', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ManufacturerModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_add_existing_manufacturer(self): + """Serializer Validation Check + + Ensure that if adding the same manufacturer + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = ManufacturerModelSerializer( + data={ + "organization": self.organization.id, + "name": self.item.name + } + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'unique' From d1c66318b22c2e9130e7a948cdcb6f8306bbf54d Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 14:37:39 +0930 Subject: [PATCH 231/617] test(core): Manufacturer API ViewSet permission checks ref: #15 #248 #354 --- .../manufacturer/test_manufacturer_viewset.py | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 app/core/tests/unit/manufacturer/test_manufacturer_viewset.py diff --git a/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py b/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py new file mode 100644 index 000000000..66447711f --- /dev/null +++ b/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py @@ -0,0 +1,181 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.manufacturer import Manufacturer + + + +class ManufacturerPermissionsAPI(TestCase, APIPermissions): + + model = Manufacturer + + app_namespace = 'API' + + url_name = '_api_v2_manufacturer' + + change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + + # self.url_kwargs = {} + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team_post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From f0b14cfa66ecab8e3fdeb58ae978b9d9e94b998c Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 15:01:35 +0930 Subject: [PATCH 232/617] feat(itam): Add Device API v2 endpoint ref: #248 #355 --- app/api/urls.py | 2 + app/itam/serializers/device.py | 101 +++++++++++++++++++++++++++++++++ app/itam/viewsets/device.py | 93 ++++++++++++++++++++++++++++++ app/itam/viewsets/index.py | 2 +- 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 app/itam/viewsets/device.py diff --git a/app/api/urls.py b/app/api/urls.py index 7edd5d230..99c5ca8af 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -67,6 +67,7 @@ from itam.viewsets import ( index as itam_index_v2, + device as device_v2, ) from itim.viewsets import ( @@ -142,6 +143,7 @@ router.register('v2/core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') +router.register('v2/itam/device', device_v2.ViewSet, basename='_api_v2_device') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') router.register('v2/itim/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index cbcec85a9..484ae461a 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -1,7 +1,18 @@ +import json + +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from rest_framework.reverse import reverse from rest_framework import serializers from access.serializers.organization import OrganizationBaseSerializer +from api.viewsets.common import ModelViewSet + +from core.fields.icon import Icon, IconField + +from access.serializers.organization import OrganizationBaseSerializer + from itam.models.device import Device @@ -29,3 +40,93 @@ class Meta: 'display_name', 'name', ] + +class DeviceModelSerializer(DeviceBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + } + + + rendered_config = serializers.JSONField(source='get_configuration', read_only=True) + + context = serializers.SerializerMethodField('get_cont') + + + def get_cont(self, item) -> dict: + + from django.core.serializers import serialize + + device = json.loads(serialize('json', [item])) + + fields = device[0]['fields'] + + fields.update({'id': device[0]['pk']}) + + context: dict = {} + + return context + + + def get_rendered_config(self, item): + + return item.get_configuration(0) + + + status_icon = IconField(read_only = True, label='') + + class Meta: + + model = Device + + fields = [ + 'id', + 'status_icon', + 'display_name', + 'name', + 'device_type', + 'model_notes', + 'serial_number', + 'uuid', + 'is_global', + 'is_virtual', + 'device_model', + 'config', + 'rendered_config', + 'inventorydate', + 'context', + 'created', + 'modified', + 'organization', + '_urls', + ] + + read_only_fields = [ + 'id', + 'context', + 'display_name', + 'inventorydate', + 'rendered_config', + 'created', + 'modified', + '_urls', + ] + + + +class DeviceViewSerializer(DeviceModelSerializer): + + organization = OrganizationBaseSerializer(many=False, read_only=True) diff --git a/app/itam/viewsets/device.py b/app/itam/viewsets/device.py new file mode 100644 index 000000000..7035de7c8 --- /dev/null +++ b/app/itam/viewsets/device.py @@ -0,0 +1,93 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itam.serializers.device import ( + Device, + DeviceModelSerializer, + DeviceViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a device', + description="""Add a new device to the ITAM database. + If you attempt to create a device and a device with a matching name and uuid or name and serial number + is found within the database, it will not re-create it. The device will be returned within the message body. + """, + responses = { + 200: OpenApiResponse(description='Device allready exists', response=DeviceViewSerializer), + 201: OpenApiResponse(description='Device created', response=DeviceViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a device', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all devices', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single device', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a device', + description = '', + responses = { + 200: OpenApiResponse(description='', response=DeviceViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Device """ + + filterset_fields = [ + 'name', + 'serial_number', + 'organization', + 'uuid', + ] + + search_fields = [ + 'name', + 'serial_number', + 'uuid', + ] + + model = Device + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] diff --git a/app/itam/viewsets/index.py b/app/itam/viewsets/index.py index 5667b44b8..7fce7b70d 100644 --- a/app/itam/viewsets/index.py +++ b/app/itam/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "device": reverse('API:_api_v2_device-list', request=request) + "device": reverse('API:_api_v2_device-list', request=request), } ) From bcb0ce42df4b3db11ea340c0e4facf4bb515ca15 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 15:45:16 +0930 Subject: [PATCH 233/617] feat(itam): Add Device Model API v2 endpoint ref: #248 #355 --- app/api/urls.py | 2 + app/itam/serializers/device.py | 7 ++- app/itam/serializers/device_model.py | 88 ++++++++++++++++++++++++++++ app/itam/viewsets/device_model.py | 88 ++++++++++++++++++++++++++++ app/settings/viewsets/index.py | 1 + 5 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 app/itam/serializers/device_model.py create mode 100644 app/itam/viewsets/device_model.py diff --git a/app/api/urls.py b/app/api/urls.py index 99c5ca8af..1141fe3f2 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -68,6 +68,7 @@ from itam.viewsets import ( index as itam_index_v2, device as device_v2, + device_model as device_model_v2, ) from itim.viewsets import ( @@ -154,6 +155,7 @@ router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') +router.register('v2/settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 484ae461a..882080660 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -11,7 +11,7 @@ from core.fields.icon import Icon, IconField -from access.serializers.organization import OrganizationBaseSerializer +from itam.serializers.device_model import DeviceModelBaseSerializer from itam.models.device import Device @@ -49,6 +49,7 @@ def get_url(self, item): return { '_self': reverse("API:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'device_model': reverse("API:_api_v2_device_model-list", request=self._context['view'].request), 'history': reverse( "API:_api_v2_model_history-list", request=self._context['view'].request, @@ -129,4 +130,6 @@ class Meta: class DeviceViewSerializer(DeviceModelSerializer): - organization = OrganizationBaseSerializer(many=False, read_only=True) + device_model = DeviceModelBaseSerializer( many = False, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itam/serializers/device_model.py b/app/itam/serializers/device_model.py new file mode 100644 index 000000000..637112031 --- /dev/null +++ b/app/itam/serializers/device_model.py @@ -0,0 +1,88 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.serializers.manufacturer import ManufacturerBaseSerializer + +from itam.models.device_models import DeviceModel + + + +class DeviceModelBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device_model-detail", format="html" + ) + + class Meta: + + model = DeviceModel + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class DeviceModelModelSerializer(DeviceModelBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, obj): + + return { + '_self': reverse("API:_api_v2_device_model-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) + } + + class Meta: + + model = DeviceModel + + fields = [ + 'id', + 'organization', + 'display_name', + 'manufacturer', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class DeviceModelViewSerializer(DeviceModelModelSerializer): + + manufacturer = ManufacturerBaseSerializer( many = False, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + diff --git a/app/itam/viewsets/device_model.py b/app/itam/viewsets/device_model.py new file mode 100644 index 000000000..b0644fc09 --- /dev/null +++ b/app/itam/viewsets/device_model.py @@ -0,0 +1,88 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itam.serializers.device_model import ( + DeviceModel, + DeviceModelModelSerializer, + DeviceModelViewSerializer +) + +from api.views.mixin import OrganizationPermissionAPI + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a device model', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=DeviceModelViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a device model', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all device models', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceModelViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single device model', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceModelViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a device model', + description = '', + responses = { + 200: OpenApiResponse(description='', response=DeviceModelViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Device Model """ + + filterset_fields = [ + 'name', + 'manufacturer', + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = DeviceModel + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Device Models' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 76cd2b1ff..f2092e654 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -64,6 +64,7 @@ def list(self, request, pk=None): return Response( { + "device_model": reverse('API:_api_v2_device_model-list', request=request), "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), } From e54d7cfeb209fbd85e623af1a036aa2f8b0d89aa Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 16:22:20 +0930 Subject: [PATCH 234/617] feat(itam): Add Software API v2 endpoint ref: #248 #355 --- app/api/urls.py | 4 + app/itam/serializers/software.py | 79 ++++++++++++++++++- app/itam/serializers/software_category.py | 92 +++++++++++++++++++++++ app/itam/viewsets/index.py | 1 + app/itam/viewsets/software.py | 86 +++++++++++++++++++++ app/itam/viewsets/software_category.py | 84 +++++++++++++++++++++ app/settings/viewsets/index.py | 5 ++ 7 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 app/itam/serializers/software_category.py create mode 100644 app/itam/viewsets/software.py create mode 100644 app/itam/viewsets/software_category.py diff --git a/app/api/urls.py b/app/api/urls.py index 1141fe3f2..a236080c8 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -69,6 +69,8 @@ index as itam_index_v2, device as device_v2, device_model as device_model_v2, + software as software_v2, + software_category as software_category_v2 ) from itim.viewsets import ( @@ -147,6 +149,7 @@ router.register('v2/itam/device', device_v2.ViewSet, basename='_api_v2_device') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') +router.register('v2/itam/software', software_v2.ViewSet, basename='_api_v2_software') router.register('v2/itim/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') @@ -159,6 +162,7 @@ router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') +router.register('v2/settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') urlpatterns = [ diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 0d6318a0a..859600b25 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -1,9 +1,12 @@ -from rest_framework import serializers from rest_framework.reverse import reverse +from rest_framework import serializers from access.serializers.organization import OrganizationBaseSerializer +from core.serializers.manufacturer import ManufacturerBaseSerializer + from itam.models.software import Software +from itam.serializers.software_category import SoftwareCategoryBaseSerializer @@ -15,6 +18,10 @@ def get_display_name(self, item): return str( item ) + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device-detail", format="html" + ) + class Meta: model = Software @@ -23,10 +30,80 @@ class Meta: 'id', 'display_name', 'name', + 'url' ] read_only_fields = [ 'id', 'display_name', 'name', + 'url' + ] + + +class SoftwareModelSerializer(SoftwareBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_software-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_software_notes-list", request=self._context['view'].request, kwargs={'software_id': item.pk}), + 'publisher': reverse("API:_api_v2_manufacturer-list", request=self._context['view'].request), + 'services': 'ToDo', + 'tickets': 'ToDo' + } + + + def get_rendered_config(self, item): + + return item.get_configuration(0) + + + class Meta: + + model = Software + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'publisher', + 'display_name', + 'name', + 'category', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class SoftwareViewSerializer(SoftwareModelSerializer): + + category = SoftwareCategoryBaseSerializer( many = False, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + publisher = ManufacturerBaseSerializer( many = False, read_only = True ) diff --git a/app/itam/serializers/software_category.py b/app/itam/serializers/software_category.py new file mode 100644 index 000000000..340ae0ced --- /dev/null +++ b/app/itam/serializers/software_category.py @@ -0,0 +1,92 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.serializers.manufacturer import ManufacturerBaseSerializer + +from itam.models.software import SoftwareCategory + + + +class SoftwareCategoryBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_software_category-detail", format="html" + ) + + class Meta: + + model = SoftwareCategory + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class SoftwareCategoryModelSerializer(SoftwareCategoryBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_software_category-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': 'ToDo', + 'notes': 'ToDo', + } + + + def get_rendered_config(self, item): + + return item.get_configuration(0) + + + class Meta: + + model = SoftwareCategory + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class SoftwareCategoryViewSerializer(SoftwareCategoryModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itam/viewsets/index.py b/app/itam/viewsets/index.py index 7fce7b70d..bb6e6066e 100644 --- a/app/itam/viewsets/index.py +++ b/app/itam/viewsets/index.py @@ -26,5 +26,6 @@ def list(self, request, pk=None): return Response( { "device": reverse('API:_api_v2_device-list', request=request), + "software": reverse('API:_api_v2_software-list', request=request) } ) diff --git a/app/itam/viewsets/software.py b/app/itam/viewsets/software.py new file mode 100644 index 000000000..7168142a3 --- /dev/null +++ b/app/itam/viewsets/software.py @@ -0,0 +1,86 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from itam.serializers.software import ( + Software, + SoftwareModelSerializer, + SoftwareViewSerializer +) +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a software', + description='', + responses = { + 200: OpenApiResponse(description='Software allready exists', response=SoftwareViewSerializer), + 201: OpenApiResponse(description='Software created', response=SoftwareViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a software', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all softwares', + description='', + responses = { + 200: OpenApiResponse(description='', response=SoftwareViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single software', + description='', + responses = { + 200: OpenApiResponse(description='', response=SoftwareViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a software', + description = '', + responses = { + 200: OpenApiResponse(description='', response=SoftwareViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Software """ + + filterset_fields = [ + 'category', + 'organization', + 'publisher', + ] + + search_fields = [ + 'name', + ] + + model = Software + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Softwares' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] diff --git a/app/itam/viewsets/software_category.py b/app/itam/viewsets/software_category.py new file mode 100644 index 000000000..92d662f08 --- /dev/null +++ b/app/itam/viewsets/software_category.py @@ -0,0 +1,84 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from itam.serializers.software_category import ( + SoftwareCategory, + SoftwareCategoryModelSerializer, + SoftwareCategoryViewSerializer +) +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a software category', + description='', + responses = { + 201: OpenApiResponse(description='Software created', response=SoftwareCategoryViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a software category', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all software categories', + description='', + responses = { + 200: OpenApiResponse(description='', response=SoftwareCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single software category', + description='', + responses = { + 200: OpenApiResponse(description='', response=SoftwareCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a software category', + description = '', + responses = { + 200: OpenApiResponse(description='', response=SoftwareCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Software """ + + filterset_fields = [ + 'is_global', + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = SoftwareCategory + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Softwares' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index f2092e654..3b7fc8248 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -50,6 +50,10 @@ class Index(CommonViewSet): { "name": "Device Model", "model": "device_model" + }, + { + "name": "Software Category", + "model": "software_category" } ] } @@ -67,5 +71,6 @@ def list(self, request, pk=None): "device_model": reverse('API:_api_v2_device_model-list', request=request), "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), + "software_category": reverse('API:_api_v2_software_category-list', request=request), } ) From 947112ba39ff5e2ebfedc61a7fd8454470d61a44 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 16:22:43 +0930 Subject: [PATCH 235/617] feat(itam): Depreciate API v1 device endpoint ref: #248 #355 --- app/api/views/itam/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/itam/device.py b/app/api/views/itam/device.py index c1939044f..c52fac372 100644 --- a/app/api/views/itam/device.py +++ b/app/api/views/itam/device.py @@ -14,7 +14,7 @@ from itam.models.device import Device - +@extend_schema( deprecated = True ) class DeviceViewSet(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ From 318d342d2bdc047e5166e0ea94595681f4e453a9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 17:11:58 +0930 Subject: [PATCH 236/617] feat(itam): Add Software Version API v2 endpoint ref: #248 #355 --- app/api/react_ui_metadata.py | 5 ++ app/api/urls.py | 6 +- app/itam/models/software.py | 15 +++- app/itam/serializers/software.py | 7 ++ app/itam/serializers/software_version.py | 101 ++++++++++++++++++++++- app/itam/viewsets/software_version.py | 97 ++++++++++++++++++++++ 6 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 app/itam/viewsets/software_version.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 07f326e2c..8b68e39b8 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -135,6 +135,11 @@ def determine_metadata(self, request, view): "name": "device", "icon": "device", "link": "/itam/device" + }, + { + "display_name": "Software", + "name": "software", + "link": "/itam/software" } ] }, diff --git a/app/api/urls.py b/app/api/urls.py index a236080c8..359473378 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -70,7 +70,8 @@ device as device_v2, device_model as device_model_v2, software as software_v2, - software_category as software_category_v2 + software_category as software_category_v2, + software_version as software_version_v2, ) from itim.viewsets import ( @@ -150,7 +151,8 @@ router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') router.register('v2/itam/software', software_v2.ViewSet, basename='_api_v2_software') -router.register('v2/itim/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') +router.register('v2/itam/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') +router.register('v2/itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') router.register('v2/itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 0b1de3a3d..fec872104 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -169,7 +169,7 @@ class Meta: "sections": [ { "layout": "table", - "field": "versions", + "field": "software_version", } ] }, @@ -242,6 +242,12 @@ class SoftwareVersion(SoftwareCommonFields, SaveHistory): class Meta: + ordering = [ + 'name' + ] + + verbose_name = 'Software Version' + verbose_name_plural = 'Software Versions' @@ -260,6 +266,13 @@ class Meta: verbose_name = 'Name' ) + table_fields: list = [ + 'name', + 'organization', + 'created', + 'modified', + ] + @property def parent_object(self): diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 859600b25..131219f9b 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -61,6 +61,13 @@ def get_url(self, item): 'notes': reverse("API:_api_v2_software_notes-list", request=self._context['view'].request, kwargs={'software_id': item.pk}), 'publisher': reverse("API:_api_v2_manufacturer-list", request=self._context['view'].request), 'services': 'ToDo', + 'software_version': reverse( + "API:_api_v2_software_version-list", + request=self._context['view'].request, + kwargs={ + 'software_id': item.pk + } + ), 'tickets': 'ToDo' } diff --git a/app/itam/serializers/software_version.py b/app/itam/serializers/software_version.py index db58aea7c..c6e18c528 100644 --- a/app/itam/serializers/software_version.py +++ b/app/itam/serializers/software_version.py @@ -2,7 +2,9 @@ from rest_framework import serializers from access.serializers.organization import OrganizationBaseSerializer -from itam.models.software import SoftwareVersion + +from itam.models.software import Software, SoftwareVersion +from itam.serializers.software import SoftwareBaseSerializer @@ -14,6 +16,21 @@ def get_display_name(self, item): return str( item ) + + url = serializers.SerializerMethodField('my_url') + + def my_url(self, item): + + return reverse( + "API:_api_v2_software_version-detail", + request=self._context['view'].request, + kwargs={ + 'software_id': item.software.pk, + 'pk': item.pk + } + ) + + class Meta: model = SoftwareVersion @@ -22,10 +39,92 @@ class Meta: 'id', 'display_name', 'name', + 'url', ] read_only_fields = [ 'id', 'display_name', 'name', + 'url', + ] + + +class SoftwareVersionModelSerializer(SoftwareVersionBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + "API:_api_v2_software_version-detail", + request=self._context['view'].request, + kwargs={ + 'software_id': item.software.pk, + 'pk': item.pk + } + ), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'tickets': 'ToDo' + } + + + class Meta: + + model = SoftwareVersion + + fields = [ + 'id', + 'display_name', + 'organization', + 'software', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', ] + + read_only_fields = [ + 'id', + 'display_name', + 'organization', + 'software', + 'created', + 'modified', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'view' in self._context: + + if 'software_id' in self._context['view'].kwargs: + + software = Software.objects.get( id = self._context['view'].kwargs['software_id'] ) + + self.validated_data['software'] = software + self.validated_data['organization'] = software.organization + + return is_valid + + + +class SoftwareVersionViewSerializer(SoftwareVersionModelSerializer): + + software = SoftwareBaseSerializer( many = False, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itam/viewsets/software_version.py b/app/itam/viewsets/software_version.py new file mode 100644 index 000000000..baa92fc27 --- /dev/null +++ b/app/itam/viewsets/software_version.py @@ -0,0 +1,97 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from itam.serializers.software_version import ( + SoftwareVersion, + SoftwareVersionModelSerializer, + SoftwareVersionViewSerializer +) +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a software version', + description='', + responses = { + 201: OpenApiResponse(description='Software created', response=SoftwareVersionViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a software version', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all software versions', + description='', + responses = { + 200: OpenApiResponse(description='', response=SoftwareVersionViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single software version', + description='', + responses = { + 200: OpenApiResponse(description='', response=SoftwareVersionViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a software version', + description = '', + responses = { + 200: OpenApiResponse(description='', response=SoftwareVersionViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Software """ + + filterset_fields = [ + 'is_global', + 'organization', + 'software', + ] + + search_fields = [ + 'name', + ] + + model = SoftwareVersion + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Softwares' + + + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter(software_id=self.kwargs['software_id']) + + self.queryset = queryset + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer'] From 1daa0ca2195e98d23078f5ab0d46907f3f082203 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 20:20:17 +0930 Subject: [PATCH 237/617] feat(itam): Add Device Type API v2 endpoint ref: #248 #355 --- app/api/urls.py | 1 + app/itam/serializers/device_type.py | 83 +++++++++++++++++++++++++++ app/itam/viewsets/device_type.py | 87 +++++++++++++++++++++++++++++ app/settings/viewsets/index.py | 5 ++ 4 files changed, 176 insertions(+) create mode 100644 app/itam/serializers/device_type.py create mode 100644 app/itam/viewsets/device_type.py diff --git a/app/api/urls.py b/app/api/urls.py index 359473378..7dcf5cc29 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -161,6 +161,7 @@ router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') router.register('v2/settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') +router.register('v2/settings/device_type', device_type_v2.ViewSet, basename='_api_v2_device_type') router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') diff --git a/app/itam/serializers/device_type.py b/app/itam/serializers/device_type.py new file mode 100644 index 000000000..8a08841ba --- /dev/null +++ b/app/itam/serializers/device_type.py @@ -0,0 +1,83 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.models.device import DeviceType + + + +class DeviceTypeBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device_type-detail", format="html" + ) + + class Meta: + + model = DeviceType + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class DeviceTypeModelSerializer(DeviceTypeBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, obj): + + return { + '_self': reverse("API:_api_v2_device_type-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) + } + + + class Meta: + + model = DeviceType + + fields = [ + 'id', + 'display_name', + 'organization', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'inventorydate', + 'created', + 'modified', + '_urls', + ] + + + +class DeviceTypeViewSerializer(DeviceTypeModelSerializer): + + organization = OrganizationBaseSerializer(many=False, read_only=True) + diff --git a/app/itam/viewsets/device_type.py b/app/itam/viewsets/device_type.py new file mode 100644 index 000000000..35786d8c8 --- /dev/null +++ b/app/itam/viewsets/device_type.py @@ -0,0 +1,87 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itam.serializers.device_type import ( + DeviceType, + DeviceTypeModelSerializer, + DeviceTypeViewSerializer +) + +from api.views.mixin import OrganizationPermissionAPI + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a device type', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=DeviceTypeViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a device type', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all device types', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceTypeViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single device type', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceTypeViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a device type', + description = '', + responses = { + 200: OpenApiResponse(description='', response=DeviceTypeViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Device Type """ + + filterset_fields = [ + 'is_global', + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = DeviceType + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Device Models' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 3b7fc8248..a263eabfc 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -51,6 +51,10 @@ class Index(CommonViewSet): "name": "Device Model", "model": "device_model" }, + { + "name": "Device Type", + "model": "device_type" + }, { "name": "Software Category", "model": "software_category" @@ -69,6 +73,7 @@ def list(self, request, pk=None): return Response( { "device_model": reverse('API:_api_v2_device_model-list', request=request), + "device_type": reverse('API:_api_v2_device_type-list', request=request), "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), "software_category": reverse('API:_api_v2_software_category-list', request=request), From 4dc0f31e69223276246e1d23bce5957a7412f369 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 20:27:48 +0930 Subject: [PATCH 238/617] feat(itam): Add Device API v2 endpoint ref: #248 #355 --- app/api/urls.py | 1 + app/itam/models/device.py | 2 +- app/itam/serializers/device.py | 13 +++++- app/itam/viewset/device.py | 78 ++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 app/itam/viewset/device.py diff --git a/app/api/urls.py b/app/api/urls.py index 7dcf5cc29..5596fd641 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -69,6 +69,7 @@ index as itam_index_v2, device as device_v2, device_model as device_model_v2, + device_type as device_type_v2, software as software_v2, software_category as software_category_v2, software_version as software_version_v2, diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 5c07b0a48..05269d6e9 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -46,7 +46,7 @@ class Meta: "layout": "double", "left": [ 'organization', - 'name' + 'name', 'is_global', ], "right": [ diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 882080660..7baecb9a6 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -11,9 +11,10 @@ from core.fields.icon import Icon, IconField +from itam.models.device import Device from itam.serializers.device_model import DeviceModelBaseSerializer +from itam.serializers.device_type import DeviceTypeBaseSerializer -from itam.models.device import Device @@ -25,6 +26,10 @@ def get_display_name(self, item): return str( item ) + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device-detail", format="html" + ) + class Meta: model = Device @@ -33,12 +38,14 @@ class Meta: 'id', 'display_name', 'name', + 'url', ] read_only_fields = [ 'id', 'display_name', 'name', + 'url', ] class DeviceModelSerializer(DeviceBaseSerializer): @@ -50,6 +57,7 @@ def get_url(self, item): return { '_self': reverse("API:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'device_model': reverse("API:_api_v2_device_model-list", request=self._context['view'].request), + 'device_type': reverse("API:_api_v2_device_type-list", request=self._context['view'].request), 'history': reverse( "API:_api_v2_model_history-list", request=self._context['view'].request, @@ -59,6 +67,7 @@ def get_url(self, item): } ), 'notes': reverse("API:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'software': reverse("API:_api_v2_device_software-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), } @@ -132,4 +141,6 @@ class DeviceViewSerializer(DeviceModelSerializer): device_model = DeviceModelBaseSerializer( many = False, read_only = True ) + device_type = DeviceTypeBaseSerializer( many = False, read_only = True ) + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itam/viewset/device.py b/app/itam/viewset/device.py new file mode 100644 index 000000000..8af85f18e --- /dev/null +++ b/app/itam/viewset/device.py @@ -0,0 +1,78 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itam.serializers.device import ( + Device, + DeviceModelSerializer, + DeviceViewSerializer +) + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a device', + description="""Add a new device to the ITAM database. + If you attempt to create a device and a device with a matching name and uuid or name and serial number + is found within the database, it will not re-create it. The device will be returned within the message body. + """, + responses = { + 200: OpenApiResponse(description='Device allready exists', response=DeviceViewSerializer), + 201: OpenApiResponse(description='Device created', response=DeviceViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a device', + description = '' + ), + list = extend_schema( + summary = 'Fetch all devices', + description='Fetch devices that are from the users assigned organization(s)', + # methods=["GET"] + ), + retrieve = extend_schema( + summary = 'Fetch a single device', + description='Fetch the selected device', + # methods=["GET"] + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a device', + description = '' + ), +) +class ViewSet( ModelViewSet ): + """ Device """ + + filterset_fields = [ + 'name', + 'serial_number', + 'organization', + 'uuid', + ] + + search_fields = [ + 'name', + 'serial_number', + 'uuid', + ] + + model = Device + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] From 538265f5263b4f8ed04ec4677bf84954111ea4a1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 21:39:28 +0930 Subject: [PATCH 239/617] feat(itam): Add Device Software API v2 endpoint ref: #248 #355 --- app/api/urls.py | 2 + app/itam/models/device.py | 12 -- app/itam/serializers/device_software.py | 136 +++++++++++++++++++++++ app/itam/serializers/software.py | 2 +- app/itam/serializers/software_version.py | 2 +- app/itam/viewsets/device_software.py | 114 +++++++++++++++++++ 6 files changed, 254 insertions(+), 14 deletions(-) create mode 100644 app/itam/serializers/device_software.py create mode 100644 app/itam/viewsets/device_software.py diff --git a/app/api/urls.py b/app/api/urls.py index 5596fd641..6c8e968ef 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -70,6 +70,7 @@ device as device_v2, device_model as device_model_v2, device_type as device_type_v2, + device_software as device_software_v2, software as software_v2, software_category as software_category_v2, software_version as software_version_v2, @@ -149,6 +150,7 @@ router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('v2/itam/device', device_v2.ViewSet, basename='_api_v2_device') +router.register('v2/itam/device/(?P[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') router.register('v2/itam/software', software_v2.ViewSet, basename='_api_v2_software') diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 05269d6e9..656689d2d 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -596,18 +596,6 @@ def action_badge(self): ) - @property - def category(self): - - category = None - - if self.software: - - category = self.software.category.id - - return category - - @property def parent_object(self): """ Fetch the parent object """ diff --git a/app/itam/serializers/device_software.py b/app/itam/serializers/device_software.py new file mode 100644 index 000000000..6671c410c --- /dev/null +++ b/app/itam/serializers/device_software.py @@ -0,0 +1,136 @@ +from rest_framework.fields import empty +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.fields.badge import Badge, BadgeField + +from itam.models.device import Device, DeviceSoftware +from itam.serializers.device import DeviceBaseSerializer +from itam.serializers.software import SoftwareBaseSerializer +from itam.serializers.software_category import SoftwareCategoryBaseSerializer +from itam.serializers.software_version import SoftwareVersionBaseSerializer + + + +class DeviceSoftwareBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device_software-detail", format="html" + ) + + + class Meta: + + model = DeviceSoftware + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class DeviceSoftwareModelSerializer(DeviceSoftwareBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, obj): + + return { + '_self': reverse( + "API:_api_v2_device_software-detail", + request=self._context['view'].request, + kwargs={ + 'device_id': self._context['view'].kwargs['device_id'], + 'pk': obj.pk + } + ) + } + + + action_badge = BadgeField(label='Action') + + category = SoftwareCategoryBaseSerializer(many=False, read_only=True, source='software.category') + + + class Meta: + + model = DeviceSoftware + + fields = [ + 'id', + 'organization', + 'device', + 'software', + 'category', + 'action', + 'action_badge', + 'version', + 'installedversion', + 'installed', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'category', + 'device', + 'installed', + 'installedversion', + 'organization', + 'created', + 'modified', + '_urls', + ] + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'view' in self._context: + + if 'device_id' in self._context['view'].kwargs: + + device = Device.objects.get(id=self._context['view'].kwargs['device_id']) + + self.validated_data['device'] = device + self.validated_data['organization'] = device.organization + + return is_valid + + + +class DeviceSoftwareViewSerializer(DeviceSoftwareModelSerializer): + + device = DeviceBaseSerializer(many=False, read_only=True) + + installedversion = SoftwareVersionBaseSerializer(many=False, read_only=True) + + organization = OrganizationBaseSerializer(many=False, read_only=True) + + software = SoftwareBaseSerializer(many=False, read_only=True) + + version = SoftwareVersionBaseSerializer(many=False, read_only=True) + diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 131219f9b..f7dbe3429 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device-detail", format="html" + view_name="API:_api_v2_software-detail", format="html" ) class Meta: diff --git a/app/itam/serializers/software_version.py b/app/itam/serializers/software_version.py index c6e18c528..77b1d778b 100644 --- a/app/itam/serializers/software_version.py +++ b/app/itam/serializers/software_version.py @@ -23,7 +23,7 @@ def my_url(self, item): return reverse( "API:_api_v2_software_version-detail", - request=self._context['view'].request, + request=self.context['view'].request, kwargs={ 'software_id': item.software.pk, 'pk': item.pk diff --git a/app/itam/viewsets/device_software.py b/app/itam/viewsets/device_software.py new file mode 100644 index 000000000..ae74f0aff --- /dev/null +++ b/app/itam/viewsets/device_software.py @@ -0,0 +1,114 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters + +from django.db.models import Q +from django.shortcuts import get_object_or_404 + +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from rest_framework.fields import empty +from rest_framework import generics, viewsets +from rest_framework.response import Response + +from access.mixin import OrganizationMixin + +from api.views.mixin import OrganizationPermissionAPI + +from api.viewsets.common import ModelViewSet + +from itam.serializers.device_software import ( + DeviceSoftware, + DeviceSoftwareModelSerializer, + DeviceSoftwareViewSerializer +) + + + + +@extend_schema_view( + create=extend_schema( + summary = 'Add device software', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=DeviceSoftwareModelSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a device software', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all device software', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceSoftwareModelSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single device software', + description='', + responses = { + 200: OpenApiResponse(description='', response=DeviceSoftwareModelSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a device software', + description = '', + responses = { + 200: OpenApiResponse(description='', response=DeviceSoftwareModelSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Device Model """ + + filterset_fields = [ + 'action', + 'software__category', + 'organization', + 'software', + ] + + search_fields = [ + 'name', + ] + + model = DeviceSoftware + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Device Models' + + + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter(device_id=self.kwargs['device_id']) + + self.queryset = queryset + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] From 09b2ea378bb9a275efce90a92b83fc7f82ebfc00 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 21:58:01 +0930 Subject: [PATCH 240/617] feat(core): Add External Link API v2 endpoint ref: #248 #355 --- app/api/urls.py | 2 + app/itam/serializers/device.py | 1 + app/itam/serializers/software.py | 1 + app/settings/models/external_link.py | 4 +- app/settings/serializers/external_links.py | 96 +++++++++++++++++++ .../viewsets/external_link.py} | 63 ++++++------ app/settings/viewsets/index.py | 1 + 7 files changed, 133 insertions(+), 35 deletions(-) create mode 100644 app/settings/serializers/external_links.py rename app/{itam/viewset/device.py => settings/viewsets/external_link.py} (51%) diff --git a/app/api/urls.py b/app/api/urls.py index 6c8e968ef..a603cb6f9 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -85,6 +85,7 @@ ) from settings.viewsets import ( + external_link as external_link_v2, index as settings_index_v2, ) @@ -165,6 +166,7 @@ router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') router.register('v2/settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') router.register('v2/settings/device_type', device_type_v2.ViewSet, basename='_api_v2_device_type') +router.register('v2/settings/external_link', external_link_v2.ViewSet, basename='_api_v2_external_link') router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 7baecb9a6..610779529 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -58,6 +58,7 @@ def get_url(self, item): '_self': reverse("API:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'device_model': reverse("API:_api_v2_device_model-list", request=self._context['view'].request), 'device_type': reverse("API:_api_v2_device_type-list", request=self._context['view'].request), + 'external_links': reverse("API:_api_v2_external_link-list", request=self._context['view'].request) + '?devices=true', 'history': reverse( "API:_api_v2_model_history-list", request=self._context['view'].request, diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index f7dbe3429..8babf786e 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -50,6 +50,7 @@ def get_url(self, item): return { '_self': reverse("API:_api_v2_software-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'external_links': reverse("API:_api_v2_external_link-list", request=self._context['view'].request) + '?software=true', 'history': reverse( "API:_api_v2_model_history-list", request=self._context['view'].request, diff --git a/app/settings/models/external_link.py b/app/settings/models/external_link.py index d2212153a..a30602401 100644 --- a/app/settings/models/external_link.py +++ b/app/settings/models/external_link.py @@ -97,7 +97,7 @@ class Meta: 'is_global', ], "right": [ - 'model_notes' + 'model_notes', 'created', 'modified', ] @@ -111,8 +111,6 @@ class Meta: ], "right": [ 'devices' - 'created', - 'modified', ] } ] diff --git a/app/settings/serializers/external_links.py b/app/settings/serializers/external_links.py new file mode 100644 index 000000000..97d472bcf --- /dev/null +++ b/app/settings/serializers/external_links.py @@ -0,0 +1,96 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from settings.models.external_link import ExternalLink + + + +class ExternalLinkBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_external_link-detail", format="html" + ) + + class Meta: + + model = ExternalLink + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class ExternalLinkModelSerializer(ExternalLinkBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_external_link-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + } + + + class Meta: + + model = ExternalLink + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'template', + 'colour', + 'cluster', + 'devices', + 'software', + 'model_notes', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + +class ExternalLinkViewSerializer(ExternalLinkModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itam/viewset/device.py b/app/settings/viewsets/external_link.py similarity index 51% rename from app/itam/viewset/device.py rename to app/settings/viewsets/external_link.py index 8af85f18e..a02f6b465 100644 --- a/app/itam/viewset/device.py +++ b/app/settings/viewsets/external_link.py @@ -2,11 +2,8 @@ from api.viewsets.common import ModelViewSet -from itam.serializers.device import ( - Device, - DeviceModelSerializer, - DeviceViewSerializer -) +from settings.serializers.external_links import ExternalLink, ExternalLinkModelSerializer, ExternalLinkViewSerializer + @extend_schema_view( @@ -17,53 +14,55 @@ is found within the database, it will not re-create it. The device will be returned within the message body. """, responses = { - 200: OpenApiResponse(description='Device allready exists', response=DeviceViewSerializer), - 201: OpenApiResponse(description='Device created', response=DeviceViewSerializer), + 201: OpenApiResponse(description='Device created', response=ExternalLinkViewSerializer), 400: OpenApiResponse(description='Validation failed.'), 403: OpenApiResponse(description='User is missing create permissions'), } ), destroy = extend_schema( summary = 'Delete a device', - description = '' + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } ), list = extend_schema( summary = 'Fetch all devices', - description='Fetch devices that are from the users assigned organization(s)', - # methods=["GET"] + description='', + responses = { + 200: OpenApiResponse(description='', response=ExternalLinkViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } ), retrieve = extend_schema( summary = 'Fetch a single device', - description='Fetch the selected device', - # methods=["GET"] + description='', + responses = { + 200: OpenApiResponse(description='', response=ExternalLinkViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } ), update = extend_schema(exclude = True), partial_update = extend_schema( summary = 'Update a device', - description = '' + description = '', + responses = { + 200: OpenApiResponse(description='', response=ExternalLinkViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } ), ) -class ViewSet( ModelViewSet ): - """ Device """ +class ViewSet(ModelViewSet): - filterset_fields = [ - 'name', - 'serial_number', - 'organization', - 'uuid', - ] + model = ExternalLink - search_fields = [ - 'name', - 'serial_number', - 'uuid', + filterset_fields = [ + 'cluster', + 'devices', + 'software', ] - model = Device - - documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' - - view_description = 'Physical Devices' def get_serializer_class(self): @@ -72,7 +71,7 @@ def get_serializer_class(self): or self.action == 'retrieve' ): - return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] - return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index a263eabfc..1e9c77e9a 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -74,6 +74,7 @@ def list(self, request, pk=None): { "device_model": reverse('API:_api_v2_device_model-list', request=request), "device_type": reverse('API:_api_v2_device_type-list', request=request), + "external_link": reverse('API:_api_v2_external_link-list', request=request), "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), "software_category": reverse('API:_api_v2_software_category-list', request=request), From 873dc71c0820f089db3c46ada1b280f59f1061ce Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 19 Oct 2024 22:10:58 +0930 Subject: [PATCH 241/617] test: enure correct type checks for url ref: #15 #248 #354 --- .../test_config_groups_software_api_v2.py | 4 ++-- app/core/tests/unit/test_notes/test_notes_api_v2.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py index 0ec03979b..85861be8d 100644 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py @@ -356,7 +356,7 @@ def test_api_field_exists_version_url(self): def test_api_field_type_version_url(self): """ Test for type for API Field - version.url field must be Hyperlink + version.url field must be str """ - assert type(self.api_data['version']['url']) is Hyperlink + assert type(self.api_data['version']['url']) is str diff --git a/app/core/tests/unit/test_notes/test_notes_api_v2.py b/app/core/tests/unit/test_notes/test_notes_api_v2.py index b0301a84d..f32af6887 100644 --- a/app/core/tests/unit/test_notes/test_notes_api_v2.py +++ b/app/core/tests/unit/test_notes/test_notes_api_v2.py @@ -362,10 +362,10 @@ def test_api_field_exists_device_url(self): def test_api_field_type_device_url(self): """ Test for type for API Field - device.url field must be str + device.url field must be Hyperlink """ - assert type(self.api_data['device']['url']) is str + assert type(self.api_data['device']['url']) is Hyperlink From 5ac063d8c91307dea8aa7d65cb4e33eb5ac3870b Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 13:09:39 +0930 Subject: [PATCH 242/617] feat(core): Add Operating System API v2 endpoint ref: #248 #355 --- app/api/urls.py | 4 +- app/itam/models/operating_system.py | 2 +- app/itam/serializers/operating_system.py | 70 +++++++++++++++++++ app/itam/viewsets/index.py | 1 + app/itam/viewsets/operating_system.py | 87 ++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 app/itam/viewsets/operating_system.py diff --git a/app/api/urls.py b/app/api/urls.py index a603cb6f9..b46b8432d 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -71,6 +71,7 @@ device_model as device_model_v2, device_type as device_type_v2, device_software as device_software_v2, + operating_system as operating_system_v2, software as software_v2, software_category as software_category_v2, software_version as software_version_v2, @@ -153,7 +154,8 @@ router.register('v2/itam/device', device_v2.ViewSet, basename='_api_v2_device') router.register('v2/itam/device/(?P[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') -router.register('v2/itim/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') +router.register('v2/itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system') +router.register('v2/itam/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') router.register('v2/itam/software', software_v2.ViewSet, basename='_api_v2_software') router.register('v2/itam/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') router.register('v2/itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 00d72b94e..791ebe48c 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -79,7 +79,7 @@ class Meta: "left": [ 'organization', 'publisher', - 'name' + 'name', 'is_global', ], "right": [ diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index d7a081199..5690da69f 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -3,6 +3,9 @@ from rest_framework import serializers from access.serializers.organization import OrganizationBaseSerializer + +from core.serializers.manufacturer import ManufacturerBaseSerializer + from itam.models.operating_system import OperatingSystem @@ -15,6 +18,10 @@ def get_display_name(self, item): return str( item ) + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_operating_system-detail", format="html" + ) + class Meta: model = OperatingSystem @@ -24,10 +31,73 @@ class Meta: 'id', 'display_name', 'name', + 'url', ] read_only_fields = [ 'id', 'display_name', 'name', + 'url', ] + + +class OperatingSystemModelSerializer(OperatingSystemBaseSerializer): + + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_operating_system-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_operating_system_notes-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), + 'tickets': 'ToDo' + } + + + + class Meta: + + model = OperatingSystem + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'display_name', + 'publisher', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class OperatingSystemViewSerializer(OperatingSystemModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + publisher = ManufacturerBaseSerializer( many = False, read_only = True ) + diff --git a/app/itam/viewsets/index.py b/app/itam/viewsets/index.py index bb6e6066e..833d0c8de 100644 --- a/app/itam/viewsets/index.py +++ b/app/itam/viewsets/index.py @@ -26,6 +26,7 @@ def list(self, request, pk=None): return Response( { "device": reverse('API:_api_v2_device-list', request=request), + "operating_system": reverse('API:_api_v2_operating_system-list', request=request), "software": reverse('API:_api_v2_software-list', request=request) } ) diff --git a/app/itam/viewsets/operating_system.py b/app/itam/viewsets/operating_system.py new file mode 100644 index 000000000..eecd477eb --- /dev/null +++ b/app/itam/viewsets/operating_system.py @@ -0,0 +1,87 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from itam.serializers.operating_system import ( + OperatingSystem, + OperatingSystemModelSerializer, + OperatingSystemViewSerializer +) +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create an operating system', + description='', + responses = { + 200: OpenApiResponse(description='Software allready exists', response=OperatingSystemViewSerializer), + 201: OpenApiResponse(description='Software created', response=OperatingSystemViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete an operating system', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all operating systems', + description='', + responses = { + 200: OpenApiResponse(description='', response=OperatingSystemViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single operating system', + description='', + responses = { + 200: OpenApiResponse(description='', response=OperatingSystemViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update an operating system', + description = '', + responses = { + 200: OpenApiResponse(description='', response=OperatingSystemViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Operating Systems """ + + filterset_fields = [ + 'is_global', + 'organization', + 'publisher', + ] + + search_fields = [ + 'name', + ] + + model = OperatingSystem + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Operating Systems' + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer'] From 0314684064f46238198b0b9a6cc6747cd82434df Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 13:45:11 +0930 Subject: [PATCH 243/617] feat(core): Add Operating System Version API v2 endpoint ref: #248 #355 --- app/api/react_ui_metadata.py | 5 + app/api/urls.py | 2 + app/itam/serializers/operating_system.py | 5 +- .../serializers/operating_system_version.py | 145 ++++++++++++++++++ app/itam/viewsets/operating_system_version.py | 99 ++++++++++++ 5 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 app/itam/serializers/operating_system_version.py create mode 100644 app/itam/viewsets/operating_system_version.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 8b68e39b8..2d713a516 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -136,6 +136,11 @@ def determine_metadata(self, request, view): "icon": "device", "link": "/itam/device" }, + { + "display_name": "Operating System", + "name": "operating_system", + "link": "/itam/operating_system" + }, { "display_name": "Software", "name": "software", diff --git a/app/api/urls.py b/app/api/urls.py index b46b8432d..b8fdb76c2 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -72,6 +72,7 @@ device_type as device_type_v2, device_software as device_software_v2, operating_system as operating_system_v2, + operating_system_version as operating_system_version_v2, software as software_v2, software_category as software_category_v2, software_version as software_version_v2, @@ -156,6 +157,7 @@ router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system') router.register('v2/itam/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') +router.register('v2/itam/operating_system/(?P[0-9]+)/version', operating_system_version_v2.ViewSet, basename='_api_v2_operating_system_version') router.register('v2/itam/software', software_v2.ViewSet, basename='_api_v2_software') router.register('v2/itam/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') router.register('v2/itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index 5690da69f..4565a9703 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -61,7 +61,8 @@ def get_url(self, item): } ), 'notes': reverse("API:_api_v2_operating_system_notes-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), - 'tickets': 'ToDo' + 'tickets': 'ToDo', + 'version': reverse("API:_api_v2_operating_system_version-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), } @@ -70,8 +71,6 @@ class Meta: model = OperatingSystem - fields = '__all__' - fields = [ 'id', 'organization', diff --git a/app/itam/serializers/operating_system_version.py b/app/itam/serializers/operating_system_version.py new file mode 100644 index 000000000..c1c42e40e --- /dev/null +++ b/app/itam/serializers/operating_system_version.py @@ -0,0 +1,145 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.serializers.manufacturer import ManufacturerBaseSerializer + +from itam.models.operating_system import OperatingSystem, OperatingSystemVersion +from itam.serializers.operating_system import OperatingSystemBaseSerializer + + + +class OperatingSystemVersionBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + + url = serializers.SerializerMethodField('my_url') + + def my_url(self, item): + + return reverse( + "API:_api_v2_operating_system_version-detail", + request=self.context['view'].request, + kwargs={ + 'operating_system_id': self._context['view'].kwargs['operating_system_id'], + 'pk': item.pk + } + ) + + + class Meta: + + model = OperatingSystemVersion + + fields = '__all__' + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class OperatingSystemVersionModelSerializer(OperatingSystemVersionBaseSerializer): + + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + "API:_api_v2_operating_system_version-detail", + request=self._context['view'].request, + kwargs={ + 'operating_system_id': self._context['view'].kwargs['operating_system_id'], + 'pk': item.pk + } + ), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse( + "API:_api_v2_operating_system_notes-list", + request=self._context['view'].request, + kwargs={ + 'operating_system_id': item.pk + } + ), + 'tickets': 'ToDo' + } + + + + class Meta: + + model = OperatingSystemVersion + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'operating_system', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'organization', + 'display_name', + 'operating_system', + 'created', + 'modified', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'view' in self._context: + + if 'operating_system_id' in self._context['view'].kwargs: + + operating_system = OperatingSystem.objects.get(id=self._context['view'].kwargs['operating_system_id']) + + self.validated_data['operating_system'] = operating_system + self.validated_data['organization'] = operating_system.organization + + return is_valid + + + +class OperatingSystemVersionViewSerializer(OperatingSystemVersionModelSerializer): + + operating_system = OperatingSystemBaseSerializer( many = False, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + diff --git a/app/itam/viewsets/operating_system_version.py b/app/itam/viewsets/operating_system_version.py new file mode 100644 index 000000000..7202a00e6 --- /dev/null +++ b/app/itam/viewsets/operating_system_version.py @@ -0,0 +1,99 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from itam.serializers.operating_system_version import ( + OperatingSystemVersion, + OperatingSystemVersionModelSerializer, + OperatingSystemVersionViewSerializer +) +from api.viewsets.common import ModelViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create an operating system version', + description='', + responses = { + 200: OpenApiResponse(description='Software allready exists', response=OperatingSystemVersionViewSerializer), + 201: OpenApiResponse(description='Software created', response=OperatingSystemVersionViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete an operating system version', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all operating system versions', + description='', + responses = { + 200: OpenApiResponse(description='', response=OperatingSystemVersionViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single operating system version', + description='', + responses = { + 200: OpenApiResponse(description='', response=OperatingSystemVersionViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update an operating system version', + description = '', + responses = { + 200: OpenApiResponse(description='', response=OperatingSystemVersionViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + """ Operating Systems """ + + filterset_fields = [ + 'is_global', + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = OperatingSystemVersion + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Operating Systems' + + + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter( + operating_system_id = self.kwargs['operating_system_id'] + ) + + self.queryset = queryset + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer'] From 9e83ec9adb14aaea4c162b0c69af31e363c364e6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 14:07:41 +0930 Subject: [PATCH 244/617] test(core): Device API ViewSet permission checks ref: #15 #248 #354 --- app/itam/models/operating_system.py | 6 +- .../tests/unit/device/test_device_viewset.py | 173 ++++++++++++++++++ 2 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 app/itam/tests/unit/device/test_device_viewset.py diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 791ebe48c..5cb7afbcd 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -96,7 +96,7 @@ class Meta: "sections": [ { "layout": "table", - "field": "software_version", + "field": "version", } ] }, @@ -187,7 +187,9 @@ class Meta: table_fields: list = [ 'name', - 'installations' + 'installations', + 'created', + 'modified', ] diff --git a/app/itam/tests/unit/device/test_device_viewset.py b/app/itam/tests/unit/device/test_device_viewset.py new file mode 100644 index 000000000..741137529 --- /dev/null +++ b/app/itam/tests/unit/device/test_device_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.device import Device + + + +class DevicePermissionsAPI(TestCase, APIPermissions): + + model = Device + + app_namespace = 'API' + + url_name = '_api_v2_device' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From b7bbf02dffd802088577598388cce6204a3a1450 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 14:32:04 +0930 Subject: [PATCH 245/617] test(itam): Device Serializer Validation checks ref: #15 #248 #353 --- app/itam/models/device.py | 12 +- app/itam/serializers/device.py | 8 +- .../unit/device/test_device_serializer.py | 347 ++++++++++++++++++ 3 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 app/itam/tests/unit/device/test_device_serializer.py diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 656689d2d..2ffc9e28b 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -6,6 +6,8 @@ from django.db import models from django.forms import ValidationError +from rest_framework import serializers + from access.fields import * from access.models import TenancyObject @@ -124,7 +126,10 @@ def validate_uuid_format(self): if not re.match(pattern, str(self)): - raise ValidationError(f'UUID must be formated to match regex {str(pattern)}') + raise serializers.ValidationError( + f'UUID must be formated to match regex {str(pattern)}', + code = 'invalid_uuid' + ) def validate_hostname_format(self): @@ -133,9 +138,10 @@ def validate_hostname_format(self): if not re.match(pattern, str(self).lower()): - raise ValidationError( + raise serializers.ValidationError( '''[RFC1035 2.3.1] A hostname must start with a letter, end with a letter or digit, - and have as interior characters only letters, digits, and hyphen.''' + and have as interior characters only letters, digits, and hyphen.''', + code = 'invalid_hostname' ) diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 610779529..2ae6472c6 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -72,11 +72,8 @@ def get_url(self, item): } - rendered_config = serializers.JSONField(source='get_configuration', read_only=True) - context = serializers.SerializerMethodField('get_cont') - def get_cont(self, item) -> dict: from django.core.serializers import serialize @@ -90,7 +87,9 @@ def get_cont(self, item) -> dict: context: dict = {} return context - + + + rendered_config = serializers.JSONField(source='get_configuration', read_only=True) def get_rendered_config(self, item): @@ -99,6 +98,7 @@ def get_rendered_config(self, item): status_icon = IconField(read_only = True, label='') + class Meta: model = Device diff --git a/app/itam/tests/unit/device/test_device_serializer.py b/app/itam/tests/unit/device/test_device_serializer.py new file mode 100644 index 000000000..0e6029762 --- /dev/null +++ b/app/itam/tests/unit/device/test_device_serializer.py @@ -0,0 +1,347 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.device import Device, DeviceModelSerializer + + + +class DeviceValidationAPI( + TestCase, +): + + model = Device + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'valid-hostname', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_update_existing_invalid_name_starts_with_digit(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid name 'starts with digit' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "name": '0-start-with-number' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'invalid_hostname' + + + + def test_serializer_validation_update_existing_invalid_name_contains_hyphon(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid name 'contains hyphon' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "name": 'has_a_hyphon' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'invalid_hostname' + + + + def test_serializer_validation_update_existing_invalid_name_ends_with_dash(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid name 'ends with dash' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "name": 'ends-with-dash-' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'invalid_hostname' + + + + def test_serializer_validation_update_existing_invalid_uuid_first_octet(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'first octet not hex' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": 'g0000000-0000-0000-0000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + + def test_serializer_validation_update_existing_invalid_uuid_first_octet_wrong_length(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'first octet wrong length' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '0000000-0000-0000-0000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + + def test_serializer_validation_update_existing_invalid_uuid_second_octet(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'second octet not hex' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-g000-0000-0000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + def test_serializer_validation_update_existing_invalid_uuid_second_octet_wrong_length(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'second octet wrong length' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-000-0000-0000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + + def test_serializer_validation_update_existing_invalid_uuid_third_octet(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'third octet not hex' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-0000-g000-0000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + def test_serializer_validation_update_existing_invalid_uuid_third_octet_wrong_length(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'third octet wrong length' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-0000-000-0000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + + def test_serializer_validation_update_existing_invalid_uuid_fourth_octet(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'fourth octet not hex' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-0000-0000-g000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + def test_serializer_validation_update_existing_invalid_uuid_fourth_octet_wrong_length(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'fourth octet wrong length' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-0000-0000-000-000000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + + def test_serializer_validation_update_existing_invalid_uuid_fifth_octet(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'fifth octet not hex' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-0000-0000-0000-g00000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' + + + def test_serializer_validation_update_existing_invalid_uuid_fifth_octet_wrong_length(self): + """Serializer Validation Check + + Ensure that if an existing item is given an invalid uuid 'fifth octet wrong length' + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelSerializer( + self.item, + data={ + "uuid": '00000000-0000-0000-0000-00000000000' + }, + partial=True, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['uuid'][0] == 'invalid_uuid' From cb73866cdc6e6554f1b21ad3974518642e3eaff2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:02:54 +0930 Subject: [PATCH 246/617] test(itam): Device API field checks ref: #15 #248 #354 --- .../tests/unit/device/test_device_api_v2.py | 548 ++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 app/itam/tests/unit/device/test_device_api_v2.py diff --git a/app/itam/tests/unit/device/test_device_api_v2.py b/app/itam/tests/unit/device/test_device_api_v2.py new file mode 100644 index 000000000..fedc94f48 --- /dev/null +++ b/app/itam/tests/unit/device/test_device_api_v2.py @@ -0,0 +1,548 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + +from itam.models.device import Device, DeviceModel, DeviceType + + + +class DeviceAPI( + TestCase, + APITenancyObject +): + + model = Device + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + manufacturer = Manufacturer.objects.create( + organization = self.organization, + name = 'a manufacturer' + ) + + device_model = DeviceModel.objects.create( + organization = self.organization, + name = 'a model', + manufacturer = manufacturer + ) + + device_type = DeviceType.objects.create( + organization = self.organization, + name = 'computer' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + config = dict({"key": "one", "existing": "dont_over_write"}), + model_notes = 'a note', + uuid = '00000000-0000-0000-0000-000000000000', + serial_number = 'serial-number', + device_model = device_model, + device_type = device_type, + inventorydate = '2024-01-01 01:01:00' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_device-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is dict + + + + def test_api_field_exists_status_icon(self): + """ Test for existance of API Field + + status_icon field must exist + """ + + assert 'status_icon' in self.api_data + + + def test_api_field_type_status_icon(self): + """ Test for type for API Field + + status_icon field must be list + """ + + assert type(self.api_data['status_icon']) is list + + + + def test_api_field_exists_serial_number(self): + """ Test for existance of API Field + + serial_number field must exist + """ + + assert 'serial_number' in self.api_data + + + def test_api_field_type_serial_number(self): + """ Test for type for API Field + + serial_number field must be str + """ + + assert type(self.api_data['serial_number']) is str + + + + def test_api_field_exists_uuid(self): + """ Test for existance of API Field + + uuid field must exist + """ + + assert 'uuid' in self.api_data + + + def test_api_field_type_uuid(self): + """ Test for type for API Field + + uuid field must be str + """ + + assert type(self.api_data['uuid']) is str + + + + def test_api_field_exists_is_virtual(self): + """ Test for existance of API Field + + is_virtual field must exist + """ + + assert 'is_virtual' in self.api_data + + + def test_api_field_type_is_virtual(self): + """ Test for type for API Field + + is_virtual field must be bool + """ + + assert type(self.api_data['is_virtual']) is bool + + + + def test_api_field_exists_inventorydate(self): + """ Test for existance of API Field + + inventorydate field must exist + """ + + assert 'inventorydate' in self.api_data + + + def test_api_field_type_inventorydate(self): + """ Test for type for API Field + + inventorydate field must be str + """ + + assert type(self.api_data['inventorydate']) is str + + + + def test_api_field_exists_device_type(self): + """ Test for existance of API Field + + device_type field must exist + """ + + assert 'device_type' in self.api_data + + + def test_api_field_type_device_type(self): + """ Test for type for API Field + + device_type field must be dict + """ + + assert type(self.api_data['device_type']) is dict + + + def test_api_field_exists_device_type_id(self): + """ Test for existance of API Field + + device_type.id field must exist + """ + + assert 'id' in self.api_data['device_type'] + + + def test_api_field_type_device_type_id(self): + """ Test for type for API Field + + device_type.id field must be int + """ + + assert type(self.api_data['device_type']['id']) is int + + + def test_api_field_exists_device_type_display_name(self): + """ Test for existance of API Field + + device_type.display_name field must exist + """ + + assert 'display_name' in self.api_data['device_type'] + + + def test_api_field_type_device_type_display_name(self): + """ Test for type for API Field + + device_type.display_name field must be str + """ + + assert type(self.api_data['device_type']['display_name']) is str + + + def test_api_field_exists_device_type_url(self): + """ Test for existance of API Field + + device_type.url field must exist + """ + + assert 'url' in self.api_data['device_type'] + + + def test_api_field_type_device_type_url(self): + """ Test for type for API Field + + device_type.url field must be Hyperlink + """ + + assert type(self.api_data['device_type']['url']) is Hyperlink + + + + def test_api_field_exists_device_model(self): + """ Test for existance of API Field + + device_model field must exist + """ + + assert 'device_model' in self.api_data + + + def test_api_field_type_device_model(self): + """ Test for type for API Field + + device_model field must be dict + """ + + assert type(self.api_data['device_model']) is dict + + + def test_api_field_exists_device_model_id(self): + """ Test for existance of API Field + + device_model.id field must exist + """ + + assert 'id' in self.api_data['device_model'] + + + def test_api_field_type_device_model_id(self): + """ Test for type for API Field + + device_model.id field must be int + """ + + assert type(self.api_data['device_model']['id']) is int + + + def test_api_field_exists_device_model_display_name(self): + """ Test for existance of API Field + + device_model.display_name field must exist + """ + + assert 'display_name' in self.api_data['device_model'] + + + def test_api_field_type_device_model_display_name(self): + """ Test for type for API Field + + device_model.display_name field must be str + """ + + assert type(self.api_data['device_model']['display_name']) is str + + + def test_api_field_exists_device_model_url(self): + """ Test for existance of API Field + + device_model.url field must exist + """ + + assert 'url' in self.api_data['device_model'] + + + def test_api_field_type_device_model_url(self): + """ Test for type for API Field + + device_model.url field must be Hyperlink + """ + + assert type(self.api_data['device_model']['url']) is Hyperlink + + + + def test_api_field_exists_organization(self): + """ Test for existance of API Field + + organization field must exist + """ + + assert 'organization' in self.api_data + + + def test_api_field_type_organization(self): + """ Test for type for API Field + + organization field must be dict + """ + + assert type(self.api_data['organization']) is dict + + + def test_api_field_exists_organization_id(self): + """ Test for existance of API Field + + organization.id field must exist + """ + + assert 'id' in self.api_data['organization'] + + + def test_api_field_type_organization_id(self): + """ Test for type for API Field + + organization.id field must be int + """ + + assert type(self.api_data['organization']['id']) is int + + + def test_api_field_exists_organization_display_name(self): + """ Test for existance of API Field + + organization.display_name field must exist + """ + + assert 'display_name' in self.api_data['organization'] + + + def test_api_field_type_organization_display_name(self): + """ Test for type for API Field + + organization.display_name field must be str + """ + + assert type(self.api_data['organization']['display_name']) is str + + + def test_api_field_exists_organization_url(self): + """ Test for existance of API Field + + organization.url field must exist + """ + + assert 'url' in self.api_data['organization'] + + + def test_api_field_type_organization_url(self): + """ Test for type for API Field + + organization.url field must be Hyperlink + """ + + assert type(self.api_data['organization']['url']) is Hyperlink + + + + def test_api_field_exists_urls_device_model(self): + """ Test for existance of API Field + + _urls.device_model field must exist + """ + + assert 'device_model' in self.api_data['_urls'] + + + def test_api_field_type_urls_device_model(self): + """ Test for type for API Field + + _urls.device_model field must be str + """ + + assert type(self.api_data['_urls']['device_model']) is str + + + + def test_api_field_exists_urls_device_type(self): + """ Test for existance of API Field + + _urls.device_type field must exist + """ + + assert 'device_type' in self.api_data['_urls'] + + + def test_api_field_type_urls_device_type(self): + """ Test for type for API Field + + _urls.device_type field must be str + """ + + assert type(self.api_data['_urls']['device_type']) is str + + + + def test_api_field_exists_urls_external_links(self): + """ Test for existance of API Field + + _urls.external_links field must exist + """ + + assert 'external_links' in self.api_data['_urls'] + + + def test_api_field_type_urls_external_links(self): + """ Test for type for API Field + + _urls.external_links field must be str + """ + + assert type(self.api_data['_urls']['external_links']) is str + + + + def test_api_field_exists_urls_history(self): + """ Test for existance of API Field + + _urls.history field must exist + """ + + assert 'history' in self.api_data['_urls'] + + + def test_api_field_type_urls_history(self): + """ Test for type for API Field + + _urls.history field must be str + """ + + assert type(self.api_data['_urls']['history']) is str + + + + def test_api_field_exists_urls_notes(self): + """ Test for existance of API Field + + _urls.notes field must exist + """ + + assert 'notes' in self.api_data['_urls'] + + + def test_api_field_type_urls_notes(self): + """ Test for type for API Field + + _urls.notes field must be str + """ + + assert type(self.api_data['_urls']['notes']) is str + + + + def test_api_field_exists_urls_software(self): + """ Test for existance of API Field + + _urls.software field must exist + """ + + assert 'software' in self.api_data['_urls'] + + + def test_api_field_type_urls_software(self): + """ Test for type for API Field + + _urls.software field must be str + """ + + assert type(self.api_data['_urls']['software']) is str + + From efa805816fab0f733bc849522a86eee3acc89674 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:10:11 +0930 Subject: [PATCH 247/617] test(itam): Operating System API field checks ref: #15 #248 #354 --- .../test_operating_system_api_v2.py | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 app/itam/tests/unit/operating_system/test_operating_system_api_v2.py diff --git a/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py b/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py new file mode 100644 index 000000000..c118017cf --- /dev/null +++ b/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py @@ -0,0 +1,156 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + +from itam.models.operating_system import OperatingSystem + + + +class OperatingSystemAPI( + TestCase, + APITenancyObject +): + + model = OperatingSystem + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + manufacturer = Manufacturer.objects.create( + organization = self.organization, + name = 'a manufacturer' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + publisher = manufacturer, + model_notes = 'a note' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_operating_system-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_publisher(self): + """ Test for existance of API Field + + publisher field must exist + """ + + assert 'publisher' in self.api_data + + + def test_api_field_type_publisher(self): + """ Test for type for API Field + + publisher field must be dict + """ + + assert type(self.api_data['publisher']) is dict + + + def test_api_field_exists_publisher_id(self): + """ Test for existance of API Field + + publisher.id field must exist + """ + + assert 'id' in self.api_data['publisher'] + + + def test_api_field_type_publisher_id(self): + """ Test for type for API Field + + publisher.id field must be int + """ + + assert type(self.api_data['publisher']['id']) is int + + + def test_api_field_exists_publisher_display_name(self): + """ Test for existance of API Field + + publisher.display_name field must exist + """ + + assert 'display_name' in self.api_data['publisher'] + + + def test_api_field_type_publisher_display_name(self): + """ Test for type for API Field + + publisher.display_name field must be str + """ + + assert type(self.api_data['publisher']['display_name']) is str + + + def test_api_field_exists_publisher_url(self): + """ Test for existance of API Field + + publisher.url field must exist + """ + + assert 'url' in self.api_data['publisher'] + + + def test_api_field_type_publisher_url(self): + """ Test for type for API Field + + publisher.url field must be Hyperlink + """ + + assert type(self.api_data['publisher']['url']) is Hyperlink + From 85cde782039f85ba139f1477b378d94251b52e13 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:14:07 +0930 Subject: [PATCH 248/617] test(itam): Operating_system API ViewSet permission checks ref: #15 #248 #354 --- .../test_operating_system_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/itam/tests/unit/operating_system/test_operating_system_viewset.py diff --git a/app/itam/tests/unit/operating_system/test_operating_system_viewset.py b/app/itam/tests/unit/operating_system/test_operating_system_viewset.py new file mode 100644 index 000000000..ddc770528 --- /dev/null +++ b/app/itam/tests/unit/operating_system/test_operating_system_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.operating_system import OperatingSystem + + + +class OperatingSystemPermissionsAPI(TestCase, APIPermissions): + + model = OperatingSystem + + app_namespace = 'API' + + url_name = '_api_v2_operating_system' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 911ba67459ebcf39c22f834a7502cdf5f10cf61c Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:17:22 +0930 Subject: [PATCH 249/617] test(itam): Operating System Serializer Validation checks ref: #15 #248 #353 --- .../test_operating_system_serializer.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 app/itam/tests/unit/operating_system/test_operating_system_serializer.py diff --git a/app/itam/tests/unit/operating_system/test_operating_system_serializer.py b/app/itam/tests/unit/operating_system/test_operating_system_serializer.py new file mode 100644 index 000000000..bd9f766dc --- /dev/null +++ b/app/itam/tests/unit/operating_system/test_operating_system_serializer.py @@ -0,0 +1,52 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.operating_system import OperatingSystem, OperatingSystemModelSerializer + + + +class OperatingSystemValidationAPI( + TestCase, +): + + model = OperatingSystem + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = OperatingSystemModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 7bd50c680506e9059a5aba7bc984aa6721457a09 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:33:12 +0930 Subject: [PATCH 250/617] test(itam): Software API field checks ref: #15 #248 #354 --- .../unit/software/test_software_api_v2.py | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 app/itam/tests/unit/software/test_software_api_v2.py diff --git a/app/itam/tests/unit/software/test_software_api_v2.py b/app/itam/tests/unit/software/test_software_api_v2.py new file mode 100644 index 000000000..ae48866f3 --- /dev/null +++ b/app/itam/tests/unit/software/test_software_api_v2.py @@ -0,0 +1,310 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + +from itam.models.software import Software, SoftwareCategory + + + +class SoftwareAPI( + TestCase, + APITenancyObject +): + + model = Software + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + manufacturer = Manufacturer.objects.create( + organization = self.organization, + name = 'a manufacturer' + ) + + category = SoftwareCategory.objects.create( + organization = self.organization, + name = 'category' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + publisher = manufacturer, + category = category, + model_notes = 'a note' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_software-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_id(self): + """ Test for existance of API Field + + category.id field must exist + """ + + assert 'id' in self.api_data['category'] + + + def test_api_field_type_category_id(self): + """ Test for type for API Field + + category.id field must be int + """ + + assert type(self.api_data['category']['id']) is int + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink + + + + def test_api_field_exists_publisher(self): + """ Test for existance of API Field + + publisher field must exist + """ + + assert 'publisher' in self.api_data + + + def test_api_field_type_publisher(self): + """ Test for type for API Field + + publisher field must be dict + """ + + assert type(self.api_data['publisher']) is dict + + + def test_api_field_exists_publisher_id(self): + """ Test for existance of API Field + + publisher.id field must exist + """ + + assert 'id' in self.api_data['publisher'] + + + def test_api_field_type_publisher_id(self): + """ Test for type for API Field + + publisher.id field must be int + """ + + assert type(self.api_data['publisher']['id']) is int + + + def test_api_field_exists_publisher_display_name(self): + """ Test for existance of API Field + + publisher.display_name field must exist + """ + + assert 'display_name' in self.api_data['publisher'] + + + def test_api_field_type_publisher_display_name(self): + """ Test for type for API Field + + publisher.display_name field must be str + """ + + assert type(self.api_data['publisher']['display_name']) is str + + + def test_api_field_exists_publisher_url(self): + """ Test for existance of API Field + + publisher.url field must exist + """ + + assert 'url' in self.api_data['publisher'] + + + def test_api_field_type_publisher_url(self): + """ Test for type for API Field + + publisher.url field must be Hyperlink + """ + + assert type(self.api_data['publisher']['url']) is Hyperlink + + + + def test_api_field_exists_urls_external_links(self): + """ Test for existance of API Field + + _urls.external_links field must exist + """ + + assert 'external_links' in self.api_data['_urls'] + + + def test_api_field_type_urls_external_links(self): + """ Test for type for API Field + + _urls.external_links field must be str + """ + + assert type(self.api_data['_urls']['external_links']) is str + + + + def test_api_field_exists_urls_history(self): + """ Test for existance of API Field + + _urls.history field must exist + """ + + assert 'history' in self.api_data['_urls'] + + + def test_api_field_type_urls_history(self): + """ Test for type for API Field + + _urls.history field must be str + """ + + assert type(self.api_data['_urls']['history']) is str + + + + def test_api_field_exists_urls_notes(self): + """ Test for existance of API Field + + _urls.notes field must exist + """ + + assert 'notes' in self.api_data['_urls'] + + + def test_api_field_type_urls_notes(self): + """ Test for type for API Field + + _urls.notes field must be str + """ + + assert type(self.api_data['_urls']['notes']) is str + + + + def test_api_field_exists_urls_version(self): + """ Test for existance of API Field + + _urls.version field must exist + """ + + assert 'version' in self.api_data['_urls'] + + + def test_api_field_type_urls_notes(self): + """ Test for type for API Field + + _urls.version field must be str + """ + + assert type(self.api_data['_urls']['version']) is str From 23bcdce93822b0aa37069a0d44aa61870b059136 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:34:05 +0930 Subject: [PATCH 251/617] test(itam): Software Serializer Validation checks ref: #15 #248 #353 --- app/itam/models/software.py | 2 +- app/itam/serializers/software.py | 2 +- .../unit/software/test_software_serializer.py | 52 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 app/itam/tests/unit/software/test_software_serializer.py diff --git a/app/itam/models/software.py b/app/itam/models/software.py index fec872104..04e4c9bd7 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -169,7 +169,7 @@ class Meta: "sections": [ { "layout": "table", - "field": "software_version", + "field": "version", } ] }, diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 8babf786e..5e117636b 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -62,7 +62,7 @@ def get_url(self, item): 'notes': reverse("API:_api_v2_software_notes-list", request=self._context['view'].request, kwargs={'software_id': item.pk}), 'publisher': reverse("API:_api_v2_manufacturer-list", request=self._context['view'].request), 'services': 'ToDo', - 'software_version': reverse( + 'version': reverse( "API:_api_v2_software_version-list", request=self._context['view'].request, kwargs={ diff --git a/app/itam/tests/unit/software/test_software_serializer.py b/app/itam/tests/unit/software/test_software_serializer.py new file mode 100644 index 000000000..22ac745ef --- /dev/null +++ b/app/itam/tests/unit/software/test_software_serializer.py @@ -0,0 +1,52 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.software import Software, SoftwareModelSerializer + + + +class SoftwareValidationAPI( + TestCase, +): + + model = Software + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = SoftwareModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 794fb6a7330ffe6d80df7e9a53d03dc0cd604922 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:39:42 +0930 Subject: [PATCH 252/617] test(itam): Software API ViewSet permission checks ref: #15 #248 #354 --- .../unit/software/test_software_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/itam/tests/unit/software/test_software_viewset.py diff --git a/app/itam/tests/unit/software/test_software_viewset.py b/app/itam/tests/unit/software/test_software_viewset.py new file mode 100644 index 000000000..1025ea7c9 --- /dev/null +++ b/app/itam/tests/unit/software/test_software_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.software import Software + + + +class SoftwarePermissionsAPI(TestCase, APIPermissions): + + model = Software + + app_namespace = 'API' + + url_name = '_api_v2_software' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 92825d3b342713ce64d3e90afd4f3b4b8daa2d21 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 15:51:02 +0930 Subject: [PATCH 253/617] test(itam): Operating System Version API field checks ref: #15 #248 #354 --- .../test_operating_system_version_api_v2.py | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py new file mode 100644 index 000000000..e98eb0f3a --- /dev/null +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py @@ -0,0 +1,154 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itam.models.operating_system import OperatingSystem, OperatingSystemVersion + + + +class OperatingSystemVersionAPI( + TestCase, + APITenancyObject +): + + model = OperatingSystemVersion + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + operating_system = OperatingSystem.objects.create( + organization = self.organization, + name = 'one', + model_notes = 'a note' + ) + + self.item = self.model.objects.create( + organization = self.organization, + name = '10', + model_notes = 'a note', + operating_system = operating_system + ) + + + self.url_view_kwargs = {'operating_system_id': operating_system.id, 'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_operating_system_version-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_operating_system(self): + """ Test for existance of API Field + + operating_system field must exist + """ + + assert 'operating_system' in self.api_data + + + def test_api_field_type_operating_system(self): + """ Test for type for API Field + + operating_system field must be dict + """ + + assert type(self.api_data['operating_system']) is dict + + + def test_api_field_exists_operating_system_id(self): + """ Test for existance of API Field + + operating_system.id field must exist + """ + + assert 'id' in self.api_data['operating_system'] + + + def test_api_field_type_operating_system_id(self): + """ Test for type for API Field + + operating_system.id field must be int + """ + + assert type(self.api_data['operating_system']['id']) is int + + + def test_api_field_exists_operating_system_display_name(self): + """ Test for existance of API Field + + operating_system.display_name field must exist + """ + + assert 'display_name' in self.api_data['operating_system'] + + + def test_api_field_type_operating_system_display_name(self): + """ Test for type for API Field + + operating_system.display_name field must be str + """ + + assert type(self.api_data['operating_system']['display_name']) is str + + + def test_api_field_exists_operating_system_url(self): + """ Test for existance of API Field + + operating_system.url field must exist + """ + + assert 'url' in self.api_data['operating_system'] + + + def test_api_field_type_operating_system_url(self): + """ Test for type for API Field + + operating_system.url field must be Hyperlink + """ + + assert type(self.api_data['operating_system']['url']) is Hyperlink + From 8b155eb8952824ee0afec40564268a8d628a3a0e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 16:03:19 +0930 Subject: [PATCH 254/617] test(itam): Operating System Version Serializer Validation checks ref: #15 #248 #353 --- ...est_operating_system_version_serializer.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 app/itam/tests/unit/operating_system_version/test_operating_system_version_serializer.py diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_serializer.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_serializer.py new file mode 100644 index 000000000..4f61b9277 --- /dev/null +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version_serializer.py @@ -0,0 +1,58 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.operating_system_version import OperatingSystem, OperatingSystemVersion, OperatingSystemVersionModelSerializer + + + +class OperatingSystemVersionValidationAPI( + TestCase, +): + + model = OperatingSystemVersion + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + os = OperatingSystem.objects.create( + organization=organization, + name = 'os name' + ) + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + operating_system = os + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = OperatingSystemVersionModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From cd78a6b12fc0d82245a7f14ddc5e5c082e557715 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 16:11:27 +0930 Subject: [PATCH 255/617] test(itam): Operating System Version API ViewSet permission checks ref: #15 #248 #354 --- .../test_operating_system_version_viewset.py | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py new file mode 100644 index 000000000..6c60f7336 --- /dev/null +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py @@ -0,0 +1,181 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.operating_system import OperatingSystem, OperatingSystemVersion + + + +class OperatingSystemVersionPermissionsAPI(TestCase, APIPermissions): + + model = OperatingSystemVersion + + app_namespace = 'API' + + url_name = '_api_v2_operating_system_version' + + change_data = {'name': '22'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + os = OperatingSystem.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = '5', + operating_system = os + ) + + + self.url_view_kwargs = {'operating_system_id': os.id, 'pk': self.item.id} + + self.url_kwargs = {'operating_system_id': os.id,} + + self.add_data = { + 'name': '22', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 65926210b2f51c22a5ba48c25ecc628ae295408a Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 16:38:04 +0930 Subject: [PATCH 256/617] test(itam): Software Category Version API field checks ref: #15 #248 #354 --- .../test_software_category_api_v2.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 app/itam/tests/unit/software_category/test_software_category_api_v2.py diff --git a/app/itam/tests/unit/software_category/test_software_category_api_v2.py b/app/itam/tests/unit/software_category/test_software_category_api_v2.py new file mode 100644 index 000000000..6ca334d83 --- /dev/null +++ b/app/itam/tests/unit/software_category/test_software_category_api_v2.py @@ -0,0 +1,81 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + +from itam.models.software import Software, SoftwareCategory + + + +class SoftwareCategoryAPI( + TestCase, + APITenancyObject +): + + model = SoftwareCategory + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + manufacturer = Manufacturer.objects.create( + organization = self.organization, + name = 'a manufacturer' + ) + + self.item = SoftwareCategory.objects.create( + organization = self.organization, + name = 'category', + model_notes = 'a note' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_software_category-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data From ef5dd3dc2143449817aa88d9a8bbfd10dc435917 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 16:40:13 +0930 Subject: [PATCH 257/617] test(itam): Software Category Serializer Validation checks ref: #15 #248 #353 --- .../test_software_category_serializer.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 app/itam/tests/unit/software_category/test_software_category_serializer.py diff --git a/app/itam/tests/unit/software_category/test_software_category_serializer.py b/app/itam/tests/unit/software_category/test_software_category_serializer.py new file mode 100644 index 000000000..1c4353095 --- /dev/null +++ b/app/itam/tests/unit/software_category/test_software_category_serializer.py @@ -0,0 +1,52 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.software_category import SoftwareCategory, SoftwareCategoryModelSerializer + + + +class SoftwareCategoryValidationAPI( + TestCase, +): + + model = SoftwareCategory + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = SoftwareCategoryModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 35e547268b65651ef8946ae962cb03fa294d7177 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 16:44:07 +0930 Subject: [PATCH 258/617] test(itam): Software Category Version API ViewSet permission checks ref: #15 #248 #354 --- .../test_software_category_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/itam/tests/unit/software_category/test_software_category_viewset.py diff --git a/app/itam/tests/unit/software_category/test_software_category_viewset.py b/app/itam/tests/unit/software_category/test_software_category_viewset.py new file mode 100644 index 000000000..cd21c5605 --- /dev/null +++ b/app/itam/tests/unit/software_category/test_software_category_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.software import SoftwareCategory + + + +class SoftwareCategoryPermissionsAPI(TestCase, APIPermissions): + + model = SoftwareCategory + + app_namespace = 'API' + + url_name = '_api_v2_software_category' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 2908f6bd4ce87217fde63d2497e8b6da4d7ae379 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 16:52:08 +0930 Subject: [PATCH 259/617] test(itam): Software Version API field checks ref: #15 #248 #354 --- .../test_software_version_api_v2.py | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 app/itam/tests/unit/software_version/test_software_version_api_v2.py diff --git a/app/itam/tests/unit/software_version/test_software_version_api_v2.py b/app/itam/tests/unit/software_version/test_software_version_api_v2.py new file mode 100644 index 000000000..62fcc007d --- /dev/null +++ b/app/itam/tests/unit/software_version/test_software_version_api_v2.py @@ -0,0 +1,157 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itam.models.software import Software, SoftwareVersion + + + +class SoftwareVersionCategoryAPI( + TestCase, + APITenancyObject +): + + model = SoftwareVersion + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + software = Software.objects.create( + organization = self.organization, + name = 'software' + ) + + self.item = self.model.objects.create( + organization = self.organization, + name = '10', + model_notes = 'a note', + software = software + ) + + + self.url_view_kwargs = {'software_id': software.id, 'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_software_version-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + + def test_api_field_exists_software(self): + """ Test for existance of API Field + + software field must exist + """ + + assert 'software' in self.api_data + + + def test_api_field_type_software(self): + """ Test for type for API Field + + software field must be dict + """ + + assert type(self.api_data['software']) is dict + + + def test_api_field_exists_software_id(self): + """ Test for existance of API Field + + software.id field must exist + """ + + assert 'id' in self.api_data['software'] + + + def test_api_field_type_software_id(self): + """ Test for type for API Field + + software.id field must be int + """ + + assert type(self.api_data['software']['id']) is int + + + def test_api_field_exists_software_display_name(self): + """ Test for existance of API Field + + software.display_name field must exist + """ + + assert 'display_name' in self.api_data['software'] + + + def test_api_field_type_software_display_name(self): + """ Test for type for API Field + + software.display_name field must be str + """ + + assert type(self.api_data['software']['display_name']) is str + + + def test_api_field_exists_software_url(self): + """ Test for existance of API Field + + software.url field must exist + """ + + assert 'url' in self.api_data['software'] + + + def test_api_field_type_software_url(self): + """ Test for type for API Field + + software.url field must be Hyperlink + """ + + assert type(self.api_data['software']['url']) is Hyperlink + + From 4f66af4bbbaa96bbdfdd3ef4dd17536ec0389a0e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:04:34 +0930 Subject: [PATCH 260/617] test(itam): Software Version Serializer Validation checks ref: #15 #248 #353 --- .../0017_alter_softwareversion_options.py | 17 +++++++ app/itam/models/software.py | 2 + .../test_software_version_serializer.py | 51 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 app/itam/migrations/0017_alter_softwareversion_options.py create mode 100644 app/itam/tests/unit/software_version/test_software_version_serializer.py diff --git a/app/itam/migrations/0017_alter_softwareversion_options.py b/app/itam/migrations/0017_alter_softwareversion_options.py new file mode 100644 index 000000000..27a930230 --- /dev/null +++ b/app/itam/migrations/0017_alter_softwareversion_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-20 07:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('itam', '0016_alter_devicesoftware_action'), + ] + + operations = [ + migrations.AlterModelOptions( + name='softwareversion', + options={'ordering': ['name'], 'verbose_name': 'Software Version', 'verbose_name_plural': 'Software Versions'}, + ), + ] diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 04e4c9bd7..5868463de 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -253,7 +253,9 @@ class Meta: software = models.ForeignKey( Software, + blank = False, help_text = 'Software this version applies', + null = False, on_delete=models.CASCADE, verbose_name = 'Software', ) diff --git a/app/itam/tests/unit/software_version/test_software_version_serializer.py b/app/itam/tests/unit/software_version/test_software_version_serializer.py new file mode 100644 index 000000000..0b64fb3f7 --- /dev/null +++ b/app/itam/tests/unit/software_version/test_software_version_serializer.py @@ -0,0 +1,51 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.software_version import Software, SoftwareVersion, SoftwareVersionModelSerializer + + + +class SoftwareVersionValidationAPI( + TestCase, +): + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.software = Software.objects.create( + organization=organization, + name = 'os name', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = SoftwareVersionModelSerializer(data={ + "organization": self.organization.id, + "software": self.software.id + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 523c82d72fbaed78f1cd63f6100e065ed2aa50ed Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:08:08 +0930 Subject: [PATCH 261/617] test(itam): Software Version API ViewSet permission checks ref: #15 #248 #354 --- .../test_software_version_viewset.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 app/itam/tests/unit/software_version/test_software_version_viewset.py diff --git a/app/itam/tests/unit/software_version/test_software_version_viewset.py b/app/itam/tests/unit/software_version/test_software_version_viewset.py new file mode 100644 index 000000000..2b309ae54 --- /dev/null +++ b/app/itam/tests/unit/software_version/test_software_version_viewset.py @@ -0,0 +1,179 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.software import Software, SoftwareVersion + + + +class SoftwareVersionPermissionsAPI(TestCase, APIPermissions): + + model = SoftwareVersion + + app_namespace = 'API' + + url_name = '_api_v2_software_version' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + software = Software.objects.create( + organization = self.organization, + name = 'software' + ) + self.item = self.model.objects.create( + organization = self.organization, + name = '12', + software = software + ) + + + self.url_view_kwargs = {'software_id': software.id, 'pk': self.item.id} + + self.url_kwargs = {'software_id': software.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 01ecc683842e5880a0390f9856fa504202a20db4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:11:50 +0930 Subject: [PATCH 262/617] test(itam): Software Version Tenancy Model Checks ref: #15 #248 #354 --- .../software_version/test_software_version.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/itam/tests/unit/software_version/test_software_version.py diff --git a/app/itam/tests/unit/software_version/test_software_version.py b/app/itam/tests/unit/software_version/test_software_version.py new file mode 100644 index 000000000..578286e69 --- /dev/null +++ b/app/itam/tests/unit/software_version/test_software_version.py @@ -0,0 +1,18 @@ +import pytest +import unittest +import requests + +from django.test import TestCase + +from app.tests.abstract.models import TenancyModel + +from itam.models.software import SoftwareVersion + + + +class SoftwareVersionModel( + TestCase, + TenancyModel +): + + model = SoftwareVersion From fbbd809ef59e86417372a153df066c6990576a2f Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:16:41 +0930 Subject: [PATCH 263/617] test(itam): Device Type API field checks ref: #15 #248 #354 --- .../device_type/test_device_type_api_v2.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 app/itam/tests/unit/device_type/test_device_type_api_v2.py diff --git a/app/itam/tests/unit/device_type/test_device_type_api_v2.py b/app/itam/tests/unit/device_type/test_device_type_api_v2.py new file mode 100644 index 000000000..b9669b5b4 --- /dev/null +++ b/app/itam/tests/unit/device_type/test_device_type_api_v2.py @@ -0,0 +1,72 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itam.models.device import DeviceType + + + +class DeviceTypeAPI( + TestCase, + APITenancyObject +): + + model = DeviceType + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + self.item = DeviceType.objects.create( + organization = self.organization, + name = 'computer', + model_notes = 'a note', + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_device_type-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data From 39bc70558ab7da4825a67acc878b66d2ca00d29f Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:19:08 +0930 Subject: [PATCH 264/617] test(itam): Device Type Serializer Validation checks ref: #15 #248 #353 --- .../test_device_type_serializer.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 app/itam/tests/unit/device_type/test_device_type_serializer.py diff --git a/app/itam/tests/unit/device_type/test_device_type_serializer.py b/app/itam/tests/unit/device_type/test_device_type_serializer.py new file mode 100644 index 000000000..1cb96acb2 --- /dev/null +++ b/app/itam/tests/unit/device_type/test_device_type_serializer.py @@ -0,0 +1,52 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.device_type import DeviceType, DeviceTypeModelSerializer + + + +class DeviceTypeValidationAPI( + TestCase, +): + + model = DeviceType + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'valid-hostname', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceTypeModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 1f9c665d960008041e5e91c29b57f7a4feba5bd4 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:22:00 +0930 Subject: [PATCH 265/617] fix(itam): Ensure software version model has page_layout field ref: #15 #248 #353 --- app/itam/models/software.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 5868463de..7cd116d63 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -268,6 +268,10 @@ class Meta: verbose_name = 'Name' ) + # model does not have it's own page + # as it's a secondary model. + page_layout: list = [] + table_fields: list = [ 'name', 'organization', From 6ba172e2dcca8f2196375fcedb2ab3e7b658f2eb Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:22:33 +0930 Subject: [PATCH 266/617] test(itam): Device Type API ViewSet permission checks ref: #15 #248 #354 --- .../device_type/test_device_type_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/itam/tests/unit/device_type/test_device_type_viewset.py diff --git a/app/itam/tests/unit/device_type/test_device_type_viewset.py b/app/itam/tests/unit/device_type/test_device_type_viewset.py new file mode 100644 index 000000000..fed7edb34 --- /dev/null +++ b/app/itam/tests/unit/device_type/test_device_type_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.device import DeviceType + + + +class DeviceTypePermissionsAPI(TestCase, APIPermissions): + + model = DeviceType + + app_namespace = 'API' + + url_name = '_api_v2_device_type' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 7d73de22644c5b8d546deaf4ee32883d31f434b0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:27:58 +0930 Subject: [PATCH 267/617] test(itam): Device Model API field checks ref: #15 #248 #354 --- .../device_model/test_device_model_api_v2.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 app/itam/tests/unit/device_model/test_device_model_api_v2.py diff --git a/app/itam/tests/unit/device_model/test_device_model_api_v2.py b/app/itam/tests/unit/device_model/test_device_model_api_v2.py new file mode 100644 index 000000000..3ebbc1a57 --- /dev/null +++ b/app/itam/tests/unit/device_model/test_device_model_api_v2.py @@ -0,0 +1,155 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + +from itam.models.device import DeviceModel + + + +class DeviceModelAPI( + TestCase, + APITenancyObject +): + + model = DeviceModel + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + manufacturer = Manufacturer.objects.create( + organization = self.organization, + name = 'a manufacturer' + ) + + self.item = self.model.objects.create( + organization = self.organization, + name = 'a model', + manufacturer = manufacturer, + model_notes = 'a note', + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_device_model-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_manufacturer(self): + """ Test for existance of API Field + + manufacturer field must exist + """ + + assert 'manufacturer' in self.api_data + + + def test_api_field_type_manufacturer(self): + """ Test for type for API Field + + manufacturer field must be dict + """ + + assert type(self.api_data['manufacturer']) is dict + + + def test_api_field_exists_manufacturer_id(self): + """ Test for existance of API Field + + manufacturer.id field must exist + """ + + assert 'id' in self.api_data['manufacturer'] + + + def test_api_field_type_manufacturer_id(self): + """ Test for type for API Field + + manufacturer.id field must be int + """ + + assert type(self.api_data['manufacturer']['id']) is int + + + def test_api_field_exists_manufacturer_display_name(self): + """ Test for existance of API Field + + manufacturer.display_name field must exist + """ + + assert 'display_name' in self.api_data['manufacturer'] + + + def test_api_field_type_manufacturer_display_name(self): + """ Test for type for API Field + + manufacturer.display_name field must be str + """ + + assert type(self.api_data['manufacturer']['display_name']) is str + + + def test_api_field_exists_manufacturer_url(self): + """ Test for existance of API Field + + manufacturer.url field must exist + """ + + assert 'url' in self.api_data['manufacturer'] + + + def test_api_field_type_manufacturer_url(self): + """ Test for type for API Field + + manufacturer.url field must be Hyperlink + """ + + assert type(self.api_data['manufacturer']['url']) is Hyperlink + From f44b97248fdc954aa794e2d23d85076d2b617af3 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:30:41 +0930 Subject: [PATCH 268/617] test(itam): Device Model Serializer Validation checks ref: #15 #248 #353 --- .../test_device_model_serializer.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 app/itam/tests/unit/device_model/test_device_model_serializer.py diff --git a/app/itam/tests/unit/device_model/test_device_model_serializer.py b/app/itam/tests/unit/device_model/test_device_model_serializer.py new file mode 100644 index 000000000..ba06decc2 --- /dev/null +++ b/app/itam/tests/unit/device_model/test_device_model_serializer.py @@ -0,0 +1,46 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.device_model import DeviceModel, DeviceModelModelSerializer + + + +class DeviceModelValidationAPI( + TestCase, +): + + model = DeviceModel + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceModelModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From ae7355ba35b711ecd626e9860f4218b3980eaed6 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:36:07 +0930 Subject: [PATCH 269/617] test(itam): Device Model API ViewSet permission checks ref: #15 #248 #354 --- .../device_model/test_device_model_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/itam/tests/unit/device_model/test_device_model_viewset.py diff --git a/app/itam/tests/unit/device_model/test_device_model_viewset.py b/app/itam/tests/unit/device_model/test_device_model_viewset.py new file mode 100644 index 000000000..aabbdb520 --- /dev/null +++ b/app/itam/tests/unit/device_model/test_device_model_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.device import DeviceModel + + + +class DeviceModelPermissionsAPI(TestCase, APIPermissions): + + model = DeviceModel + + app_namespace = 'API' + + url_name = '_api_v2_device_model' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 613b904648926945c311631f3a948580eae3c98f Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 17:36:54 +0930 Subject: [PATCH 270/617] fix(itam): Don't attempt to include manufacturer in name for Device Model if not defined ref: #248 #354 --- app/itam/models/device_models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/itam/models/device_models.py b/app/itam/models/device_models.py index 6a9ee5ec1..ac74cf970 100644 --- a/app/itam/models/device_models.py +++ b/app/itam/models/device_models.py @@ -84,4 +84,8 @@ def clean(self): def __str__(self): - return self.manufacturer.name + ' ' + self.name + if self.manufacturer: + + return self.manufacturer.name + ' ' + self.name + + return self.name From 5a7dc5afd1c55c47115fdc6d3d2e9cb4f2ee7625 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 18:11:10 +0930 Subject: [PATCH 271/617] test(itam): Device Software API field checks ref: #15 #248 #354 --- app/itam/serializers/device_software.py | 1 + .../test_device_software_api_v2.py | 772 ++++++++++++++++++ 2 files changed, 773 insertions(+) create mode 100644 app/itam/tests/unit/device_software/test_device_software_api_v2.py diff --git a/app/itam/serializers/device_software.py b/app/itam/serializers/device_software.py index 6671c410c..02920d164 100644 --- a/app/itam/serializers/device_software.py +++ b/app/itam/serializers/device_software.py @@ -94,6 +94,7 @@ class Meta: read_only_fields = [ 'id', + 'software', 'category', 'device', 'installed', diff --git a/app/itam/tests/unit/device_software/test_device_software_api_v2.py b/app/itam/tests/unit/device_software/test_device_software_api_v2.py new file mode 100644 index 000000000..c485d1850 --- /dev/null +++ b/app/itam/tests/unit/device_software/test_device_software_api_v2.py @@ -0,0 +1,772 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.manufacturer import Manufacturer + +from itam.models.device import Device, DeviceSoftware +from itam.models.software import Software, SoftwareCategory, SoftwareVersion + + + +class DeviceSoftwareAPI( + TestCase, + APITenancyObject +): + + model = DeviceSoftware + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + manufacturer = Manufacturer.objects.create( + organization = self.organization, + name = 'a manufacturer' + ) + + category = SoftwareCategory.objects.create( + organization = self.organization, + name = 'category' + ) + + + software = Software.objects.create( + organization = self.organization, + name = 'one', + publisher = manufacturer, + category = category, + model_notes = 'a note' + ) + + software_version = SoftwareVersion.objects.create( + organization = self.organization, + name = '10', + software = software + ) + + device = Device.objects.create( + organization = self.organization, + name = 'device' + ) + + self.item = self.model.objects.create( + organization = self.organization, + device = device, + software = software, + version = software_version, + installedversion = software_version, + installed = '2024-01-01 01:00:00', + action = DeviceSoftware.Actions.INSTALL, + model_notes = 'notes' + + ) + + + self.url_view_kwargs = {'device_id': device.id, 'pk': self.item.id} + + self.url_kwargs = {'deice_id': device.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_device_software-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_display_name(self): + """ Test for existance of API Field + + this test case is a custom test of a test with the same name. + this model does not have a display_name field. + + display_name field must exist + """ + + assert 'display_name' not in self.api_data + + + def test_api_field_type_display_name(self): + """ Test for type for API Field + + this test case is a custom test of a test with the same name. + this model does not have a display_name field. + + display_name field must be str + """ + + assert True + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + this test case is a custom test of a test with the same name. + this model does not have a model_notes field. + + model_notes field must exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + this test case is a custom test of a test with the same name. + this model does not have a model_notes field. + + model_notes field must be str + """ + + assert True + + + + def test_api_field_exists_action(self): + """ Test for existance of API Field + + action field must exist + """ + + assert 'action' in self.api_data + + + def test_api_field_type_action(self): + """ Test for type for API Field + + action field must be int + """ + + assert type(self.api_data['action']) is int + + + + def test_api_field_exists_installed(self): + """ Test for existance of API Field + + installed field must exist + """ + + assert 'installed' in self.api_data + + + def test_api_field_type_installed(self): + """ Test for type for API Field + + installed field must be str + """ + + assert type(self.api_data['installed']) is str + + + + def test_api_field_exists_action_badge(self): + """ Test for existance of API Field + + action_badge field must exist + """ + + assert 'action_badge' in self.api_data + + + def test_api_field_type_action_badge(self): + """ Test for type for API Field + + action_badge field must be dict + """ + + assert type(self.api_data['action_badge']) is dict + + + + def test_api_field_exists_action_badge_icon(self): + """ Test for existance of API Field + + action_badge.icon field must exist + """ + + assert 'icon' in self.api_data['action_badge'] + + + def test_api_field_type_action_badge(self): + """ Test for type for API Field + + action_badge.icon field must be dict + """ + + assert type(self.api_data['action_badge']['icon']) is dict + + + + def test_api_field_exists_action_badge_icon_name(self): + """ Test for existance of API Field + + action_badge.icon.name field must exist + """ + + assert 'name' in self.api_data['action_badge']['icon'] + + + def test_api_field_type_action_badge_icon_name(self): + """ Test for type for API Field + + action_badge.icon.name field must be str + """ + + assert type(self.api_data['action_badge']['icon']['name']) is str + + + + def test_api_field_exists_action_badge_icon_style(self): + """ Test for existance of API Field + + action_badge.icon.style field must exist + """ + + assert 'style' in self.api_data['action_badge']['icon'] + + + def test_api_field_type_action_badge_icon_style(self): + """ Test for type for API Field + + action_badge.icon.style field must be str + """ + + assert type(self.api_data['action_badge']['icon']['style']) is str + + + + def test_api_field_exists_action_badge_text(self): + """ Test for existance of API Field + + action_badge.text field must exist + """ + + assert 'text' in self.api_data['action_badge'] + + + def test_api_field_type_action_text(self): + """ Test for type for API Field + + action_badge.text field must be str + """ + + assert type(self.api_data['action_badge']['text']) is str + + + def test_api_field_exists_action_badge_text_style(self): + """ Test for existance of API Field + + action_badge.text_style field must exist + """ + + assert 'text_style' in self.api_data['action_badge'] + + + def test_api_field_type_action_text_style(self): + """ Test for type for API Field + + action_badge.text_style field must be str + """ + + assert type(self.api_data['action_badge']['text_style']) is str + + + def test_api_field_exists_action_badge_url(self): + """ Test for existance of API Field + + action_badge.url field must exist + """ + + assert 'url' in self.api_data['action_badge'] + + + def test_api_field_type_action_url(self): + """ Test for type for API Field + + action_badge.url field must be str + """ + + assert type(self.api_data['action_badge']['url']) is str + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_id(self): + """ Test for existance of API Field + + category.id field must exist + """ + + assert 'id' in self.api_data['category'] + + + def test_api_field_type_category_id(self): + """ Test for type for API Field + + category.id field must be int + """ + + assert type(self.api_data['category']['id']) is int + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink + + + + def test_api_field_exists_device(self): + """ Test for existance of API Field + + device field must exist + """ + + assert 'device' in self.api_data + + + def test_api_field_type_device(self): + """ Test for type for API Field + + device field must be dict + """ + + assert type(self.api_data['device']) is dict + + + def test_api_field_exists_device_id(self): + """ Test for existance of API Field + + device.id field must exist + """ + + assert 'id' in self.api_data['device'] + + + def test_api_field_type_device_id(self): + """ Test for type for API Field + + device.id field must be int + """ + + assert type(self.api_data['device']['id']) is int + + + def test_api_field_exists_device_display_name(self): + """ Test for existance of API Field + + device.display_name field must exist + """ + + assert 'display_name' in self.api_data['device'] + + + def test_api_field_type_device_display_name(self): + """ Test for type for API Field + + device.display_name field must be str + """ + + assert type(self.api_data['device']['display_name']) is str + + + def test_api_field_exists_device_url(self): + """ Test for existance of API Field + + device.url field must exist + """ + + assert 'url' in self.api_data['device'] + + + def test_api_field_type_device_url(self): + """ Test for type for API Field + + device.url field must be Hyperlink + """ + + assert type(self.api_data['device']['url']) is Hyperlink + + + + def test_api_field_exists_software(self): + """ Test for existance of API Field + + software field must exist + """ + + assert 'software' in self.api_data + + + def test_api_field_type_software(self): + """ Test for type for API Field + + software field must be dict + """ + + assert type(self.api_data['software']) is dict + + + def test_api_field_exists_software_id(self): + """ Test for existance of API Field + + software.id field must exist + """ + + assert 'id' in self.api_data['software'] + + + def test_api_field_type_software_id(self): + """ Test for type for API Field + + software.id field must be int + """ + + assert type(self.api_data['software']['id']) is int + + + def test_api_field_exists_software_display_name(self): + """ Test for existance of API Field + + software.display_name field must exist + """ + + assert 'display_name' in self.api_data['software'] + + + def test_api_field_type_software_display_name(self): + """ Test for type for API Field + + software.display_name field must be str + """ + + assert type(self.api_data['software']['display_name']) is str + + + def test_api_field_exists_software_url(self): + """ Test for existance of API Field + + software.url field must exist + """ + + assert 'url' in self.api_data['software'] + + + def test_api_field_type_software_url(self): + """ Test for type for API Field + + software.url field must be Hyperlink + """ + + assert type(self.api_data['software']['url']) is Hyperlink + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_id(self): + """ Test for existance of API Field + + category.id field must exist + """ + + assert 'id' in self.api_data['category'] + + + def test_api_field_type_category_id(self): + """ Test for type for API Field + + category.id field must be int + """ + + assert type(self.api_data['category']['id']) is int + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink + + + + def test_api_field_exists_version(self): + """ Test for existance of API Field + + version field must exist + """ + + assert 'version' in self.api_data + + + def test_api_field_type_version(self): + """ Test for type for API Field + + version field must be dict + """ + + assert type(self.api_data['version']) is dict + + + def test_api_field_exists_version_id(self): + """ Test for existance of API Field + + version.id field must exist + """ + + assert 'id' in self.api_data['version'] + + + def test_api_field_type_version_id(self): + """ Test for type for API Field + + version.id field must be int + """ + + assert type(self.api_data['version']['id']) is int + + + def test_api_field_exists_version_display_name(self): + """ Test for existance of API Field + + version.display_name field must exist + """ + + assert 'display_name' in self.api_data['version'] + + + def test_api_field_type_version_display_name(self): + """ Test for type for API Field + + version.display_name field must be str + """ + + assert type(self.api_data['version']['display_name']) is str + + + def test_api_field_exists_version_url(self): + """ Test for existance of API Field + + version.url field must exist + """ + + assert 'url' in self.api_data['version'] + + + def test_api_field_type_version_url(self): + """ Test for type for API Field + + version.url field must be str + """ + + assert type(self.api_data['version']['url']) is str + + + + def test_api_field_exists_installedversion(self): + """ Test for existance of API Field + + installedversion field must exist + """ + + assert 'installedversion' in self.api_data + + + def test_api_field_type_installedversion(self): + """ Test for type for API Field + + installedversion field must be dict + """ + + assert type(self.api_data['installedversion']) is dict + + + def test_api_field_exists_installedversion_id(self): + """ Test for existance of API Field + + installedversion.id field must exist + """ + + assert 'id' in self.api_data['installedversion'] + + + def test_api_field_type_installedversion_id(self): + """ Test for type for API Field + + installedversion.id field must be int + """ + + assert type(self.api_data['installedversion']['id']) is int + + + def test_api_field_exists_installedversion_display_name(self): + """ Test for existance of API Field + + installedversion.display_name field must exist + """ + + assert 'display_name' in self.api_data['installedversion'] + + + def test_api_field_type_installedversion_display_name(self): + """ Test for type for API Field + + installedversion.display_name field must be str + """ + + assert type(self.api_data['installedversion']['display_name']) is str + + + def test_api_field_exists_installedversion_url(self): + """ Test for existance of API Field + + installedversion.url field must exist + """ + + assert 'url' in self.api_data['installedversion'] + + + def test_api_field_type_installedversion_url(self): + """ Test for type for API Field + + installedversion.url field must be str + """ + + assert type(self.api_data['installedversion']['url']) is str From d6eebc1cabd5cf7514fe103ea8eb25db9434d7d0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 21:17:32 +0930 Subject: [PATCH 272/617] test(itam): Device Software Serializer Validation checks ref: #15 #248 #353 --- app/app/settings.py | 2 +- app/itam/serializers/device_software.py | 14 ++- .../test_device_software_serializer.py | 115 ++++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 app/itam/tests/unit/device_software/test_device_software_serializer.py diff --git a/app/app/settings.py b/app/app/settings.py index ed682d021..cad22346d 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -309,7 +309,7 @@ """, 'VERSION': '1.0.0', - 'SCHEMA_PATH_PREFIX': '/api/v2/([a-z_]+)/|/api/', + 'SCHEMA_PATH_PREFIX': '/api/v2/|/api/', 'SERVE_INCLUDE_SCHEMA': False, 'SWAGGER_UI_DIST': 'SIDECAR', diff --git a/app/itam/serializers/device_software.py b/app/itam/serializers/device_software.py index 02920d164..b14d7bc55 100644 --- a/app/itam/serializers/device_software.py +++ b/app/itam/serializers/device_software.py @@ -2,6 +2,7 @@ from rest_framework.reverse import reverse from rest_framework import serializers +from rest_framework.fields import empty from access.serializers.organization import OrganizationBaseSerializer @@ -94,9 +95,7 @@ class Meta: read_only_fields = [ 'id', - 'software', 'category', - 'device', 'installed', 'installedversion', 'organization', @@ -105,6 +104,17 @@ class Meta: '_urls', ] + def __init__(self, instance=None, data=empty, **kwargs): + + super().__init__(instance=instance, data=data, **kwargs) + + if isinstance(self.instance, DeviceSoftware): + + self.fields.fields['device'].read_only = True + + self.fields.fields['software'].read_only = True + + def is_valid(self, *, raise_exception=False): diff --git a/app/itam/tests/unit/device_software/test_device_software_serializer.py b/app/itam/tests/unit/device_software/test_device_software_serializer.py new file mode 100644 index 000000000..a03a9f5d9 --- /dev/null +++ b/app/itam/tests/unit/device_software/test_device_software_serializer.py @@ -0,0 +1,115 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.device_software import Device, DeviceSoftware, DeviceSoftwareModelSerializer +from itam.models.software import Software, SoftwareCategory, SoftwareVersion + + + +class DeviceSoftwareValidationAPI( + TestCase, +): + + model = DeviceSoftware + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.software_category = SoftwareCategory.objects.create( + organization=organization, + name = 'category' + ) + + self.software = Software.objects.create( + organization=organization, + name = 'software', + category = self.software_category + ) + + self.software_version = SoftwareVersion.objects.create( + organization=organization, + name = '12', + software = self.software + ) + + self.device = Device.objects.create( + organization=organization, + name = 'device' + ) + + + self.item = self.model.objects.create( + organization=self.organization, + software = self.software, + version = self.software_version, + device = self.device + ) + + + + def test_serializer_validation_create(self): + """Serializer Validation Check + + Ensure that an item can be created + """ + + serializer = DeviceSoftwareModelSerializer(data={ + 'organization': self.organization.pk, + 'software': self.software.pk, + 'version': self.software_version.pk, + 'device': self.device.pk + }) + + assert serializer.is_valid(raise_exception = True) + + + def test_serializer_validation_no_device(self): + """Serializer Validation Check + + Ensure that if creating and no device is provided a validation exception is thrown + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceSoftwareModelSerializer(data={ + 'organization': self.organization.pk, + 'software': self.software.pk, + 'version': self.software_version.pk, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['device'][0] == 'required' + + + def test_serializer_validation_no_software(self): + """Serializer Validation Check + + Ensure that if creating and no device is provided a validation exception is thrown + """ + + with pytest.raises(ValidationError) as err: + + serializer = DeviceSoftwareModelSerializer(data={ + 'organization': self.organization.pk, + 'version': self.software_version.pk, + 'device': self.device.pk + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['software'][0] == 'required' From e504393c090d6ef7e0793207fd654684beaed8cf Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 21:17:50 +0930 Subject: [PATCH 273/617] test(itam): Device Software API ViewSet permission checks ref: #15 #248 #354 --- .../test_device_software_viewset.py | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 app/itam/tests/unit/device_software/test_device_software_viewset.py diff --git a/app/itam/tests/unit/device_software/test_device_software_viewset.py b/app/itam/tests/unit/device_software/test_device_software_viewset.py new file mode 100644 index 000000000..b9fa6950a --- /dev/null +++ b/app/itam/tests/unit/device_software/test_device_software_viewset.py @@ -0,0 +1,193 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.device import Device, DeviceSoftware +from itam.models.software import Software + + + +class DeviceSoftwarePermissionsAPI(TestCase, APIPermissions): + + model = DeviceSoftware + + app_namespace = 'API' + + url_name = '_api_v2_device_software' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + device = Device.objects.create( + organization = self.organization, + name = 'a-device' + ) + + software = Software.objects.create( + organization = self.organization, + name = 'a-software' + ) + + software_b = Software.objects.create( + organization = self.organization, + name = 'b-software' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + device = device, + software = software + ) + + + self.url_view_kwargs = {'device_id': device.id, 'pk': self.item.id} + + self.url_kwargs = {'device_id': device.id} + + self.add_data = { + 'device': device.id, + 'software': software_b.id, + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From e180c3aa547136d72417624f362eeeb4f08d74fa Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 20 Oct 2024 21:18:03 +0930 Subject: [PATCH 274/617] feat(itam): Depreciate API v1 Software Endpoint ref: #248 #354 --- app/api/views/itam/software.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/api/views/itam/software.py b/app/api/views/itam/software.py index 7a13e331f..7c2b0f01f 100644 --- a/app/api/views/itam/software.py +++ b/app/api/views/itam/software.py @@ -1,6 +1,8 @@ from django.db.models import Q from django.shortcuts import get_object_or_404 +from drf_spectacular.utils import extend_schema + from rest_framework import generics, viewsets from access.mixin import OrganizationMixin @@ -11,7 +13,7 @@ from itam.models.software import Software - +@extend_schema(deprecated = True) class SoftwareViewSet(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ From f1c5ebca71a5a5eac539efc1072a6a62bb2b3463 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 14:37:44 +0930 Subject: [PATCH 275/617] feat(itim): Add Service API v2 endpoint ref: #248 #356 --- app/api/urls.py | 4 +- app/itim/serializers/service.py | 85 +++++++++++++++++++++++++++++++ app/itim/viewsets/index.py | 2 +- app/itim/viewsets/service.py | 89 +++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 app/itim/viewsets/service.py diff --git a/app/api/urls.py b/app/api/urls.py index b8fdb76c2..5305fab20 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -79,7 +79,8 @@ ) from itim.viewsets import ( - index as itim_v2 + index as itim_v2, + service as service_v2, ) from project_management.viewsets import ( @@ -163,6 +164,7 @@ router.register('v2/itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') +router.register('v2/itim/service', service_v2.ViewSet, basename='_api_v2_service') router.register('v2/itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index 2f051166f..23a3347c7 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -3,6 +3,10 @@ from access.serializers.organization import OrganizationBaseSerializer +from itam.serializers.device import DeviceBaseSerializer + +from itim.serializers.cluster import ClusterBaseSerializer +from itim.serializers.port import PortBaseSerializer from itim.models.services import Service @@ -15,6 +19,10 @@ def get_display_name(self, item): return str( item ) + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_service-detail", format="html" + ) + class Meta: model = Service @@ -23,10 +31,87 @@ class Meta: 'id', 'display_name', 'name', + 'url', ] read_only_fields = [ 'id', 'display_name', 'name', + 'url', + ] + + +class ServiceModelSerializer(ServiceBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_service-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_service_notes-list", request=self._context['view'].request, kwargs={'service_id': item.pk}), + 'tickets': 'ToDo' + } + + + rendered_config = serializers.JSONField(source='config_variables') + + + class Meta: + + model = Service + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'is_template', + 'template', + 'device', + 'cluster', + 'config', + 'rendered_config', + 'config_key_variable', + 'port', + 'dependent_service', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'rendered_config', + 'created', + 'modified', + '_urls', ] + + + +class ServiceViewSerializer(ServiceModelSerializer): + + cluster = ClusterBaseSerializer( many = False, read_only = True ) + + device = DeviceBaseSerializer( many = False, read_only = True ) + + dependent_service = ServiceBaseSerializer( many = True, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + port = PortBaseSerializer( many = True, read_only = True ) + + template = ServiceBaseSerializer( many = False, read_only = True ) diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py index 0e6ff9024..0fe8e7cd0 100644 --- a/app/itim/viewsets/index.py +++ b/app/itim/viewsets/index.py @@ -29,6 +29,6 @@ def list(self, request, pk=None): "cluster": "ToDo", "incident": "ToDo", "problem": "ToDo", - "service": "ToDo", + "service": reverse('API:_api_v2_service-list', request=request), } ) diff --git a/app/itim/viewsets/service.py b/app/itim/viewsets/service.py new file mode 100644 index 000000000..0156cada0 --- /dev/null +++ b/app/itim/viewsets/service.py @@ -0,0 +1,89 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itim.serializers.service import ( + Service, + ServiceModelSerializer, + ServiceViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a service', + description="""Add a new device to the ITAM database. + If you attempt to create a device and a device with a matching name and uuid or name and serial number + is found within the database, it will not re-create it. The device will be returned within the message body. + """, + responses = { + 201: OpenApiResponse(description='Device created', response=ServiceViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a service', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all services', + description='', + responses = { + 200: OpenApiResponse(description='', response=ServiceViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single service', + description='', + responses = { + 200: OpenApiResponse(description='', response=ServiceViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a service', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ServiceViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'name', + 'cluster', + 'device', + 'port', + ] + + search_fields = [ + 'name', + ] + + model = Service + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] From 02822cc70decb6ca51f21ca72e70332595d8205e Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 15:09:19 +0930 Subject: [PATCH 276/617] feat(itim): Add Cluster Type API v2 endpoint ref: #248 #356 --- app/api/react_ui_metadata.py | 11 ++++ app/api/urls.py | 3 + app/itim/models/clusters.py | 14 +---- app/itim/serializers/cluster_type.py | 92 ++++++++++++++++++++++++++++ app/itim/viewsets/cluster_type.py | 85 +++++++++++++++++++++++++ app/settings/viewsets/index.py | 10 +++ 6 files changed, 202 insertions(+), 13 deletions(-) create mode 100644 app/itim/serializers/cluster_type.py create mode 100644 app/itim/viewsets/cluster_type.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 2d713a516..c48a7ab8d 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -148,6 +148,17 @@ def determine_metadata(self, request, view): } ] }, + { + "display_name": "ITIM", + "name": "itim", + "pages": [ + { + "display_name": "Services", + "name": "service", + "link": "/itim/service" + }, + ] + }, { "display_name": "Config Management", "name": "config_management", diff --git a/app/api/urls.py b/app/api/urls.py index 5305fab20..e79d269c9 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -80,6 +80,7 @@ from itim.viewsets import ( index as itim_v2, + cluster_type as cluster_type_v2, service as service_v2, ) @@ -170,6 +171,8 @@ router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') +router.register('v2/settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') +router.register('v2/settings/cluster_type/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_type_notes') router.register('v2/settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') router.register('v2/settings/device_type', device_type_v2.ViewSet, basename='_api_v2_device_type') router.register('v2/settings/external_link', external_link_v2.ViewSet, basename='_api_v2_external_link') diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index c39c879ba..6a1f97552 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -65,7 +65,7 @@ class Meta: "layout": "double", "left": [ 'organization', - 'name' + 'name', 'is_global', ], "right": [ @@ -82,18 +82,6 @@ class Meta: } ] }, - { - "name": "Rendered Config", - "slug": "config_management", - "sections": [ - { - "layout": "single", - "fields": [ - "rendered_config", - ] - } - ] - }, { "name": "Tickets", "slug": "ticket", diff --git a/app/itim/serializers/cluster_type.py b/app/itim/serializers/cluster_type.py new file mode 100644 index 000000000..c84ed120a --- /dev/null +++ b/app/itim/serializers/cluster_type.py @@ -0,0 +1,92 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.serializers.device import DeviceBaseSerializer + +from itim.models.clusters import ClusterType + + + +class ClusterTypeBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_cluster_type-detail", format="html" + ) + + class Meta: + + model = ClusterType + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class ClusterTypeModelSerializer(ClusterTypeBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_cluster_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_cluster_type_notes-list", request=self._context['view'].request, kwargs={'cluster_type_id': item.pk}), + } + + + class Meta: + + model = ClusterType + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'config', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class ClusterTypeViewSerializer(ClusterTypeModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itim/viewsets/cluster_type.py b/app/itim/viewsets/cluster_type.py new file mode 100644 index 000000000..b2966d0b7 --- /dev/null +++ b/app/itim/viewsets/cluster_type.py @@ -0,0 +1,85 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itim.serializers.cluster_type import ( + ClusterType, + ClusterTypeModelSerializer, + ClusterTypeViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a cluster type', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=ClusterTypeViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a cluster type', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all cluster types', + description='', + responses = { + 200: OpenApiResponse(description='', response=ClusterTypeViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single cluster type', + description='', + responses = { + 200: OpenApiResponse(description='', response=ClusterTypeViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a cluster type', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ClusterTypeViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'is_global', + ] + + search_fields = [ + 'name', + ] + + model = ClusterType + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 1e9c77e9a..146ef43fd 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -60,6 +60,15 @@ class Index(CommonViewSet): "model": "software_category" } ] + }, + { + "name": "ITIM", + "links": [ + { + "name": "Cluster Type", + "model": "cluster_type" + }, + ] } ] @@ -72,6 +81,7 @@ def list(self, request, pk=None): return Response( { + "cluster_type": reverse('API:_api_v2_cluster_type-list', request=request), "device_model": reverse('API:_api_v2_device_model-list', request=request), "device_type": reverse('API:_api_v2_device_type-list', request=request), "external_link": reverse('API:_api_v2_external_link-list', request=request), From 06362f226c451da227d51f5de3b1a965e1ad4a07 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 15:24:29 +0930 Subject: [PATCH 277/617] feat(itim): Add Cluster API v2 endpoint ref: #248 #356 --- app/api/react_ui_metadata.py | 5 ++ app/api/urls.py | 3 + app/itim/models/clusters.py | 2 +- app/itim/serializers/cluster.py | 111 ++++++++++++++++++++++++++++++++ app/itim/viewsets/cluster.py | 86 +++++++++++++++++++++++++ app/itim/viewsets/index.py | 2 +- 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 app/itim/serializers/cluster.py create mode 100644 app/itim/viewsets/cluster.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index c48a7ab8d..f6f894c91 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -152,6 +152,11 @@ def determine_metadata(self, request, view): "display_name": "ITIM", "name": "itim", "pages": [ + { + "display_name": "Clusters", + "name": "cluster", + "link": "/itim/cluster" + }, { "display_name": "Services", "name": "service", diff --git a/app/api/urls.py b/app/api/urls.py index e79d269c9..a988c6f7b 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -80,6 +80,7 @@ from itim.viewsets import ( index as itim_v2, + cluster as cluster_v2, cluster_type as cluster_type_v2, service as service_v2, ) @@ -165,6 +166,8 @@ router.register('v2/itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') +router.register('v2/itim/cluster', cluster_v2.ViewSet, basename='_api_v2_cluster') +router.register('v2/itim/cluster/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_notes') router.register('v2/itim/service', service_v2.ViewSet, basename='_api_v2_service') router.register('v2/itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index 6a1f97552..4080ea112 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -208,7 +208,7 @@ class Meta: 'organization', 'parent_cluster', 'cluster_type', - 'name' + 'name', 'is_global', ], "right": [ diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py new file mode 100644 index 000000000..d0cd85a0c --- /dev/null +++ b/app/itim/serializers/cluster.py @@ -0,0 +1,111 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.serializers.device import DeviceBaseSerializer + +from itim.serializers.cluster_type import ClusterTypeBaseSerializer +from itim.models.clusters import Cluster + + + +class ClusterBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_cluster-detail", format="html" + ) + + class Meta: + + model = Cluster + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class ClusterModelSerializer(ClusterBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_cluster-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), + 'tickets': 'ToDo' + } + + + rendered_config = serializers.JSONField() + + + class Meta: + + model = Cluster + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'parent_cluster', + 'cluster_type', + 'config', + 'rendered_config', + 'nodes', + 'devices', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'rendered_config', + 'created', + 'modified', + '_urls', + ] + + + +class ClusterViewSerializer(ClusterModelSerializer): + + cluster_type = ClusterTypeBaseSerializer( many = False, read_only = True ) + + devices = DeviceBaseSerializer( many = True, read_only = True ) + + nodes = DeviceBaseSerializer( many = True, read_only = True ) + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + parent_cluster = ClusterBaseSerializer( many = False, read_only = True ) diff --git a/app/itim/viewsets/cluster.py b/app/itim/viewsets/cluster.py new file mode 100644 index 000000000..80d61759c --- /dev/null +++ b/app/itim/viewsets/cluster.py @@ -0,0 +1,86 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itim.serializers.cluster import ( + Cluster, + ClusterModelSerializer, + ClusterViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a cluster', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=ClusterViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a cluster', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all clusters', + description='', + responses = { + 200: OpenApiResponse(description='', response=ClusterViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single cluster', + description='', + responses = { + 200: OpenApiResponse(description='', response=ClusterViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a cluster', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ClusterViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'parent_cluster', + 'cluster_type', + 'nodes', + 'devices', + ] + + search_fields = [ + 'name', + ] + + model = Cluster + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py index 0fe8e7cd0..2f7ebaea9 100644 --- a/app/itim/viewsets/index.py +++ b/app/itim/viewsets/index.py @@ -26,7 +26,7 @@ def list(self, request, pk=None): return Response( { "change": "ToDo", - "cluster": "ToDo", + "cluster": reverse('API:_api_v2_cluster-list', request=request), "incident": "ToDo", "problem": "ToDo", "service": reverse('API:_api_v2_service-list', request=request), From cfedd4b74eaab8b652b35594ef0dddcaf63edf09 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 15:43:28 +0930 Subject: [PATCH 278/617] feat(itim): Add Port API v2 endpoint ref: #248 #356 --- app/api/urls.py | 3 ++ app/itim/models/services.py | 4 +- app/itim/serializers/port.py | 97 ++++++++++++++++++++++++++++++++++ app/itim/viewsets/port.py | 84 +++++++++++++++++++++++++++++ app/settings/viewsets/index.py | 5 ++ 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 app/itim/serializers/port.py create mode 100644 app/itim/viewsets/port.py diff --git a/app/api/urls.py b/app/api/urls.py index a988c6f7b..8130e7169 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -82,6 +82,7 @@ index as itim_v2, cluster as cluster_v2, cluster_type as cluster_type_v2, + port as port_v2, service as service_v2, ) @@ -182,6 +183,8 @@ router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') +router.register('v2/settings/port', port_v2.ViewSet, basename='_api_v2_port') +router.register('v2/settings/port/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_port_notes') router.register('v2/settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') urlpatterns = [ diff --git a/app/itim/models/services.py b/app/itim/models/services.py index ce83d9d32..53cd31154 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -23,9 +23,9 @@ class Meta: 'protocol', ] - verbose_name = "Protocol" + verbose_name = "Port" - verbose_name_plural = "Protocols" + verbose_name_plural = "Ports" class Protocol(models.TextChoices): diff --git a/app/itim/serializers/port.py b/app/itim/serializers/port.py new file mode 100644 index 000000000..b62b674ef --- /dev/null +++ b/app/itim/serializers/port.py @@ -0,0 +1,97 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.serializers.device import DeviceBaseSerializer + +from itim.models.services import Port + + + +class PortBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_port-detail", format="html" + ) + + name = serializers.SerializerMethodField('get_display_name') + + class Meta: + + model = Port + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class PortModelSerializer(PortBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_port-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_port_notes-list", request=self._context['view'].request, kwargs={'port_id': item.pk}), + } + + + class Meta: + + model = Port + + fields = [ + 'id', + 'organization', + 'name', + 'model_notes', + 'number', + 'description', + 'protocol', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'created', + 'modified', + '_urls', + ] + + + +class PortViewSerializer(PortModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/itim/viewsets/port.py b/app/itim/viewsets/port.py new file mode 100644 index 000000000..215e40e20 --- /dev/null +++ b/app/itim/viewsets/port.py @@ -0,0 +1,84 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from itim.serializers.port import ( + Port, + PortModelSerializer, + PortViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a port', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=PortViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a port', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all ports', + description='', + responses = { + 200: OpenApiResponse(description='', response=PortViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single port', + description='', + responses = { + 200: OpenApiResponse(description='', response=PortViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a port', + description = '', + responses = { + 200: OpenApiResponse(description='', response=PortViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'number', + 'protocol', + ] + + search_fields = [ + 'description', + ] + + model = Port + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 146ef43fd..9dad4dd9d 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -68,6 +68,10 @@ class Index(CommonViewSet): "name": "Cluster Type", "model": "cluster_type" }, + { + "name": "Service Port", + "model": "port" + }, ] } ] @@ -87,6 +91,7 @@ def list(self, request, pk=None): "external_link": reverse('API:_api_v2_external_link-list', request=request), "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), + "port": reverse('API:_api_v2_port-list', request=request), "software_category": reverse('API:_api_v2_software_category-list', request=request), } ) From b4f3f0ec4808f617706096905fba971584cd8189 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 15:44:56 +0930 Subject: [PATCH 279/617] test(itam): Device Service API field checks ref: #15 #248 #356 --- app/api/urls.py | 2 + app/itam/serializers/device.py | 1 + .../tests/unit/device/test_device_api_v2.py | 36 ++++++++++++++++ app/itim/viewsets/service_device.py | 42 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 app/itim/viewsets/service_device.py diff --git a/app/api/urls.py b/app/api/urls.py index 8130e7169..35e31b54a 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -84,6 +84,7 @@ cluster_type as cluster_type_v2, port as port_v2, service as service_v2, + service_device as service_device_v2 ) from project_management.viewsets import ( @@ -158,6 +159,7 @@ router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('v2/itam/device', device_v2.ViewSet, basename='_api_v2_device') router.register('v2/itam/device/(?P[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software') +router.register('v2/itam/device/(?P[0-9]+)/service', service_device_v2.ViewSet, basename='_api_v2_service_device') router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') router.register('v2/itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system') router.register('v2/itam/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 2ae6472c6..710fa938b 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -68,6 +68,7 @@ def get_url(self, item): } ), 'notes': reverse("API:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'service': reverse("API:_api_v2_service_device-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), 'software': reverse("API:_api_v2_device_software-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), } diff --git a/app/itam/tests/unit/device/test_device_api_v2.py b/app/itam/tests/unit/device/test_device_api_v2.py index fedc94f48..8bef7515e 100644 --- a/app/itam/tests/unit/device/test_device_api_v2.py +++ b/app/itam/tests/unit/device/test_device_api_v2.py @@ -528,6 +528,25 @@ def test_api_field_type_urls_notes(self): + def test_api_field_exists_urls_service(self): + """ Test for existance of API Field + + _urls.service field must exist + """ + + assert 'service' in self.api_data['_urls'] + + + def test_api_field_type_urls_service(self): + """ Test for type for API Field + + _urls.service field must be str + """ + + assert type(self.api_data['_urls']['service']) is str + + + def test_api_field_exists_urls_software(self): """ Test for existance of API Field @@ -546,3 +565,20 @@ def test_api_field_type_urls_software(self): assert type(self.api_data['_urls']['software']) is str + + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str diff --git a/app/itim/viewsets/service_device.py b/app/itim/viewsets/service_device.py new file mode 100644 index 000000000..e4f592710 --- /dev/null +++ b/app/itim/viewsets/service_device.py @@ -0,0 +1,42 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from itim.serializers.service import Service, ServiceModelSerializer, ServiceViewSerializer + +from api.viewsets.common import ModelViewSet + + +@extend_schema_view( + list=extend_schema(exclude=True), + retrieve=extend_schema(exclude=True), + create=extend_schema(exclude=True), + update=extend_schema(exclude=True), + partial_update=extend_schema(exclude=True), + destroy=extend_schema(exclude=True) + ) +class ViewSet(ModelViewSet): + + model = Service + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + + + def get_queryset(self): + + queryset = super().queryset() + + queryset = queryset.filter(device_id=self.kwargs['device_id']) + + self.queryset = queryset + + return self.queryset From cf31f198c8cdbbb5e464176b0dd6557df43ed588 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 15:57:02 +0930 Subject: [PATCH 280/617] test(itam): remove Device Ticket API field checks tickets api endpooint not yet available ref: #15 #248 #356 --- .../tests/unit/device/test_device_api_v2.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/itam/tests/unit/device/test_device_api_v2.py b/app/itam/tests/unit/device/test_device_api_v2.py index 8bef7515e..e665fc021 100644 --- a/app/itam/tests/unit/device/test_device_api_v2.py +++ b/app/itam/tests/unit/device/test_device_api_v2.py @@ -566,19 +566,19 @@ def test_api_field_type_urls_software(self): - def test_api_field_exists_urls_tickets(self): - """ Test for existance of API Field + # def test_api_field_exists_urls_tickets(self): + # """ Test for existance of API Field - _urls.tickets field must exist - """ + # _urls.tickets field must exist + # """ - assert 'tickets' in self.api_data['_urls'] + # assert 'tickets' in self.api_data['_urls'] - def test_api_field_type_urls_tickets(self): - """ Test for type for API Field + # def test_api_field_type_urls_tickets(self): + # """ Test for type for API Field - _urls.tickets field must be str - """ + # _urls.tickets field must be str + # """ - assert type(self.api_data['_urls']['tickets']) is str + # assert type(self.api_data['_urls']['tickets']) is str From d9450921539d7fdf4a74092f7e5d3ed52a6c5ff3 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 16:15:49 +0930 Subject: [PATCH 281/617] fix(itim): Correct Device Service API v2 endpoint ref: #248 #356 --- app/itim/serializers/service.py | 36 +++++++++++++++++++++++--- app/itim/viewsets/service_device.py | 40 +++++++++++++++++++---------- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index 23a3347c7..f09e76ba4 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -1,14 +1,15 @@ -from rest_framework.reverse import reverse +from rest_framework.fields import empty from rest_framework import serializers +from rest_framework.reverse import reverse from access.serializers.organization import OrganizationBaseSerializer -from itam.serializers.device import DeviceBaseSerializer - from itim.serializers.cluster import ClusterBaseSerializer from itim.serializers.port import PortBaseSerializer from itim.models.services import Service +from itam.serializers.device import Device, DeviceBaseSerializer + class ServiceBaseSerializer(serializers.ModelSerializer): @@ -101,6 +102,35 @@ class Meta: ] + def get_field_names(self, declared_fields, info): + + if 'view' in self._context: + + if 'device_id' in self._context['view'].kwargs: + + self.Meta.read_only_fields += [ 'cluster', 'device', 'organization', 'is_global' ] + + fields = super().get_field_names(declared_fields, info) + + return fields + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid(raise_exception=raise_exception) + + if 'view' in self._context: + + if 'device_id' in self._context['view'].kwargs: + + device = Device.objects.get( id = self._context['view'].kwargs['device_id'] ) + + self.validated_data['device'] = device + self.validated_data['organization'] = device.organization + + return is_valid + + class ServiceViewSerializer(ServiceModelSerializer): diff --git a/app/itim/viewsets/service_device.py b/app/itim/viewsets/service_device.py index e4f592710..bfa7306f6 100644 --- a/app/itim/viewsets/service_device.py +++ b/app/itim/viewsets/service_device.py @@ -1,9 +1,14 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse -from itim.serializers.service import Service, ServiceModelSerializer, ServiceViewSerializer - from api.viewsets.common import ModelViewSet +from itim.serializers.service import ( + Service, + ServiceModelSerializer, + ServiceViewSerializer +) + + @extend_schema_view( list=extend_schema(exclude=True), @@ -15,9 +20,29 @@ ) class ViewSet(ModelViewSet): + filterset_fields = [ + 'cluster', + 'port', + ] + + search_fields = [ + 'name', + ] + model = Service + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter(device_id=self.kwargs['device_id']) + + self.queryset = queryset + + return self.queryset + + def get_serializer_class(self): if ( @@ -29,14 +54,3 @@ def get_serializer_class(self): return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] - - - def get_queryset(self): - - queryset = super().queryset() - - queryset = queryset.filter(device_id=self.kwargs['device_id']) - - self.queryset = queryset - - return self.queryset From fe1816156abee749d3f507ed868f06c27ed49823 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 16:54:29 +0930 Subject: [PATCH 282/617] feat(itim): Ensure cluster cant assign itself as parent on api v2 endpoint ref: #248 #356 --- app/itim/models/clusters.py | 1 + app/itim/serializers/cluster.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index 4080ea112..23b2b3516 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -213,6 +213,7 @@ class Meta: ], "right": [ 'model_notes', + 'resources', 'created', 'modified', ] diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index d0cd85a0c..e1262e62d 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -62,7 +62,14 @@ def get_url(self, item): } - rendered_config = serializers.JSONField() + rendered_config = serializers.JSONField( read_only = True) + + resources = serializers.CharField( + label = 'Available Resources', + read_only = True, + initial = 'xx/yy CPU, xx/yy RAM, xx/yy Storage', + default = 'xx/yy CPU, xx/yy RAM, xx/yy Storage', + ) class Meta: @@ -77,6 +84,7 @@ class Meta: 'model_notes', 'parent_cluster', 'cluster_type', + 'resources', 'config', 'rendered_config', 'nodes', @@ -91,12 +99,32 @@ class Meta: 'id', 'display_name', 'rendered_config', + 'resources', 'created', 'modified', '_urls', ] + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid(raise_exception=False) + + + if hasattr(self.instance, 'id') and self.validated_data['parent_cluster']: + + if self.validated_data['parent_cluster'].id == self.instance.id: + + is_valid = False + + raise serializers.ValidationError( + detail = "Cluster can't have itself as its parent cluster", + code = 'parent_not_self' + ) + + return is_valid + + class ClusterViewSerializer(ClusterModelSerializer): From 8f68345bb30934fa733a219127de75ff0d4cda77 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:20:09 +0930 Subject: [PATCH 283/617] test(itam): Cluster API field checks ref: #15 #248 #356 --- .../tests/unit/cluster/test_cluster_api_v2.py | 450 ++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 app/itim/tests/unit/cluster/test_cluster_api_v2.py diff --git a/app/itim/tests/unit/cluster/test_cluster_api_v2.py b/app/itim/tests/unit/cluster/test_cluster_api_v2.py new file mode 100644 index 000000000..091dc9dc3 --- /dev/null +++ b/app/itim/tests/unit/cluster/test_cluster_api_v2.py @@ -0,0 +1,450 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itam.models.device import Device + +from itim.models.clusters import Cluster, ClusterType + + + +class ClusterAPI( + TestCase, + APITenancyObject +): + + model = Cluster + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + node = Device.objects.create( + organization = self.organization, + name = 'node', + ) + + device = Device.objects.create( + organization = self.organization, + name = 'device', + is_virtual = True, + ) + + cluster_type = ClusterType.objects.create( + organization = self.organization, + name = 'cluster_type' + ) + + parent_cluster = Cluster.objects.create( + organization = self.organization, + name = 'two' + ) + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + cluster_type = cluster_type, + config = dict({"one": "two"}), + parent_cluster = parent_cluster, + model_notes = 'a note' + ) + + self.item.devices.set([ device ]) + + self.item.nodes.set([ node ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_cluster-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is dict + + + def test_api_field_exists_rendered_config(self): + """ Test for existance of API Field + + rendered_config field must exist + """ + + assert 'rendered_config' in self.api_data + + + def test_api_field_type_rendered_config(self): + """ Test for type for API Field + + rendered_config field must be dict + """ + + assert type(self.api_data['rendered_config']) is dict + + + def test_api_field_exists_resources(self): + """ Test for existance of API Field + + resources field must exist + """ + + assert 'resources' in self.api_data + + + def test_api_field_type_resources(self): + """ Test for type for API Field + + resources field must be dict + """ + + assert type(self.api_data['resources']) is str + + + + def test_api_field_exists_nodes(self): + """ Test for existance of API Field + + nodes field must exist + """ + + assert 'nodes' in self.api_data + + + def test_api_field_type_nodes(self): + """ Test for type for API Field + + nodes field must be dict + """ + + assert type(self.api_data['nodes'][0]) is dict + + + def test_api_field_exists_nodes_id(self): + """ Test for existance of API Field + + nodes.id field must exist + """ + + assert 'id' in self.api_data['nodes'][0] + + + def test_api_field_type_nodes_id(self): + """ Test for type for API Field + + nodes.id field must be int + """ + + assert type(self.api_data['nodes'][0]['id']) is int + + + def test_api_field_exists_nodes_display_name(self): + """ Test for existance of API Field + + nodes.display_name field must exist + """ + + assert 'display_name' in self.api_data['nodes'][0] + + + def test_api_field_type_nodes_display_name(self): + """ Test for type for API Field + + nodes.display_name field must be str + """ + + assert type(self.api_data['nodes'][0]['display_name']) is str + + + def test_api_field_exists_nodes_url(self): + """ Test for existance of API Field + + nodes.url field must exist + """ + + assert 'url' in self.api_data['nodes'][0] + + + def test_api_field_type_nodes_url(self): + """ Test for type for API Field + + nodes.url field must be Hyperlink + """ + + assert type(self.api_data['nodes'][0]['url']) is Hyperlink + + + + def test_api_field_exists_devices(self): + """ Test for existance of API Field + + devices field must exist + """ + + assert 'devices' in self.api_data + + + def test_api_field_type_devices(self): + """ Test for type for API Field + + devices field must be list + """ + + assert type(self.api_data['devices']) is list + + + def test_api_field_exists_devices_id(self): + """ Test for existance of API Field + + devices.id field must exist + """ + + assert 'id' in self.api_data['devices'][0] + + + def test_api_field_type_devices_id(self): + """ Test for type for API Field + + devices.id field must be int + """ + + assert type(self.api_data['devices'][0]['id']) is int + + + def test_api_field_exists_devices_display_name(self): + """ Test for existance of API Field + + devices.display_name field must exist + """ + + assert 'display_name' in self.api_data['devices'][0] + + + def test_api_field_type_devices_display_name(self): + """ Test for type for API Field + + devices.display_name field must be str + """ + + assert type(self.api_data['devices'][0]['display_name']) is str + + + def test_api_field_exists_devices_url(self): + """ Test for existance of API Field + + devices.url field must exist + """ + + assert 'url' in self.api_data['devices'][0] + + + def test_api_field_type_devices_url(self): + """ Test for type for API Field + + devices.url field must be Hyperlink + """ + + assert type(self.api_data['devices'][0]['url']) is Hyperlink + + + + def test_api_field_exists_cluster_type(self): + """ Test for existance of API Field + + cluster_type field must exist + """ + + assert 'cluster_type' in self.api_data + + + def test_api_field_type_cluster_type(self): + """ Test for type for API Field + + cluster_type field must be dict + """ + + assert type(self.api_data['cluster_type']) is dict + + + def test_api_field_exists_cluster_type_id(self): + """ Test for existance of API Field + + cluster_type.id field must exist + """ + + assert 'id' in self.api_data['cluster_type'] + + + def test_api_field_type_cluster_type_id(self): + """ Test for type for API Field + + cluster_type.id field must be int + """ + + assert type(self.api_data['cluster_type']['id']) is int + + + def test_api_field_exists_cluster_type_display_name(self): + """ Test for existance of API Field + + cluster_type.display_name field must exist + """ + + assert 'display_name' in self.api_data['cluster_type'] + + + def test_api_field_type_cluster_type_display_name(self): + """ Test for type for API Field + + cluster_type.display_name field must be str + """ + + assert type(self.api_data['cluster_type']['display_name']) is str + + + def test_api_field_exists_cluster_type_url(self): + """ Test for existance of API Field + + cluster_type.url field must exist + """ + + assert 'url' in self.api_data['cluster_type'] + + + def test_api_field_type_cluster_type_url(self): + """ Test for type for API Field + + cluster_type.url field must be Hyperlink + """ + + assert type(self.api_data['cluster_type']['url']) is Hyperlink + + + + def test_api_field_exists_parent_cluster(self): + """ Test for existance of API Field + + parent_cluster field must exist + """ + + assert 'parent_cluster' in self.api_data + + + def test_api_field_type_parent_cluster(self): + """ Test for type for API Field + + parent_cluster field must be dict + """ + + assert type(self.api_data['parent_cluster']) is dict + + + def test_api_field_exists_parent_cluster_id(self): + """ Test for existance of API Field + + parent_cluster.id field must exist + """ + + assert 'id' in self.api_data['parent_cluster'] + + + def test_api_field_type_parent_cluster_id(self): + """ Test for type for API Field + + parent_cluster.id field must be int + """ + + assert type(self.api_data['parent_cluster']['id']) is int + + + def test_api_field_exists_parent_cluster_display_name(self): + """ Test for existance of API Field + + parent_cluster.display_name field must exist + """ + + assert 'display_name' in self.api_data['parent_cluster'] + + + def test_api_field_type_parent_cluster_display_name(self): + """ Test for type for API Field + + parent_cluster.display_name field must be str + """ + + assert type(self.api_data['parent_cluster']['display_name']) is str + + + def test_api_field_exists_parent_cluster_url(self): + """ Test for existance of API Field + + parent_cluster.url field must exist + """ + + assert 'url' in self.api_data['parent_cluster'] + + + def test_api_field_type_parent_cluster_url(self): + """ Test for type for API Field + + parent_cluster.url field must be Hyperlink + """ + + assert type(self.api_data['parent_cluster']['url']) is Hyperlink From 176f1c1073f4ce6096085f5e90b6d413aa23250c Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:38:14 +0930 Subject: [PATCH 284/617] fix(itim): Ensure params passed to super when validating cluster ref: #248 #356 --- app/itim/serializers/cluster.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index e1262e62d..0647c6c11 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -108,7 +108,7 @@ class Meta: def is_valid(self, *, raise_exception=False): - is_valid = super().is_valid(raise_exception=False) + is_valid = super().is_valid(raise_exception=raise_exception) if hasattr(self.instance, 'id') and self.validated_data['parent_cluster']: @@ -118,7 +118,9 @@ def is_valid(self, *, raise_exception=False): is_valid = False raise serializers.ValidationError( - detail = "Cluster can't have itself as its parent cluster", + detail = { + "parent_cluster": "Cluster can't have itself as its parent cluster" + }, code = 'parent_not_self' ) From bfe3f10535cb140a141c4ac71f2a0e945f4509b1 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:38:44 +0930 Subject: [PATCH 285/617] test(itim): Cluster Serializer Validation checks ref: #15 #248 #353 --- .../unit/cluster/test_cluster_serializer.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/itim/tests/unit/cluster/test_cluster_serializer.py diff --git a/app/itim/tests/unit/cluster/test_cluster_serializer.py b/app/itim/tests/unit/cluster/test_cluster_serializer.py new file mode 100644 index 000000000..ff107559a --- /dev/null +++ b/app/itim/tests/unit/cluster/test_cluster_serializer.py @@ -0,0 +1,74 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itim.serializers.cluster import Cluster, ClusterModelSerializer + + + +class ClusterValidationAPI( + TestCase, +): + + model = Cluster + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ClusterModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_self_not_parent(self): + """Serializer Validation Check + + Ensure that if assiging itself as parent a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ClusterModelSerializer( + self.item, + data={ + "parent_cluster": self.item.id, + }, + partial = True + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['parent_cluster'] == 'parent_not_self' From 800b5d87cf6b95a0143517e3748fd9abfa7dda9a Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:44:00 +0930 Subject: [PATCH 286/617] test(itim): Cluster API ViewSet permission checks ref: #15 #248 #356 --- app/itim/serializers/cluster.py | 20 +- .../unit/cluster/test_cluster_viewset.py | 173 ++++++++++++++++++ 2 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 app/itim/tests/unit/cluster/test_cluster_viewset.py diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index 0647c6c11..c45c2a380 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -111,18 +111,20 @@ def is_valid(self, *, raise_exception=False): is_valid = super().is_valid(raise_exception=raise_exception) - if hasattr(self.instance, 'id') and self.validated_data['parent_cluster']: + if 'parent_cluster' in self.validated_data: - if self.validated_data['parent_cluster'].id == self.instance.id: + if hasattr(self.instance, 'id') and self.validated_data['parent_cluster']: - is_valid = False + if self.validated_data['parent_cluster'].id == self.instance.id: - raise serializers.ValidationError( - detail = { - "parent_cluster": "Cluster can't have itself as its parent cluster" - }, - code = 'parent_not_self' - ) + is_valid = False + + raise serializers.ValidationError( + detail = { + "parent_cluster": "Cluster can't have itself as its parent cluster" + }, + code = 'parent_not_self' + ) return is_valid diff --git a/app/itim/tests/unit/cluster/test_cluster_viewset.py b/app/itim/tests/unit/cluster/test_cluster_viewset.py new file mode 100644 index 000000000..6b7dce1ab --- /dev/null +++ b/app/itim/tests/unit/cluster/test_cluster_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itim.models.clusters import Cluster + + + +class ClusterPermissionsAPI(TestCase, APIPermissions): + + model = Cluster + + app_namespace = 'API' + + url_name = '_api_v2_cluster' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 0803b2c766403d23ffbf60ae514386a1d2235d08 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:53:03 +0930 Subject: [PATCH 287/617] test(itam): Cluster Type API field checks ref: #15 #248 #356 --- .../cluster_types/test_cluster_type_api_v2.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py diff --git a/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py b/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py new file mode 100644 index 000000000..f796c4563 --- /dev/null +++ b/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py @@ -0,0 +1,93 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itim.models.clusters import ClusterType + + + +class ClusterTypeAPI( + TestCase, + APITenancyObject +): + + model = ClusterType + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + config = dict({"one": "two"}), + model_notes = 'a note' + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_cluster_type-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is dict + From a92bfd427f9e2e1bfb80c7c3cba53d5fc11cac51 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:56:03 +0930 Subject: [PATCH 288/617] test(itim): Cluster Type Serializer Validation checks ref: #15 #248 #353 --- .../test_cluster_type_serializer.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 app/itim/tests/unit/cluster_types/test_cluster_type_serializer.py diff --git a/app/itim/tests/unit/cluster_types/test_cluster_type_serializer.py b/app/itim/tests/unit/cluster_types/test_cluster_type_serializer.py new file mode 100644 index 000000000..54e49f3df --- /dev/null +++ b/app/itim/tests/unit/cluster_types/test_cluster_type_serializer.py @@ -0,0 +1,53 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itim.serializers.cluster_type import ClusterType, ClusterTypeModelSerializer + + + +class ClusterTypeValidationAPI( + TestCase, +): + + model = ClusterType + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + ) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ClusterTypeModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + From 6e34e33c00375381bdf493a08718912dab663910 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 17:58:14 +0930 Subject: [PATCH 289/617] test(itim): Cluster Type API v2 ViewSet permission checks ref: #15 #248 #356 --- .../test_cluster_type_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py diff --git a/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py b/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py new file mode 100644 index 000000000..7fd3a3404 --- /dev/null +++ b/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itim.models.clusters import ClusterType + + + +class ClusterTypePermissionsAPI(TestCase, APIPermissions): + + model = ClusterType + + app_namespace = 'API' + + url_name = '_api_v2_cluster_type' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From a7c9ff4cee932dc270f2f600296787ffeff913f7 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 18:37:22 +0930 Subject: [PATCH 290/617] test(itim): Service API field checks ref: #15 #248 #356 --- .../tests/unit/service/test_service_api_v2.py | 571 ++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 app/itim/tests/unit/service/test_service_api_v2.py diff --git a/app/itim/tests/unit/service/test_service_api_v2.py b/app/itim/tests/unit/service/test_service_api_v2.py new file mode 100644 index 000000000..97a8568e6 --- /dev/null +++ b/app/itim/tests/unit/service/test_service_api_v2.py @@ -0,0 +1,571 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itam.models.device import Device + +from itim.models.clusters import Cluster +from itim.models.services import Service, Port + + + +class ServiceAPI( + TestCase, + APITenancyObject +): + + model = Service + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + port = Port.objects.create( + organization = self.organization, + number = 80, + protocol = Port.Protocol.TCP + ) + + device = Device.objects.create( + organization = self.organization, + name = 'device-one' + ) + + + cluster = Cluster.objects.create( + organization = self.organization, + name = 'cluster one' + ) + + + dependent_service = self.model.objects.create( + organization = self.organization, + name = 'one', + ) + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + model_notes = 'a note', + device = device, + config = dict({ "one": "two"}), + is_template = True, + config_key_variable = 'boo' + ) + + self.item_two = self.model.objects.create( + organization = self.organization, + name = 'one', + model_notes = 'a note', + cluster = cluster, + template = self.item + ) + + + self.item.port.set([ port ]) + + self.item.dependent_service.set([ dependent_service ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_service-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + url = reverse('API:_api_v2_service-detail', kwargs={'pk': self.item_two.id}) + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data_two = response.data + + + + def test_api_field_exists_config(self): + """ Test for existance of API Field + + config field must exist + """ + + assert 'config' in self.api_data + + + def test_api_field_type_config(self): + """ Test for type for API Field + + config field must be dict + """ + + assert type(self.api_data['config']) is dict + + + + def test_api_field_exists_rendered_config(self): + """ Test for existance of API Field + + rendered_config field must exist + """ + + assert 'rendered_config' in self.api_data + + + def test_api_field_type_rendered_config(self): + """ Test for type for API Field + + rendered_config field must be dict + """ + + assert type(self.api_data['rendered_config']) is dict + + + + def test_api_field_exists_is_template(self): + """ Test for existance of API Field + + is_template field must exist + """ + + assert 'is_template' in self.api_data + + + def test_api_field_type_is_template(self): + """ Test for type for API Field + + is_template field must be bool + """ + + assert type(self.api_data['is_template']) is bool + + + + def test_api_field_exists_template(self): + """ Test for existance of API Field + + template field must exist + """ + + assert 'template' in self.api_data_two + + + def test_api_field_type_template(self): + """ Test for type for API Field + + template field must be dict + """ + + assert type(self.api_data_two['template']) is dict + + + + def test_api_field_exists_template_id(self): + """ Test for existance of API Field + + template.id field must exist + """ + + assert 'id' in self.api_data_two['template'] + + + def test_api_field_type_template_id(self): + """ Test for type for API Field + + template.id field must be int + """ + + assert type(self.api_data_two['template']['id']) is int + + + def test_api_field_exists_template_display_name(self): + """ Test for existance of API Field + + template.display_name field must exist + """ + + assert 'display_name' in self.api_data_two['template'] + + + def test_api_field_type_template_display_name(self): + """ Test for type for API Field + + template.display_name field must be str + """ + + assert type(self.api_data_two['template']['display_name']) is str + + + def test_api_field_exists_template_url(self): + """ Test for existance of API Field + + template.url field must exist + """ + + assert 'url' in self.api_data_two['template'] + + + def test_api_field_type_template_url(self): + """ Test for type for API Field + + template.url field must be Hyperlink + """ + + assert type(self.api_data_two['template']['url']) is Hyperlink + + + + def test_api_field_exists_config_key_variable(self): + """ Test for existance of API Field + + config_key_variable field must exist + """ + + assert 'config_key_variable' in self.api_data + + + def test_api_field_type_config_key_variable(self): + """ Test for type for API Field + + config_key_variable field must be str + """ + + assert type(self.api_data['config_key_variable']) is str + + + + def test_api_field_exists_dependent_service(self): + """ Test for existance of API Field + + dependent_service field must exist + """ + + assert 'dependent_service' in self.api_data + + + def test_api_field_type_dependent_service(self): + """ Test for type for API Field + + dependent_service field must be list + """ + + assert type(self.api_data['dependent_service']) is list + + + + def test_api_field_exists_dependent_service_id(self): + """ Test for existance of API Field + + dependent_service.id field must exist + """ + + assert 'id' in self.api_data['dependent_service'][0] + + + def test_api_field_type_dependent_service_id(self): + """ Test for type for API Field + + dependent_service.id field must be int + """ + + assert type(self.api_data['dependent_service'][0]['id']) is int + + + def test_api_field_exists_dependent_service_display_name(self): + """ Test for existance of API Field + + dependent_service.display_name field must exist + """ + + assert 'display_name' in self.api_data['dependent_service'][0] + + + def test_api_field_type_dependent_service_display_name(self): + """ Test for type for API Field + + dependent_service.display_name field must be str + """ + + assert type(self.api_data['dependent_service'][0]['display_name']) is str + + + def test_api_field_exists_dependent_service_url(self): + """ Test for existance of API Field + + dependent_service.url field must exist + """ + + assert 'url' in self.api_data['dependent_service'][0] + + + def test_api_field_type_dependent_service_url(self): + """ Test for type for API Field + + dependent_service.url field must be Hyperlink + """ + + assert type(self.api_data['dependent_service'][0]['url']) is Hyperlink + + + + def test_api_field_exists_port(self): + """ Test for existance of API Field + + port field must exist + """ + + assert 'port' in self.api_data + + + def test_api_field_type_port(self): + """ Test for type for API Field + + port field must be list + """ + + assert type(self.api_data['port']) is list + + + + def test_api_field_exists_port_id(self): + """ Test for existance of API Field + + port.id field must exist + """ + + assert 'id' in self.api_data['port'][0] + + + def test_api_field_type_port_id(self): + """ Test for type for API Field + + port.id field must be int + """ + + assert type(self.api_data['port'][0]['id']) is int + + + def test_api_field_exists_port_display_name(self): + """ Test for existance of API Field + + port.display_name field must exist + """ + + assert 'display_name' in self.api_data['port'][0] + + + def test_api_field_type_port_display_name(self): + """ Test for type for API Field + + port.display_name field must be str + """ + + assert type(self.api_data['port'][0]['display_name']) is str + + + def test_api_field_exists_port_url(self): + """ Test for existance of API Field + + port.url field must exist + """ + + assert 'url' in self.api_data['port'][0] + + + def test_api_field_type_port_url(self): + """ Test for type for API Field + + port.url field must be Hyperlink + """ + + assert type(self.api_data['port'][0]['url']) is Hyperlink + + + + def test_api_field_exists_device(self): + """ Test for existance of API Field + + device field must exist + """ + + assert 'device' in self.api_data + + + def test_api_field_type_device(self): + """ Test for type for API Field + + device field must be dict + """ + + assert type(self.api_data['device']) is dict + + + + def test_api_field_exists_device_id(self): + """ Test for existance of API Field + + device.id field must exist + """ + + assert 'id' in self.api_data['device'] + + + def test_api_field_type_device_id(self): + """ Test for type for API Field + + device.id field must be int + """ + + assert type(self.api_data['device']['id']) is int + + + def test_api_field_exists_device_display_name(self): + """ Test for existance of API Field + + device.display_name field must exist + """ + + assert 'display_name' in self.api_data['device'] + + + def test_api_field_type_device_display_name(self): + """ Test for type for API Field + + device.display_name field must be str + """ + + assert type(self.api_data['device']['display_name']) is str + + + def test_api_field_exists_device_url(self): + """ Test for existance of API Field + + device.url field must exist + """ + + assert 'url' in self.api_data['device'] + + + def test_api_field_type_device_url(self): + """ Test for type for API Field + + device.url field must be Hyperlink + """ + + assert type(self.api_data['device']['url']) is Hyperlink + + + + def test_api_field_exists_cluster(self): + """ Test for existance of API Field + + cluster field must exist + """ + + assert 'cluster' in self.api_data_two + + + def test_api_field_type_cluster(self): + """ Test for type for API Field + + cluster field must be list + """ + + assert type(self.api_data_two['cluster']) is dict + + + + def test_api_field_exists_cluster_id(self): + """ Test for existance of API Field + + cluster.id field must exist + """ + + assert 'id' in self.api_data_two['cluster'] + + + def test_api_field_type_cluster_id(self): + """ Test for type for API Field + + cluster.id field must be int + """ + + assert type(self.api_data_two['cluster']['id']) is int + + + def test_api_field_exists_cluster_display_name(self): + """ Test for existance of API Field + + cluster.display_name field must exist + """ + + assert 'display_name' in self.api_data_two['cluster'] + + + def test_api_field_type_cluster_display_name(self): + """ Test for type for API Field + + cluster.display_name field must be str + """ + + assert type(self.api_data_two['cluster']['display_name']) is str + + + def test_api_field_exists_cluster_url(self): + """ Test for existance of API Field + + cluster.url field must exist + """ + + assert 'url' in self.api_data_two['cluster'] + + + def test_api_field_type_cluster_url(self): + """ Test for type for API Field + + cluster.url field must be Hyperlink + """ + + assert type(self.api_data_two['cluster']['url']) is Hyperlink From a230edf25ad25c121c05c41c4ae5cc01b8612ac7 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 18:38:00 +0930 Subject: [PATCH 291/617] fix(itim): Ensure service config from template is not gathered if not defined ref: #15 #248 #356 --- app/itim/models/services.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 53cd31154..1efc9d9cc 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -334,23 +334,22 @@ def validate_config_key_variable(value): @property def config_variables(self): - if self.is_template: + config: dict = {} - return self.config if self.template: - template_config: dict = Service.objects.get(id=self.template.id).config + if self.template.config: - template_config.update(self.config) + config.update(self.template.config) - return template_config - else: + if self.config: - return self.config + config.update(self.config) + + return config - return None def save(self, force_insert=False, force_update=False, using=None, update_fields=None): From 5fe2b9e646848f97c3ad61c87d4a40006766cfd9 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 20:15:21 +0930 Subject: [PATCH 292/617] test(itim): Service Serializer Validation checks ref: #15 #248 #356 --- .../unit/service/test_service_serializer.py | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 app/itim/tests/unit/service/test_service_serializer.py diff --git a/app/itim/tests/unit/service/test_service_serializer.py b/app/itim/tests/unit/service/test_service_serializer.py new file mode 100644 index 000000000..da3e19524 --- /dev/null +++ b/app/itim/tests/unit/service/test_service_serializer.py @@ -0,0 +1,310 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.models.device import Device + +from itim.models.services import Port + +from itim.models.clusters import Cluster +from itim.serializers.service import Service, ServiceModelSerializer + + + +class ServiceValidationAPI( + TestCase, +): + + model = Service + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + + self.port = Port.objects.create( + organization = self.organization, + number = 80, + protocol = Port.Protocol.TCP + ) + + self.device = Device.objects.create( + organization = self.organization, + name = 'a-device' + ) + + self.cluster = Cluster.objects.create( + organization = self.organization, + name = 'a cluster' + ) + + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + cluster = self.cluster, + config_key_variable = 'value' + ) + + self.item_two = self.model.objects.create( + organization=organization, + name = 'os name', + cluster = self.cluster, + ) + + self.item_two.dependent_service.set([ self.item ]) + + + self.item_is_template = self.model.objects.create( + organization=organization, + name = 'os name', + is_template = True, + ) + + self.item_is_template.port.set([ self.port ]) + + + self.item_is_template_no_port = self.model.objects.create( + organization=organization, + name = 'os name', + is_template = True, + ) + + + + + def test_serializer_validation_can_create_device(self): + """Serializer Validation Check + + Ensure that a valid item is serialized + """ + + serializer = ServiceModelSerializer( + data={ + 'organization': self.organization.id, + 'name': 'service', + 'port': [ + self.port.id + ], + 'config_key_variable': 'a_key', + 'device': self.device.id + }, + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_can_create_cluster(self): + """Serializer Validation Check + + Ensure that a valid item is serialized + """ + + serializer = ServiceModelSerializer( + data={ + 'organization': self.organization.id, + 'name': 'service', + 'port': [ + self.port.id + ], + 'config_key_variable': 'a_key', + 'cluster': self.cluster.id + }, + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'port': [ + self.port.id + ], + 'config_key_variable': 'a_key', + 'device': self.device.id + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_no_port(self): + """Serializer Validation Check + + Ensure that if creating and no port is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'name': 'service', + 'config_key_variable': 'a_key', + 'device': self.device.id + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['port'][0] == 'required' + + + + def test_serializer_validation_no_port_required_if_template_with_port(self): + """Serializer Validation Check + + Ensure that if creating and no port is provided and the template has a port + no validation error occurs + """ + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'name': 'service', + 'config_key_variable': 'a_key', + 'device': self.device.id, + 'template': self.item_is_template.id + }) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_template_without_port(self): + """Serializer Validation Check + + Ensure that if creating a port is provided and the template has no port + no validation error occurs + """ + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'name': 'service', + 'port': [ + self.port.id + ], + 'config_key_variable': 'a_key', + 'device': self.device.id, + 'template': self.item_is_template_no_port.id + }) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_port_or_template_port(self): + """Serializer Validation Check + + Ensure that if creating and no port is provided and the template + has no port a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'name': 'service', + 'config_key_variable': 'a_key', + 'device': self.device.id, + 'template': self.item_is_template_no_port.id + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['port'][0] == 'required' + + + + def test_serializer_validation_no_device(self): + """Serializer Validation Check + + Ensure that if creating and no device is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'name': 'service', + 'port': [ + self.port.id + ], + 'config_key_variable': 'a_key', + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['non_field_errors'][0] == 'one_of_cluster_or_device' + + + + def test_serializer_validation_device_and_cluster(self): + """Serializer Validation Check + + Ensure that if creating and a cluster and device is provided + a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ServiceModelSerializer(data={ + 'organization': self.organization.id, + 'name': 'service', + 'port': [ + self.port.id + ], + 'config_key_variable': 'a_key', + 'device': self.device.id, + 'cluster': self.cluster.id + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['non_field_errors'][0] == 'either_cluster_or_device' + + + + def test_serializer_validation_no_circular_dependency(self): + """Serializer Validation Check + + Ensure that if creating and a dependent service loop + a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ServiceModelSerializer( + self.item, + data={ + 'dependent_service': [ + self.item_two.id + ], + }, + partial = True + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['dependent_service'][0] == 'no_circular_dependencies' From ed34ed34cb69e99f96ca0b60b866d343f3ee9d1c Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 20:16:02 +0930 Subject: [PATCH 293/617] feat(itim): Service Serializer Validations ref: #15 #248 #356 --- app/itim/serializers/service.py | 137 +++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index f09e76ba4..56e3e0bff 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -64,7 +64,7 @@ def get_url(self, item): } - rendered_config = serializers.JSONField(source='config_variables') + rendered_config = serializers.JSONField( source='config_variables', read_only = True ) class Meta: @@ -131,6 +131,141 @@ def is_valid(self, *, raise_exception=False): return is_valid + def validate(self, attrs): + + attrs = super().validate(attrs=attrs) + + cluster = None + + config_key_variable = None + + device = None + + port = [] + + port_required = False + + if self.instance: + + cluster = self.instance.cluster + + config_key_variable = self.instance.config_key_variable + + device = self.instance.device + + port = self.instance.port.all() + + + if 'is_template' in attrs: + + is_template = attrs['is_template'] + + else: + + is_template = self.fields.fields['is_template'].initial + + + if 'template' in attrs: + + template = attrs['template'] + + else: + + template = self.fields.fields['template'].initial + + + if 'device' in attrs: + + device = attrs['device'] + + + if 'cluster' in attrs: + + cluster = attrs['cluster'] + + + if 'config_key_variable' in attrs: + + config_key_variable = attrs['config_key_variable'] + + + if 'port' in attrs: + + port = attrs['port'] + + + if not is_template and not template: + + if not device and not cluster: + + raise serializers.ValidationError( + detail = 'A Service must be assigned to either a "Cluster" or a "Device".', + code = 'one_of_cluster_or_device' + ) + + + if device and cluster: + + raise serializers.ValidationError( + detail = 'A Service must only be assigned to either a "Cluster" or a "Device". Not both.', + code = 'either_cluster_or_device' + ) + + if len(port) == 0: + + port_required = True + + + if template: + + if len(template.port.all()) == 0 and len(port) == 0: + + port_required = True + + + if not is_template and not config_key_variable: + + raise serializers.ValidationError( + detail = { + 'config_key_variable': 'Configuration Key must be specified' + }, + code = 'required' + ) + + if 'dependent_service' in attrs: + + if len(attrs['dependent_service']) > 0: + + for dependency in attrs['dependent_service']: + + if hasattr(self.instance, 'pk'): + + query = Service.objects.filter( + dependent_service = self.instance.pk, + id = dependency.id, + ) + + if query.exists(): + + raise serializers.ValidationError( + detail = { + 'dependent_service': 'A dependent service already depends upon this service. Circular dependencies are not allowed.' + }, + code = 'no_circular_dependencies' + ) + + if port_required: + + raise serializers.ValidationError( + detail = { + 'port': 'Port(s) must be assigned to a service.' + }, + code = 'required' + ) + + return attrs + + class ServiceViewSerializer(ServiceModelSerializer): From 2dbee2a058c4963eb721c8798c7200fb80b9aa51 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 20:27:20 +0930 Subject: [PATCH 294/617] test(itim): Service API v2 ViewSet permission checks ref: #15 #248 #356 --- .../unit/service/test_service_viewset.py | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 app/itim/tests/unit/service/test_service_viewset.py diff --git a/app/itim/tests/unit/service/test_service_viewset.py b/app/itim/tests/unit/service/test_service_viewset.py new file mode 100644 index 000000000..5282ead73 --- /dev/null +++ b/app/itim/tests/unit/service/test_service_viewset.py @@ -0,0 +1,193 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itam.models.device import Device + +from itim.models.services import Service, Port + + + +class ServicePermissionsAPI(TestCase, APIPermissions): + + model = Service + + app_namespace = 'API' + + url_name = '_api_v2_service' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + device = Device.objects.create( + organization=organization, + name = 'device' + ) + + port = Port.objects.create( + organization=organization, + number = 80, + protocol = Port.Protocol.TCP + ) + + self.item = self.model.objects.create( + organization=organization, + name = 'os name', + device = device, + config_key_variable = 'value' + ) + + self.item.port.set([ port ]) + + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + 'device': device.id, + 'port': [ port.id ], + 'config_key_variable': 'value' + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 814c4b2beba670f3fa37422c6cb2b4b331055ba2 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 20:56:13 +0930 Subject: [PATCH 295/617] test(itim): Port API field checks ref: #15 #248 #356 --- app/itim/serializers/port.py | 1 + app/itim/tests/unit/port/test_port_api_v2.py | 132 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 app/itim/tests/unit/port/test_port_api_v2.py diff --git a/app/itim/serializers/port.py b/app/itim/serializers/port.py index b62b674ef..32ceb9bdd 100644 --- a/app/itim/serializers/port.py +++ b/app/itim/serializers/port.py @@ -70,6 +70,7 @@ class Meta: fields = [ 'id', 'organization', + 'display_name', 'name', 'model_notes', 'number', diff --git a/app/itim/tests/unit/port/test_port_api_v2.py b/app/itim/tests/unit/port/test_port_api_v2.py new file mode 100644 index 000000000..4ce1ef32a --- /dev/null +++ b/app/itim/tests/unit/port/test_port_api_v2.py @@ -0,0 +1,132 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from itim.models.services import Port + + + +class PortAPI( + TestCase, + APITenancyObject +): + + model = Port + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + self.item = self.model.objects.create( + organization = self.organization, + number = 80, + protocol = Port.Protocol.TCP, + description = 'one', + model_notes = 'a note' + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('API:_api_v2_port-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_number(self): + """ Test for existance of API Field + + number field must exist + """ + + assert 'number' in self.api_data + + + def test_api_field_type_number(self): + """ Test for type for API Field + + number field must be int + """ + + assert type(self.api_data['number']) is int + + + + def test_api_field_exists_description(self): + """ Test for existance of API Field + + description field must exist + """ + + assert 'description' in self.api_data + + + def test_api_field_type_description(self): + """ Test for type for API Field + + description field must be str + """ + + assert type(self.api_data['description']) is str + + + + def test_api_field_exists_protocol(self): + """ Test for existance of API Field + + protocol field must exist + """ + + assert 'protocol' in self.api_data + + + def test_api_field_type_protocol(self): + """ Test for type for API Field + + protocol field must be str + """ + + assert type(self.api_data['protocol']) is str From 48b5754dcfad877b01c1cd60e4b8e2e7d547549f Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 21:05:42 +0930 Subject: [PATCH 296/617] feat(itim): Port Serializer Validations ref: #15 #248 #356 --- ..._alter_port_options_alter_port_protocol.py | 22 +++++ app/itim/models/services.py | 1 - .../tests/unit/port/test_port_serializer.py | 90 +++++++++++++++++++ 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 app/itim/migrations/0006_alter_port_options_alter_port_protocol.py create mode 100644 app/itim/tests/unit/port/test_port_serializer.py diff --git a/app/itim/migrations/0006_alter_port_options_alter_port_protocol.py b/app/itim/migrations/0006_alter_port_options_alter_port_protocol.py new file mode 100644 index 000000000..173a79747 --- /dev/null +++ b/app/itim/migrations/0006_alter_port_options_alter_port_protocol.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2024-10-21 11:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('itim', '0005_alter_cluster_cluster_type_alter_cluster_id_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='port', + options={'ordering': ['number', 'protocol'], 'verbose_name': 'Port', 'verbose_name_plural': 'Ports'}, + ), + migrations.AlterField( + model_name='port', + name='protocol', + field=models.CharField(choices=[('TCP', 'TCP'), ('UDP', 'UDP')], help_text='Layer 4 Network Protocol', max_length=3, verbose_name='Protocol'), + ), + ] diff --git a/app/itim/models/services.py b/app/itim/models/services.py index 1efc9d9cc..d5daff4d9 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -67,7 +67,6 @@ def validation_port_number(number: int): protocol = models.CharField( blank = False, choices=Protocol.choices, - default = Protocol.TCP, help_text = 'Layer 4 Network Protocol', max_length = 3, verbose_name = 'Protocol', diff --git a/app/itim/tests/unit/port/test_port_serializer.py b/app/itim/tests/unit/port/test_port_serializer.py new file mode 100644 index 000000000..39f11b01a --- /dev/null +++ b/app/itim/tests/unit/port/test_port_serializer.py @@ -0,0 +1,90 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itim.serializers.port import Port, PortModelSerializer + + + +class PortValidationAPI( + TestCase, +): + + model = Port + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + # 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + # self.item = self.model.objects.create( + # organization=organization, + # number = 'os name', + # ) + + + + def test_serializer_validation_can_create(self): + """Serializer Validation Check + + Ensure that a valid item has no validation errors + """ + + serializer = PortModelSerializer(data={ + "organization": self.organization.id, + "number": 80, + "protocol": Port.Protocol.TCP + }) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_number(self): + """Serializer Validation Check + + Ensure that if creating and no number is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = PortModelSerializer(data={ + "organization": self.organization.id, + # "number": 80, + "protocol": Port.Protocol.TCP + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['number'][0] == 'required' + + + + def test_serializer_validation_no_protocol(self): + """Serializer Validation Check + + Ensure that if creating and no protocol is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = PortModelSerializer(data={ + "organization": self.organization.id, + "number": 80, + # "protocol": Port.Protocol.TCP + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['protocol'][0] == 'required' From e524d4d43dbcadd019798f8b01d2c204d78baea5 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 21 Oct 2024 21:09:19 +0930 Subject: [PATCH 297/617] test(itim): Port API v2 ViewSet permission checks ref: #15 #248 #356 --- app/itim/tests/unit/port/test_port_viewset.py | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 app/itim/tests/unit/port/test_port_viewset.py diff --git a/app/itim/tests/unit/port/test_port_viewset.py b/app/itim/tests/unit/port/test_port_viewset.py new file mode 100644 index 000000000..98a94c656 --- /dev/null +++ b/app/itim/tests/unit/port/test_port_viewset.py @@ -0,0 +1,175 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from itim.models.services import Port + + + +class PortPermissionsAPI(TestCase, APIPermissions): + + model = Port + + app_namespace = 'API' + + url_name = '_api_v2_port' + + change_data = {'number': 21} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + number = 80, + protocol = Port.Protocol.TCP + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'number': 80, + 'protocol': Port.Protocol.TCP, + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From a70864480997f028f9e996f87380707452f22a2e Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 10:52:33 +0930 Subject: [PATCH 298/617] feat(project_management): Add Project API v2 endpoint ref: #248 #357 --- app/api/urls.py | 4 +- app/project_management/serializers/project.py | 110 ++++++++++++++++++ app/project_management/viewsets/index.py | 2 +- app/project_management/viewsets/project.py | 87 ++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 app/project_management/serializers/project.py create mode 100644 app/project_management/viewsets/project.py diff --git a/app/api/urls.py b/app/api/urls.py index 35e31b54a..713c1ac23 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -88,7 +88,8 @@ ) from project_management.viewsets import ( - index as project_management_v2 + index as project_management_v2, + project as project_v2, ) from settings.viewsets import ( @@ -175,6 +176,7 @@ router.register('v2/itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') +router.register('v2/project_management/project', project_v2.ViewSet, basename='_api_v2_project') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') router.register('v2/settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py new file mode 100644 index 000000000..4ffc3ea06 --- /dev/null +++ b/app/project_management/serializers/project.py @@ -0,0 +1,110 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from itam.serializers.device import DeviceBaseSerializer + +from project_management.models.projects import Project + + + +class ProjectBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_project-detail", format="html" + ) + + class Meta: + + model = Project + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class ProjectModelSerializer(ProjectBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_project-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'history': reverse( + "API:_api_v2_model_history-list", + request=self._context['view'].request, + kwargs={ + 'model_class': self.Meta.model._meta.model_name, + 'model_id': item.pk + } + ), + 'notes': reverse("API:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), + 'tickets': 'ToDo' + } + + + class Meta: + + model = Project + + fields = '__all__' + + fields = [ + 'id', + 'external_ref', + 'external_system', + 'organization', + 'display_name', + 'name', + 'description', + 'priority', + 'state', + 'project_type', + 'code', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'manager_user', + 'manager_team', + 'team_members', + 'is_deleted', + + + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class ProjectViewSerializer(ProjectModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/viewsets/index.py b/app/project_management/viewsets/index.py index ba979e831..0914da618 100644 --- a/app/project_management/viewsets/index.py +++ b/app/project_management/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "project": "ToDo", + "project": reverse('API:_api_v2_project-list', request=request), } ) diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py new file mode 100644 index 000000000..950ed6634 --- /dev/null +++ b/app/project_management/viewsets/project.py @@ -0,0 +1,87 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from project_management.serializers.project import ( + Project, + ProjectModelSerializer, + ProjectViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a cluster', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=ProjectViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a cluster', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all clusters', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single cluster', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a cluster', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ProjectViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'external_system', + 'priority', + 'state', + ] + + search_fields = [ + 'name', + 'description', + ] + + model = Project + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer'] From 11e3b04b4643208879537222da92f91f3aa13332 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 11:22:26 +0930 Subject: [PATCH 299/617] feat(project_management): Add Project Milestone API v2 endpoint ref: #248 #357 --- app/api/urls.py | 2 + .../project_management/project_milestone.py | 2 +- app/itam/serializers/device_software.py | 4 +- app/project_management/serializers/project.py | 5 +- .../serializers/project_milestone.py | 129 ++++++++++++++++++ .../viewsets/project_milestone.py | 94 +++++++++++++ 6 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 app/project_management/serializers/project_milestone.py create mode 100644 app/project_management/viewsets/project_milestone.py diff --git a/app/api/urls.py b/app/api/urls.py index 713c1ac23..93d19561f 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -90,6 +90,7 @@ from project_management.viewsets import ( index as project_management_v2, project as project_v2, + project_milestone as project_milestone_v2 ) from settings.viewsets import ( @@ -177,6 +178,7 @@ router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('v2/project_management/project', project_v2.ViewSet, basename='_api_v2_project') +router.register('v2/project_management/project/(?P[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') router.register('v2/settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') diff --git a/app/api/views/project_management/project_milestone.py b/app/api/views/project_management/project_milestone.py index 136d7bcef..e981cd736 100644 --- a/app/api/views/project_management/project_milestone.py +++ b/app/api/views/project_management/project_milestone.py @@ -9,7 +9,7 @@ from api.views.mixin import OrganizationPermissionAPI - +@extend_schema(deprecated = True ) class View(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ diff --git a/app/itam/serializers/device_software.py b/app/itam/serializers/device_software.py index b14d7bc55..9b03bdcab 100644 --- a/app/itam/serializers/device_software.py +++ b/app/itam/serializers/device_software.py @@ -1,8 +1,8 @@ +from rest_framework import serializers from rest_framework.fields import empty from rest_framework.reverse import reverse -from rest_framework import serializers -from rest_framework.fields import empty + from access.serializers.organization import OrganizationBaseSerializer diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 4ffc3ea06..d65abfca3 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -56,6 +56,7 @@ def get_url(self, item): 'model_id': item.pk } ), + 'milestone': reverse("API:_api_v2_project_milestone-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), 'notes': reverse("API:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), 'tickets': 'ToDo' } @@ -65,8 +66,6 @@ class Meta: model = Project - fields = '__all__' - fields = [ 'id', 'external_ref', @@ -87,8 +86,6 @@ class Meta: 'manager_team', 'team_members', 'is_deleted', - - 'is_global', 'created', 'modified', diff --git a/app/project_management/serializers/project_milestone.py b/app/project_management/serializers/project_milestone.py new file mode 100644 index 000000000..04915f315 --- /dev/null +++ b/app/project_management/serializers/project_milestone.py @@ -0,0 +1,129 @@ +from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.reverse import reverse + +from access.serializers.organization import OrganizationBaseSerializer + +from project_management.models.project_milestone import ProjectMilestone +from project_management.serializers.project import Project, ProjectBaseSerializer + + + +class ProjectMilestoneBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return reverse( + "API:_api_v2_project_milestone-detail", + request=self._context['view'].request, + kwargs={ + 'project_id': item.project.id, + 'pk': item.pk + } + ) + + + class Meta: + + model = ProjectMilestone + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class ProjectMilestoneModelSerializer(ProjectMilestoneBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + "API:_api_v2_project_milestone-detail", + request=self._context['view'].request, + kwargs={ + 'project_id': item.project.id, + 'pk': item.pk + } + ), + } + + + class Meta: + + model = ProjectMilestone + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'description', + 'start_date', + 'finish_date', + 'project', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + def __init__(self, instance=None, data=empty, **kwargs): + + super().__init__(instance=instance, data=data, **kwargs) + + self.fields.fields['project'].read_only = True + + self.fields.fields['organization'].read_only = True + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid(raise_exception=raise_exception) + + project = Project.objects.get( + pk = int(self._kwargs['context']['view'].kwargs['project_id']) + ) + + self.validated_data.update({ + 'organization': project.organization, + 'project': project + }) + + return is_valid + + + +class ProjectMilestoneViewSerializer(ProjectMilestoneModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + project = ProjectBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/viewsets/project_milestone.py b/app/project_management/viewsets/project_milestone.py new file mode 100644 index 000000000..6b078ef62 --- /dev/null +++ b/app/project_management/viewsets/project_milestone.py @@ -0,0 +1,94 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from project_management.serializers.project_milestone import ( + ProjectMilestone, + ProjectMilestoneModelSerializer, + ProjectMilestoneViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a cluster', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=ProjectMilestoneViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a cluster', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all clusters', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectMilestoneViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single cluster', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectMilestoneViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a cluster', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ProjectMilestoneViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [] + + search_fields = [ + 'name', + 'description', + ] + + model = ProjectMilestone + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + + def get_queryset(self): + + queryset = super().get_queryset() + + queryset = queryset.filter( project_id = self.kwargs['project_id']) + + self.queryset = queryset + + return self.queryset + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] From 9ecd545d1f4ac5ae46a21c19d6376b0794620bc1 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 12:00:40 +0930 Subject: [PATCH 300/617] feat(project_management): Add Project State API v2 endpoint ref: #248 #357 --- .../views/project_management/project_state.py | 2 +- app/api/views/project_management/projects.py | 2 +- .../models/project_states.py | 2 +- app/project_management/serializers/project.py | 4 + .../serializers/project_states.py | 95 +++++++++++++++++++ .../viewsets/project_state.py | 84 ++++++++++++++++ app/settings/viewsets/index.py | 10 ++ 7 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 app/project_management/serializers/project_states.py create mode 100644 app/project_management/viewsets/project_state.py diff --git a/app/api/views/project_management/project_state.py b/app/api/views/project_management/project_state.py index b529dd431..ad9d08471 100644 --- a/app/api/views/project_management/project_state.py +++ b/app/api/views/project_management/project_state.py @@ -9,7 +9,7 @@ from api.views.mixin import OrganizationPermissionAPI - +@extend_schema(deprecated = True ) class View(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ diff --git a/app/api/views/project_management/projects.py b/app/api/views/project_management/projects.py index 9f068e265..161388325 100644 --- a/app/api/views/project_management/projects.py +++ b/app/api/views/project_management/projects.py @@ -16,7 +16,7 @@ from settings.models.user_settings import UserSettings - +@extend_schema(deprecated = True ) class View(OrganizationMixin, viewsets.ModelViewSet): filterset_fields = [ diff --git a/app/project_management/models/project_states.py b/app/project_management/models/project_states.py index 1e385c3e1..563f08f83 100644 --- a/app/project_management/models/project_states.py +++ b/app/project_management/models/project_states.py @@ -82,7 +82,7 @@ class Meta: 'is_completed', ], "right": [ - 'model_notes' + 'model_notes', 'created', 'modified', ] diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index d65abfca3..01ccf357c 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -6,6 +6,7 @@ from itam.serializers.device import DeviceBaseSerializer from project_management.models.projects import Project +from project_management.serializers.project_states import ProjectStateBaseSerializer @@ -40,6 +41,7 @@ class Meta: ] + class ProjectModelSerializer(ProjectBaseSerializer): _urls = serializers.SerializerMethodField('get_url') @@ -105,3 +107,5 @@ class Meta: class ProjectViewSerializer(ProjectModelSerializer): organization = OrganizationBaseSerializer( many = False, read_only = True ) + + state = ProjectStateBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/serializers/project_states.py b/app/project_management/serializers/project_states.py new file mode 100644 index 000000000..665564cdb --- /dev/null +++ b/app/project_management/serializers/project_states.py @@ -0,0 +1,95 @@ +from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.reverse import reverse + +from access.serializers.organization import OrganizationBaseSerializer + +from assistance.serializers.knowledge_base import KnowledgeBaseBaseSerializer + +from project_management.models.project_states import ProjectState + + + +class ProjectStateBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_project_state-detail", format="html" + ) + + + class Meta: + + model = ProjectState + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class ProjectStateModelSerializer(ProjectStateBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + "API:_api_v2_project_state-detail", + request=self._context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + } + + + class Meta: + + model = ProjectState + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'runbook', + 'is_completed', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class ProjectStateViewSerializer(ProjectStateModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + runbook = KnowledgeBaseBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/viewsets/project_state.py b/app/project_management/viewsets/project_state.py new file mode 100644 index 000000000..fdee26cef --- /dev/null +++ b/app/project_management/viewsets/project_state.py @@ -0,0 +1,84 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from project_management.serializers.project_states import ( + ProjectState, + ProjectStateModelSerializer, + ProjectStateViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a project state', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=ProjectStateViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a project state', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all project states', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectStateViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single project state', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectStateViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a project state', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ProjectStateViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'is_global', + ] + + search_fields = [ + 'name', + ] + + model = ProjectState + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 9dad4dd9d..d16f1fe33 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -73,6 +73,15 @@ class Index(CommonViewSet): "model": "port" }, ] + }, + { + "name": "Project Management", + "links": [ + { + "name": "Project State", + "model": "project_state" + }, + ] } ] @@ -92,6 +101,7 @@ def list(self, request, pk=None): "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), "port": reverse('API:_api_v2_port-list', request=request), + "project_state": reverse('API:_api_v2_project_state-list', request=request), "software_category": reverse('API:_api_v2_software_category-list', request=request), } ) From ce566a8928df2f2ca0e6fcd60caed2a70ca8fe52 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 12:02:29 +0930 Subject: [PATCH 301/617] feat(project_management): Add Project Type API v2 endpoint ref: #248 #357 --- .../views/project_management/project_type.py | 2 +- app/project_management/serializers/project.py | 3 + .../serializers/project_type.py | 93 +++++++++++++++++++ app/project_management/viewsets/project.py | 1 + .../viewsets/project_type.py | 84 +++++++++++++++++ app/settings/viewsets/index.py | 5 + 6 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 app/project_management/serializers/project_type.py create mode 100644 app/project_management/viewsets/project_type.py diff --git a/app/api/views/project_management/project_type.py b/app/api/views/project_management/project_type.py index f16cb8866..757d92f4a 100644 --- a/app/api/views/project_management/project_type.py +++ b/app/api/views/project_management/project_type.py @@ -8,7 +8,7 @@ from api.views.mixin import OrganizationPermissionAPI - +@extend_schema(deprecated = True ) class View(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 01ccf357c..0c238d5fc 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -7,6 +7,7 @@ from project_management.models.projects import Project from project_management.serializers.project_states import ProjectStateBaseSerializer +from project_management.serializers.project_type import ProjectTypeBaseSerializer @@ -109,3 +110,5 @@ class ProjectViewSerializer(ProjectModelSerializer): organization = OrganizationBaseSerializer( many = False, read_only = True ) state = ProjectStateBaseSerializer( many = False, read_only = True ) + + project_type = ProjectTypeBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/serializers/project_type.py b/app/project_management/serializers/project_type.py new file mode 100644 index 000000000..1ee9c15bb --- /dev/null +++ b/app/project_management/serializers/project_type.py @@ -0,0 +1,93 @@ +from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.reverse import reverse + +from access.serializers.organization import OrganizationBaseSerializer + +from assistance.serializers.knowledge_base import KnowledgeBaseBaseSerializer + +from project_management.models.project_types import ProjectType + + + +class ProjectTypeBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_project_type-detail", format="html" + ) + + + class Meta: + + model = ProjectType + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class ProjectTypeModelSerializer(ProjectTypeBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse( + "API:_api_v2_project_type-detail", + request=self._context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + } + + + class Meta: + + model = ProjectType + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'runbook', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class ProjectTypeViewSerializer(ProjectTypeModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + runbook = KnowledgeBaseBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py index 950ed6634..74213f443 100644 --- a/app/project_management/viewsets/project.py +++ b/app/project_management/viewsets/project.py @@ -60,6 +60,7 @@ class ViewSet( ModelViewSet ): 'organization', 'external_system', 'priority', + 'project_type', 'state', ] diff --git a/app/project_management/viewsets/project_type.py b/app/project_management/viewsets/project_type.py new file mode 100644 index 000000000..f189b8d3d --- /dev/null +++ b/app/project_management/viewsets/project_type.py @@ -0,0 +1,84 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from project_management.serializers.project_type import ( + ProjectType, + ProjectTypeModelSerializer, + ProjectTypeViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a project type', + description='', + responses = { + 201: OpenApiResponse(description='Device created', response=ProjectTypeViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a project type', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all project types', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectTypeViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single project type', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectTypeViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a project type', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ProjectTypeViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet( ModelViewSet ): + + filterset_fields = [ + 'organization', + 'is_global', + ] + + search_fields = [ + 'name', + ] + + model = ProjectType + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_description = 'Physical Devices' + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index d16f1fe33..15a3601dc 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -81,6 +81,10 @@ class Index(CommonViewSet): "name": "Project State", "model": "project_state" }, + { + "name": "Project Type", + "model": "project_type" + }, ] } ] @@ -102,6 +106,7 @@ def list(self, request, pk=None): "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), "port": reverse('API:_api_v2_port-list', request=request), "project_state": reverse('API:_api_v2_project_state-list', request=request), + "project_type": reverse('API:_api_v2_project_type-list', request=request), "software_category": reverse('API:_api_v2_software_category-list', request=request), } ) From 85a415841326e4f35c982e8b503562956bfe7d9e Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 12:37:54 +0930 Subject: [PATCH 302/617] fix(project_management): for project serializer (api v1) ensure org is id ref: #248 #357 --- app/api/views/project_management/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/project_management/projects.py b/app/api/views/project_management/projects.py index 161388325..39bf86c9e 100644 --- a/app/api/views/project_management/projects.py +++ b/app/api/views/project_management/projects.py @@ -40,7 +40,7 @@ class View(OrganizationMixin, viewsets.ModelViewSet): def get_serializer_class(self): if self.has_organization_permission( - organization = UserSettings.objects.get(user = self.request.user).default_organization, + organization = UserSettings.objects.get(user = self.request.user).default_organization.id, permissions_required = ['project_management.import_project'] ) or self.request.user.is_superuser: From 663f496dc7a75a1160d3b9d061c55fc7a5895687 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 12:40:06 +0930 Subject: [PATCH 303/617] feat(project_management): Project Validation for API v2 ref: #248 #357 --- app/core/serializers/notes.py | 10 ++++++++++ app/project_management/models/projects.py | 4 ++-- app/project_management/serializers/project.py | 20 +++++++++++++++++-- app/project_management/viewsets/project.py | 12 +++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/app/core/serializers/notes.py b/app/core/serializers/notes.py index 9df686375..991a81dca 100644 --- a/app/core/serializers/notes.py +++ b/app/core/serializers/notes.py @@ -97,6 +97,16 @@ def get_url(self, item): } ) + elif 'project_id' in self._kwargs['context']['view'].kwargs: + + _self = reverse("API:_api_v2_project_notes-detail", + request=self._context['view'].request, + kwargs={ + 'project_id': self._kwargs['context']['view'].kwargs['project_id'], + 'pk': item.pk + } + ) + elif 'software_id' in self._kwargs['context']['view'].kwargs: _self = reverse("API:_api_v2_software_notes-detail", diff --git a/app/project_management/models/projects.py b/app/project_management/models/projects.py index ef53fc282..f3979f0c1 100644 --- a/app/project_management/models/projects.py +++ b/app/project_management/models/projects.py @@ -241,11 +241,11 @@ class Priority(models.IntegerChoices): }, { "name": "Milestones", - "slug": "milestones", + "slug": "milestone", "sections": [ { "layout": "table", - "field": "milestones", + "field": "milestone", } ] }, diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 0c238d5fc..51606a367 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -1,5 +1,6 @@ -from rest_framework.reverse import reverse from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.reverse import reverse from access.serializers.organization import OrganizationBaseSerializer @@ -60,7 +61,7 @@ def get_url(self, item): } ), 'milestone': reverse("API:_api_v2_project_milestone-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), - 'notes': reverse("API:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), + 'notes': reverse("API:_api_v2_project_notes-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), 'tickets': 'ToDo' } @@ -105,6 +106,21 @@ class Meta: + def __init__(self, instance=None, data=empty, **kwargs): + + super().__init__(instance=instance, data=data, **kwargs) + + if 'view' in self.context: + + if not self.context['view'].is_import_user: + + self.Meta.read_only_fields += [ + 'external_ref', + 'external_system', + ] + + + class ProjectViewSerializer(ProjectModelSerializer): organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py index 74213f443..5348bbd3d 100644 --- a/app/project_management/viewsets/project.py +++ b/app/project_management/viewsets/project.py @@ -8,6 +8,8 @@ ProjectViewSerializer ) +from settings.models.user_settings import UserSettings + @extend_schema_view( @@ -64,6 +66,8 @@ class ViewSet( ModelViewSet ): 'state', ] + is_import_user: bool = False + search_fields = [ 'name', 'description', @@ -75,8 +79,16 @@ class ViewSet( ModelViewSet ): view_description = 'Physical Devices' + def get_serializer_class(self): + if self.has_organization_permission( + organization = UserSettings.objects.get(user = self.request.user).default_organization.id, + permissions_required = ['project_management.import_project'] + ) or self.request.user.is_superuser: + + self.is_import_user = True + if ( self.action == 'list' or self.action == 'retrieve' From 77e09e8a13e241defb6629123ac0278b2a6acae0 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 12:49:18 +0930 Subject: [PATCH 304/617] feat(project_management): Add remaining Project base serializers for API v2 ref: #248 #357 --- app/api/react_ui_metadata.py | 11 +++++++++++ app/api/urls.py | 7 ++++++- app/project_management/serializers/project.py | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index f6f894c91..ab9df9e42 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -176,6 +176,17 @@ def determine_metadata(self, request, view): } ] }, + { + "display_name": "Project Management", + "name": "project_management", + "pages": [ + { + "display_name": "Projects", + "name": "project", + "link": "/project_management/project" + } + ] + }, { "display_name": "Settings", diff --git a/app/api/urls.py b/app/api/urls.py index 93d19561f..92b5700db 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -90,7 +90,9 @@ from project_management.viewsets import ( index as project_management_v2, project as project_v2, - project_milestone as project_milestone_v2 + project_milestone as project_milestone_v2, + project_state as project_state_v2, + project_type as project_type_v2, ) from settings.viewsets import ( @@ -179,6 +181,7 @@ router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('v2/project_management/project', project_v2.ViewSet, basename='_api_v2_project') router.register('v2/project_management/project/(?P[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone') +router.register('v2/itim/project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') router.register('v2/settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') @@ -191,6 +194,8 @@ router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') router.register('v2/settings/port', port_v2.ViewSet, basename='_api_v2_port') router.register('v2/settings/port/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_port_notes') +router.register('v2/settings/project_state', project_state_v2.ViewSet, basename='_api_v2_project_state') +router.register('v2/settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type') router.register('v2/settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') urlpatterns = [ diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 51606a367..93ad8db1a 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -6,6 +6,10 @@ from itam.serializers.device import DeviceBaseSerializer +from app.serializers.user import UserBaseSerializer + +from access.serializers.teams import TeamBaseSerializer + from project_management.models.projects import Project from project_management.serializers.project_states import ProjectStateBaseSerializer from project_management.serializers.project_type import ProjectTypeBaseSerializer @@ -123,8 +127,14 @@ def __init__(self, instance=None, data=empty, **kwargs): class ProjectViewSerializer(ProjectModelSerializer): + manager_team = TeamBaseSerializer( many = False, read_only = True ) + + manager_user = UserBaseSerializer( many = False, read_only = True ) + organization = OrganizationBaseSerializer( many = False, read_only = True ) state = ProjectStateBaseSerializer( many = False, read_only = True ) + team_members = UserBaseSerializer( many = True, read_only = True ) + project_type = ProjectTypeBaseSerializer( many = False, read_only = True ) From b1b127b9f44c758643b388e45329711c7d2c68d7 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Oct 2024 13:25:53 +0930 Subject: [PATCH 305/617] test(project_management): Project API field checks ref: #15 #248 #357 --- .../tests/unit/project/test_project_api_v2.py | 695 ++++++++++++++++++ 1 file changed, 695 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_api_v2.py diff --git a/app/project_management/tests/unit/project/test_project_api_v2.py b/app/project_management/tests/unit/project/test_project_api_v2.py new file mode 100644 index 000000000..d9e3ee9af --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_api_v2.py @@ -0,0 +1,695 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +# from itam.models.device import Device + +# from itim.models.clusters import Cluster, ClusterType + +from project_management.models.projects import Project, ProjectState, ProjectType + +from settings.models.user_settings import UserSettings + + + +class ProjectAPI( + TestCase, + APITenancyObject +): + + model = Project + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + project_type = ProjectType.objects.create( + organization = self.organization, + name = 'proj type' + ) + + project_state = ProjectState.objects.create( + organization = self.organization, + name = 'a state' + ) + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + user_settings = UserSettings.objects.get(user = self.view_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + state = project_state, + project_type = project_type, + description = 'a note', + manager_user = self.view_user, + manager_team = view_team, + planned_start_date = '2024-01-01 00:01:00', + planned_finish_date = '2024-01-01 00:01:01', + real_start_date = '2024-01-02 00:01:00', + real_finish_date = '2024-01-02 00:01:01', + code = 'acode', + external_ref = 1, + external_system = Project.Ticket_ExternalSystem.CUSTOM_1 + ) + + + self.item.team_members.set([ self.view_user ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_project-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This test is a custom test of a test case with the same name. + this model does not have a model_notes_field + + model_notes field must exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This test is a custom test of a test case with the same name. + this model does not have a model_notes_field + + model_notes field must be str + """ + + assert True + + + + def test_api_field_exists_description(self): + """ Test for existance of API Field + + model_notes field must exist + """ + + assert 'description' in self.api_data + + + def test_api_field_type_description(self): + """ Test for type for API Field + + description field must be str + """ + + assert type(self.api_data['description']) is str + + + + def test_api_field_exists_priority(self): + """ Test for existance of API Field + + priority field must exist + """ + + assert 'priority' in self.api_data + + + def test_api_field_type_priority(self): + """ Test for type for API Field + + priority field must be int + """ + + assert type(self.api_data['priority']) is int + + + + def test_api_field_exists_code(self): + """ Test for existance of API Field + + code field must exist + """ + + assert 'code' in self.api_data + + + def test_api_field_type_code(self): + """ Test for type for API Field + + code field must be str + """ + + assert type(self.api_data['code']) is str + + + + def test_api_field_exists_planned_start_date(self): + """ Test for existance of API Field + + planned_start_date field must exist + """ + + assert 'planned_start_date' in self.api_data + + + def test_api_field_type_planned_start_date(self): + """ Test for type for API Field + + planned_start_date field must be str + """ + + assert type(self.api_data['planned_start_date']) is str + + + + def test_api_field_exists_planned_finish_date(self): + """ Test for existance of API Field + + planned_finish_date field must exist + """ + + assert 'planned_finish_date' in self.api_data + + + def test_api_field_type_planned_finish_date(self): + """ Test for type for API Field + + planned_finish_date field must be str + """ + + assert type(self.api_data['planned_finish_date']) is str + + + + def test_api_field_exists_real_start_date(self): + """ Test for existance of API Field + + real_start_date field must exist + """ + + assert 'real_start_date' in self.api_data + + + def test_api_field_type_real_start_date(self): + """ Test for type for API Field + + real_start_date field must be str + """ + + assert type(self.api_data['real_start_date']) is str + + + + def test_api_field_exists_real_finish_date(self): + """ Test for existance of API Field + + real_finish_date field must exist + """ + + assert 'real_finish_date' in self.api_data + + + def test_api_field_type_real_finish_date(self): + """ Test for type for API Field + + real_finish_date field must be str + """ + + assert type(self.api_data['real_finish_date']) is str + + + + def test_api_field_exists_is_deleted(self): + """ Test for existance of API Field + + is_deleted field must exist + """ + + assert 'is_deleted' in self.api_data + + + def test_api_field_type_is_deleted(self): + """ Test for type for API Field + + is_deleted field must be bool + """ + + assert type(self.api_data['is_deleted']) is bool + + + + def test_api_field_exists_external_ref(self): + """ Test for existance of API Field + + external_ref field must exist + """ + + assert 'external_ref' in self.api_data + + + def test_api_field_type_external_ref(self): + """ Test for type for API Field + + external_ref field must be int + """ + + assert type(self.api_data['external_ref']) is int + + + + def test_api_field_exists_external_system(self): + """ Test for existance of API Field + + external_system field must exist + """ + + assert 'external_system' in self.api_data + + + def test_api_field_type_external_system(self): + """ Test for type for API Field + + external_system field must be int + """ + + assert type(self.api_data['external_system']) is int + + + + def test_api_field_exists_project_type(self): + """ Test for existance of API Field + + project_type field must exist + """ + + assert 'project_type' in self.api_data + + + def test_api_field_type_project_type(self): + """ Test for type for API Field + + project_type field must be dict + """ + + assert type(self.api_data['project_type']) is dict + + + def test_api_field_exists_project_type_id(self): + """ Test for existance of API Field + + project_type.id field must exist + """ + + assert 'id' in self.api_data['project_type'] + + + def test_api_field_type_project_type_id(self): + """ Test for type for API Field + + project_type.id field must be int + """ + + assert type(self.api_data['project_type']['id']) is int + + + def test_api_field_exists_project_type_display_name(self): + """ Test for existance of API Field + + project_type.display_name field must exist + """ + + assert 'display_name' in self.api_data['project_type'] + + + def test_api_field_type_project_type_display_name(self): + """ Test for type for API Field + + project_type.display_name field must be str + """ + + assert type(self.api_data['project_type']['display_name']) is str + + + def test_api_field_exists_project_type_url(self): + """ Test for existance of API Field + + project_type.url field must exist + """ + + assert 'url' in self.api_data['project_type'] + + + def test_api_field_type_project_type_url(self): + """ Test for type for API Field + + project_type.url field must be Hyperlink + """ + + assert type(self.api_data['project_type']['url']) is Hyperlink + + + + def test_api_field_exists_state(self): + """ Test for existance of API Field + + state field must exist + """ + + assert 'state' in self.api_data + + + def test_api_field_type_state(self): + """ Test for type for API Field + + state field must be dict + """ + + assert type(self.api_data['state']) is dict + + + def test_api_field_exists_state_id(self): + """ Test for existance of API Field + + state.id field must exist + """ + + assert 'id' in self.api_data['state'] + + + def test_api_field_type_state_id(self): + """ Test for type for API Field + + state.id field must be int + """ + + assert type(self.api_data['state']['id']) is int + + + def test_api_field_exists_state_display_name(self): + """ Test for existance of API Field + + state.display_name field must exist + """ + + assert 'display_name' in self.api_data['state'] + + + def test_api_field_type_state_display_name(self): + """ Test for type for API Field + + state.display_name field must be str + """ + + assert type(self.api_data['state']['display_name']) is str + + + def test_api_field_exists_state_url(self): + """ Test for existance of API Field + + state.url field must exist + """ + + assert 'url' in self.api_data['state'] + + + def test_api_field_type_state_url(self): + """ Test for type for API Field + + state.url field must be Hyperlink + """ + + assert type(self.api_data['state']['url']) is Hyperlink + + + + def test_api_field_exists_manager_user(self): + """ Test for existance of API Field + + manager_user field must exist + """ + + assert 'manager_user' in self.api_data + + + def test_api_field_type_manager_user(self): + """ Test for type for API Field + + manager_user field must be dict + """ + + assert type(self.api_data['manager_user']) is dict + + + def test_api_field_exists_manager_user_id(self): + """ Test for existance of API Field + + manager_user.id field must exist + """ + + assert 'id' in self.api_data['manager_user'] + + + def test_api_field_type_manager_user_id(self): + """ Test for type for API Field + + manager_user.id field must be int + """ + + assert type(self.api_data['manager_user']['id']) is int + + + def test_api_field_exists_manager_user_display_name(self): + """ Test for existance of API Field + + manager_user.display_name field must exist + """ + + assert 'display_name' in self.api_data['manager_user'] + + + def test_api_field_type_manager_user_display_name(self): + """ Test for type for API Field + + manager_user.display_name field must be str + """ + + assert type(self.api_data['manager_user']['display_name']) is str + + + def test_api_field_exists_manager_user_url(self): + """ Test for existance of API Field + + manager_user.url field must exist + """ + + assert 'url' in self.api_data['manager_user'] + + + def test_api_field_type_manager_user_url(self): + """ Test for type for API Field + + manager_user.url field must be Hyperlink + """ + + assert type(self.api_data['manager_user']['url']) is Hyperlink + + + + def test_api_field_exists_manager_team(self): + """ Test for existance of API Field + + manager_team field must exist + """ + + assert 'manager_team' in self.api_data + + + def test_api_field_type_manager_team(self): + """ Test for type for API Field + + manager_team field must be dict + """ + + assert type(self.api_data['manager_team']) is dict + + + def test_api_field_exists_manager_team_id(self): + """ Test for existance of API Field + + manager_team.id field must exist + """ + + assert 'id' in self.api_data['manager_team'] + + + def test_api_field_type_manager_team_id(self): + """ Test for type for API Field + + manager_team.id field must be int + """ + + assert type(self.api_data['manager_team']['id']) is int + + + def test_api_field_exists_manager_team_display_name(self): + """ Test for existance of API Field + + manager_team.display_name field must exist + """ + + assert 'display_name' in self.api_data['manager_team'] + + + def test_api_field_type_manager_team_display_name(self): + """ Test for type for API Field + + manager_team.display_name field must be str + """ + + assert type(self.api_data['manager_team']['display_name']) is str + + + def test_api_field_exists_manager_team_url(self): + """ Test for existance of API Field + + manager_team.url field must exist + """ + + assert 'url' in self.api_data['manager_team'] + + + def test_api_field_type_manager_team_url(self): + """ Test for type for API Field + + manager_team.url field must be str + """ + + assert type(self.api_data['manager_team']['url']) is str + + + + def test_api_field_exists_team_members(self): + """ Test for existance of API Field + + team_members field must exist + """ + + assert 'team_members' in self.api_data + + + def test_api_field_type_team_members(self): + """ Test for type for API Field + + team_members field must be dict + """ + + assert type(self.api_data['team_members']) is list + + + def test_api_field_exists_team_members_id(self): + """ Test for existance of API Field + + team_members.id field must exist + """ + + assert 'id' in self.api_data['team_members'][0] + + + def test_api_field_type_team_members_id(self): + """ Test for type for API Field + + team_members.id field must be int + """ + + assert type(self.api_data['team_members'][0]['id']) is int + + + def test_api_field_exists_team_members_display_name(self): + """ Test for existance of API Field + + team_members.display_name field must exist + """ + + assert 'display_name' in self.api_data['team_members'][0] + + + def test_api_field_type_team_members_display_name(self): + """ Test for type for API Field + + team_members.display_name field must be str + """ + + assert type(self.api_data['team_members'][0]['display_name']) is str + + + def test_api_field_exists_team_members_url(self): + """ Test for existance of API Field + + team_members.url field must exist + """ + + assert 'url' in self.api_data['team_members'][0] + + + def test_api_field_type_team_members_url(self): + """ Test for type for API Field + + team_members.url field must be Hyperlink + """ + + assert type(self.api_data['team_members'][0]['url']) is Hyperlink From 036dbdeba385bf22778b56c15bdb647c30845cb5 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 14:09:41 +0930 Subject: [PATCH 306/617] fix(project_management): if user not hav org specified dont attempt to access ref: #358 --- app/api/views/project_management/projects.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/api/views/project_management/projects.py b/app/api/views/project_management/projects.py index 39bf86c9e..54d697aef 100644 --- a/app/api/views/project_management/projects.py +++ b/app/api/views/project_management/projects.py @@ -39,12 +39,18 @@ class View(OrganizationMixin, viewsets.ModelViewSet): def get_serializer_class(self): - if self.has_organization_permission( - organization = UserSettings.objects.get(user = self.request.user).default_organization.id, - permissions_required = ['project_management.import_project'] - ) or self.request.user.is_superuser: + user_default_organization = UserSettings.objects.get(user = self.request.user).default_organization - return ProjectImportSerializer + if user_default_organization: + + if hasattr(user_default_organization, 'default_organization'): + + if self.has_organization_permission( + organization = user_default_organization.default_organization.id, + permissions_required = ['project_management.import_project'] + ) or self.request.user.is_superuser: + + return ProjectImportSerializer return ProjectSerializer From 9327c6b3773b2f37523fa6030aaf1f1a1033fa2f Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 14:42:28 +0930 Subject: [PATCH 307/617] test(project_management): Project Serializer Validation checks ref: #15 #248 #357 --- .../unit/project/test_project_serializer.py | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 app/project_management/tests/unit/project/test_project_serializer.py diff --git a/app/project_management/tests/unit/project/test_project_serializer.py b/app/project_management/tests/unit/project/test_project_serializer.py new file mode 100644 index 000000000..83f5a58db --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_serializer.py @@ -0,0 +1,134 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from project_management.serializers.project import ( + Project, + ProjectModelSerializer +) + + + +class ProjectValidationAPI( + TestCase, +): + + model = Project + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + + + def test_serializer_validation_can_create(self): + """Serializer Validation Check + + Ensure that a valid item can be creates + """ + + serializer = ProjectModelSerializer(data={ + "organization": self.organization.id, + "name": 'a project' + }) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ProjectModelSerializer( + data={ + "organization": self.organization.id, + }, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_external_ref_not_import_user(self): + """Serializer Validation Check + + Ensure that if creating by user who is not import user, they can't edit + fields external_ref and external_system. + """ + + class MockView: + + is_import_user = False + + + serializer = ProjectModelSerializer( + context = { + 'view': MockView + }, + data={ + "name": 'a project name', + "organization": self.organization.id, + 'external_ref': 1, + 'external_system': int(Project.Ticket_ExternalSystem.CUSTOM_1) + }, + ) + + serializer.is_valid(raise_exception = True) + + serializer.save() + + assert serializer.instance.external_ref is None and serializer.instance.external_system is None + + + + def test_serializer_validation_external_ref_is_import_user(self): + """Serializer Validation Check + + Ensure that if creating by user who import user, they can edit + fields external_ref and external_system. + """ + + class MockView: + + is_import_user = True + + + serializer = ProjectModelSerializer( + context = { + 'view': MockView + }, + data={ + "name": 'a project name', + "organization": self.organization.id, + 'external_ref': 1, + 'external_system': int(Project.Ticket_ExternalSystem.CUSTOM_1) + }, + ) + + serializer.is_valid(raise_exception = True) + + serializer.save() + + assert ( + serializer.instance.external_ref == 1 and + serializer.instance.external_system == int(Project.Ticket_ExternalSystem.CUSTOM_1) + ) From ab41c961826c9f7597bb16ea60883d7b5bfc9b42 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 14:55:51 +0930 Subject: [PATCH 308/617] test(project_management): Project API v2 ViewSet permission checks ref: #15 #248 #357 --- .../unit/project/test_project_viewset.py | 173 ++++++++++++++++++ app/project_management/viewsets/project.py | 18 +- 2 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 app/project_management/tests/unit/project/test_project_viewset.py diff --git a/app/project_management/tests/unit/project/test_project_viewset.py b/app/project_management/tests/unit/project/test_project_viewset.py new file mode 100644 index 000000000..082f7e91b --- /dev/null +++ b/app/project_management/tests/unit/project/test_project_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from project_management.models.projects import Project + + + +class ProjectPermissionsAPI(TestCase, APIPermissions): + + model = Project + + app_namespace = 'API' + + url_name = '_api_v2_project' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py index 5348bbd3d..233ba3acf 100644 --- a/app/project_management/viewsets/project.py +++ b/app/project_management/viewsets/project.py @@ -82,12 +82,20 @@ class ViewSet( ModelViewSet ): def get_serializer_class(self): - if self.has_organization_permission( - organization = UserSettings.objects.get(user = self.request.user).default_organization.id, - permissions_required = ['project_management.import_project'] - ) or self.request.user.is_superuser: - self.is_import_user = True + user_default_organization = UserSettings.objects.get(user = self.request.user).default_organization + + if user_default_organization: + + if hasattr(user_default_organization, 'default_organization'): + + + if self.has_organization_permission( + organization = user_default_organization.default_organization.id, + permissions_required = ['project_management.import_project'] + ) or self.request.user.is_superuser: + + self.is_import_user = True if ( self.action == 'list' From 89588211fda3d87c8683977a131bba655b8cb739 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 15:38:55 +0930 Subject: [PATCH 309/617] test(project_management): Project Milestone API field checks ref: #15 #248 #357 --- .../test_project_milestone_api_v2.py | 548 ++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py new file mode 100644 index 000000000..c6fd6639f --- /dev/null +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py @@ -0,0 +1,548 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from project_management.models.project_milestone import Project, ProjectMilestone + +from settings.models.user_settings import UserSettings + + + +class ProjectMilestoneAPI( + TestCase, + APITenancyObject +): + + model = ProjectMilestone + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + project = Project.objects.create( + organization = self.organization, + name = 'a state' + ) + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + user_settings = UserSettings.objects.get(user = self.view_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + description = 'notes', + project = project, + start_date = '2024-01-01 00:01:00', + finish_date = '2024-01-01 00:01:01', + ) + + + self.url_view_kwargs = {'project_id': project.id, 'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_project_milestone-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This test is a custom test of a test case with the same name. + this model does not have a model_notes_field + + model_notes field must exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This test is a custom test of a test case with the same name. + this model does not have a model_notes_field + + model_notes field must be str + """ + + assert True + + + + def test_api_field_exists_description(self): + """ Test for existance of API Field + + model_notes field must exist + """ + + assert 'description' in self.api_data + + + def test_api_field_type_description(self): + """ Test for type for API Field + + description field must be str + """ + + assert type(self.api_data['description']) is str + + + + def test_api_field_exists_start_date(self): + """ Test for existance of API Field + + start_date field must exist + """ + + assert 'start_date' in self.api_data + + + def test_api_field_type_start_date(self): + """ Test for type for API Field + + start_date field must be str + """ + + assert type(self.api_data['start_date']) is str + + + + def test_api_field_exists_finish_date(self): + """ Test for existance of API Field + + finish_date field must exist + """ + + assert 'finish_date' in self.api_data + + + def test_api_field_type_finish_date(self): + """ Test for type for API Field + + finish_date field must be str + """ + + assert type(self.api_data['finish_date']) is str + + + + + + def test_api_field_exists_project(self): + """ Test for existance of API Field + + project field must exist + """ + + assert 'project' in self.api_data + + + def test_api_field_type_project(self): + """ Test for type for API Field + + project field must be dict + """ + + assert type(self.api_data['project']) is dict + + + def test_api_field_exists_project_id(self): + """ Test for existance of API Field + + project.id field must exist + """ + + assert 'id' in self.api_data['project'] + + + def test_api_field_type_project_id(self): + """ Test for type for API Field + + project.id field must be int + """ + + assert type(self.api_data['project']['id']) is int + + + def test_api_field_exists_project_display_name(self): + """ Test for existance of API Field + + project.display_name field must exist + """ + + assert 'display_name' in self.api_data['project'] + + + def test_api_field_type_project_display_name(self): + """ Test for type for API Field + + project.display_name field must be str + """ + + assert type(self.api_data['project']['display_name']) is str + + + def test_api_field_exists_project_url(self): + """ Test for existance of API Field + + project.url field must exist + """ + + assert 'url' in self.api_data['project'] + + + def test_api_field_type_project_url(self): + """ Test for type for API Field + + project.url field must be Hyperlink + """ + + assert type(self.api_data['project']['url']) is Hyperlink + + + + + + + + + # def test_api_field_exists_state(self): + # """ Test for existance of API Field + + # state field must exist + # """ + + # assert 'state' in self.api_data + + + # def test_api_field_type_state(self): + # """ Test for type for API Field + + # state field must be dict + # """ + + # assert type(self.api_data['state']) is dict + + + # def test_api_field_exists_state_id(self): + # """ Test for existance of API Field + + # state.id field must exist + # """ + + # assert 'id' in self.api_data['state'] + + + # def test_api_field_type_state_id(self): + # """ Test for type for API Field + + # state.id field must be int + # """ + + # assert type(self.api_data['state']['id']) is int + + + # def test_api_field_exists_state_display_name(self): + # """ Test for existance of API Field + + # state.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['state'] + + + # def test_api_field_type_state_display_name(self): + # """ Test for type for API Field + + # state.display_name field must be str + # """ + + # assert type(self.api_data['state']['display_name']) is str + + + # def test_api_field_exists_state_url(self): + # """ Test for existance of API Field + + # state.url field must exist + # """ + + # assert 'url' in self.api_data['state'] + + + # def test_api_field_type_state_url(self): + # """ Test for type for API Field + + # state.url field must be Hyperlink + # """ + + # assert type(self.api_data['state']['url']) is Hyperlink + + + + # def test_api_field_exists_manager_user(self): + # """ Test for existance of API Field + + # manager_user field must exist + # """ + + # assert 'manager_user' in self.api_data + + + # def test_api_field_type_manager_user(self): + # """ Test for type for API Field + + # manager_user field must be dict + # """ + + # assert type(self.api_data['manager_user']) is dict + + + # def test_api_field_exists_manager_user_id(self): + # """ Test for existance of API Field + + # manager_user.id field must exist + # """ + + # assert 'id' in self.api_data['manager_user'] + + + # def test_api_field_type_manager_user_id(self): + # """ Test for type for API Field + + # manager_user.id field must be int + # """ + + # assert type(self.api_data['manager_user']['id']) is int + + + # def test_api_field_exists_manager_user_display_name(self): + # """ Test for existance of API Field + + # manager_user.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['manager_user'] + + + # def test_api_field_type_manager_user_display_name(self): + # """ Test for type for API Field + + # manager_user.display_name field must be str + # """ + + # assert type(self.api_data['manager_user']['display_name']) is str + + + # def test_api_field_exists_manager_user_url(self): + # """ Test for existance of API Field + + # manager_user.url field must exist + # """ + + # assert 'url' in self.api_data['manager_user'] + + + # def test_api_field_type_manager_user_url(self): + # """ Test for type for API Field + + # manager_user.url field must be Hyperlink + # """ + + # assert type(self.api_data['manager_user']['url']) is Hyperlink + + + + # def test_api_field_exists_manager_team(self): + # """ Test for existance of API Field + + # manager_team field must exist + # """ + + # assert 'manager_team' in self.api_data + + + # def test_api_field_type_manager_team(self): + # """ Test for type for API Field + + # manager_team field must be dict + # """ + + # assert type(self.api_data['manager_team']) is dict + + + # def test_api_field_exists_manager_team_id(self): + # """ Test for existance of API Field + + # manager_team.id field must exist + # """ + + # assert 'id' in self.api_data['manager_team'] + + + # def test_api_field_type_manager_team_id(self): + # """ Test for type for API Field + + # manager_team.id field must be int + # """ + + # assert type(self.api_data['manager_team']['id']) is int + + + # def test_api_field_exists_manager_team_display_name(self): + # """ Test for existance of API Field + + # manager_team.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['manager_team'] + + + # def test_api_field_type_manager_team_display_name(self): + # """ Test for type for API Field + + # manager_team.display_name field must be str + # """ + + # assert type(self.api_data['manager_team']['display_name']) is str + + + # def test_api_field_exists_manager_team_url(self): + # """ Test for existance of API Field + + # manager_team.url field must exist + # """ + + # assert 'url' in self.api_data['manager_team'] + + + # def test_api_field_type_manager_team_url(self): + # """ Test for type for API Field + + # manager_team.url field must be str + # """ + + # assert type(self.api_data['manager_team']['url']) is str + + + + # def test_api_field_exists_team_members(self): + # """ Test for existance of API Field + + # team_members field must exist + # """ + + # assert 'team_members' in self.api_data + + + # def test_api_field_type_team_members(self): + # """ Test for type for API Field + + # team_members field must be dict + # """ + + # assert type(self.api_data['team_members']) is list + + + # def test_api_field_exists_team_members_id(self): + # """ Test for existance of API Field + + # team_members.id field must exist + # """ + + # assert 'id' in self.api_data['team_members'][0] + + + # def test_api_field_type_team_members_id(self): + # """ Test for type for API Field + + # team_members.id field must be int + # """ + + # assert type(self.api_data['team_members'][0]['id']) is int + + + # def test_api_field_exists_team_members_display_name(self): + # """ Test for existance of API Field + + # team_members.display_name field must exist + # """ + + # assert 'display_name' in self.api_data['team_members'][0] + + + # def test_api_field_type_team_members_display_name(self): + # """ Test for type for API Field + + # team_members.display_name field must be str + # """ + + # assert type(self.api_data['team_members'][0]['display_name']) is str + + + # def test_api_field_exists_team_members_url(self): + # """ Test for existance of API Field + + # team_members.url field must exist + # """ + + # assert 'url' in self.api_data['team_members'][0] + + + # def test_api_field_type_team_members_url(self): + # """ Test for type for API Field + + # team_members.url field must be Hyperlink + # """ + + # assert type(self.api_data['team_members'][0]['url']) is Hyperlink From c89a8e8007e12f126584e3b4dfcba53338c8d9f9 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 15:54:17 +0930 Subject: [PATCH 310/617] test(project_management): add trace output to Project serializer tests are passing locally and not on GH actions ref: #15 #248 #357 --- .../tests/unit/project/test_project_serializer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/project_management/tests/unit/project/test_project_serializer.py b/app/project_management/tests/unit/project/test_project_serializer.py index 83f5a58db..8416629c5 100644 --- a/app/project_management/tests/unit/project/test_project_serializer.py +++ b/app/project_management/tests/unit/project/test_project_serializer.py @@ -85,7 +85,7 @@ class MockView: 'view': MockView }, data={ - "name": 'a project name', + "name": 'project name', "organization": self.organization.id, 'external_ref': 1, 'external_system': int(Project.Ticket_ExternalSystem.CUSTOM_1) @@ -117,7 +117,7 @@ class MockView: 'view': MockView }, data={ - "name": 'a project name', + "name": 'another project', "organization": self.organization.id, 'external_ref': 1, 'external_system': int(Project.Ticket_ExternalSystem.CUSTOM_1) @@ -128,6 +128,12 @@ class MockView: serializer.save() + print(f'[Debug] instance {serializer.instance.__dict__}') + + for project in Project.objects.all(): + + print(f'[Trace] project found: {project.__dict__}') + assert ( serializer.instance.external_ref == 1 and serializer.instance.external_system == int(Project.Ticket_ExternalSystem.CUSTOM_1) From dff794c433fc803cc08c1c43a3f36e8c36d1ed4c Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 16:19:08 +0930 Subject: [PATCH 311/617] test(project_management): Project milestone Serializer Validation checks ref: #15 #248 #357 --- .../test_project_milestone_serializer.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 app/project_management/tests/unit/project_milestone/test_project_milestone_serializer.py diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_serializer.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_serializer.py new file mode 100644 index 000000000..2558dbb3e --- /dev/null +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_serializer.py @@ -0,0 +1,126 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from project_management.serializers.project_milestone import ( + Project, + ProjectMilestone, + ProjectMilestoneModelSerializer +) + + + +class ProjectMilestoneValidationAPI( + TestCase, +): + + model = ProjectMilestone + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.project = Project.objects.create( + name = 'proj mile', + organization = self.organization + ) + + + + def test_serializer_validation_can_create(self): + """Serializer Validation Check + + Ensure that a valid item can be creates + """ + + # self._kwargs['context']['view'].kwargs['project_id']) + + class MockView: + + kwargs = { + 'project_id': self.project.id + } + + serializer = ProjectMilestoneModelSerializer( + context = { + 'view': MockView + }, + data={ + "organization": self.organization.id, + "name": 'a milestone', + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + class MockView: + + kwargs = { + 'project_id': self.project.id + } + + with pytest.raises(ValidationError) as err: + + serializer = ProjectMilestoneModelSerializer( + context = { + 'view': MockView + }, + data={ + "organization": self.organization.id, + }, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + # def test_serializer_validation_no_project(self): + # """Serializer Validation Check + + # Ensure that if creating and no name is provided a validation error occurs + # """ + + # class MockView: + + # kwargs = { + # 'project_id': self.project.id + # } + + # with pytest.raises(ValidationError) as err: + + # serializer = ProjectMilestoneModelSerializer( + # context = { + # 'view': MockView + # }, + # data={ + # "organization": self.organization.id, + # "name": 'a milestone', + # }, + # ) + + # serializer.is_valid(raise_exception = True) + + # assert err.value.get_codes()['project'][0] == 'required' + From 8cc3adf3c230e34b8679dfae39b7bb68f544b586 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 16:26:03 +0930 Subject: [PATCH 312/617] test(project_management): Project Milestone API v2 ViewSet permission checks ref: #15 #248 #357 --- .../test_project_milestone_viewset.py | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py new file mode 100644 index 000000000..d2f60755d --- /dev/null +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py @@ -0,0 +1,181 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from project_management.models.project_milestone import Project, ProjectMilestone + + + +class ProjectMilestonePermissionsAPI(TestCase, APIPermissions): + + model = ProjectMilestone + + app_namespace = 'API' + + url_name = '_api_v2_project_milestone' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + project = Project.objects.create( + organization = self.organization, + name = 'proj milestone test' + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add', + project = project + ) + + + self.url_view_kwargs = {'project_id': project.id, 'pk': self.item.id} + + self.url_kwargs = {'project_id': project.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 7263b3a8a3ecce6143ad6cea35a2012c0b9d22f8 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 18:08:21 +0930 Subject: [PATCH 313/617] test(project_management): Project state API field checks ref: #15 #248 #357 --- .../test_project_state_api_v2.py | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 app/project_management/tests/unit/project_state/test_project_state_api_v2.py diff --git a/app/project_management/tests/unit/project_state/test_project_state_api_v2.py b/app/project_management/tests/unit/project_state/test_project_state_api_v2.py new file mode 100644 index 000000000..9fe2a2be2 --- /dev/null +++ b/app/project_management/tests/unit/project_state/test_project_state_api_v2.py @@ -0,0 +1,207 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from assistance.models.knowledge_base import KnowledgeBase + +from project_management.models.projects import ProjectState + +from settings.models.user_settings import UserSettings + + + +class ProjectStateAPI( + TestCase, + APITenancyObject +): + + model = ProjectState + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + kb = KnowledgeBase.objects.create( + organization = self.organization, + title = 'kb article' + ) + + self.item = ProjectState.objects.create( + organization = self.organization, + name = 'a state', + model_notes = 'note', + runbook = kb, + ) + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + user_settings = UserSettings.objects.get(user = self.view_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + # self.item = self.model.objects.create( + # organization = self.organization, + # name = 'one', + # state = project_state, + # runbook = runbook, + # description = 'a note', + # manager_user = self.view_user, + # manager_team = view_team, + # planned_start_date = '2024-01-01 00:01:00', + # planned_finish_date = '2024-01-01 00:01:01', + # real_start_date = '2024-01-02 00:01:00', + # real_finish_date = '2024-01-02 00:01:01', + # code = 'acode', + # external_ref = 1, + # external_system = Project.Ticket_ExternalSystem.CUSTOM_1 + # ) + + + # self.item.team_members.set([ self.view_user ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_project_state-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_is_completed(self): + """ Test for existance of API Field + + is_completed field must exist + """ + + assert 'is_completed' in self.api_data + + + def test_api_field_type_is_completed(self): + """ Test for type for API Field + + is_completed field must be bool + """ + + assert type(self.api_data['is_completed']) is bool + + + + def test_api_field_exists_runbook(self): + """ Test for existance of API Field + + runbook field must exist + """ + + assert 'runbook' in self.api_data + + + def test_api_field_type_runbook(self): + """ Test for type for API Field + + runbook field must be dict + """ + + assert type(self.api_data['runbook']) is dict + + + def test_api_field_exists_runbook_id(self): + """ Test for existance of API Field + + runbook.id field must exist + """ + + assert 'id' in self.api_data['runbook'] + + + def test_api_field_type_runbook_id(self): + """ Test for type for API Field + + runbook.id field must be int + """ + + assert type(self.api_data['runbook']['id']) is int + + + def test_api_field_exists_runbook_display_name(self): + """ Test for existance of API Field + + runbook.display_name field must exist + """ + + assert 'display_name' in self.api_data['runbook'] + + + def test_api_field_type_runbook_display_name(self): + """ Test for type for API Field + + runbook.display_name field must be str + """ + + assert type(self.api_data['runbook']['display_name']) is str + + + def test_api_field_exists_runbook_url(self): + """ Test for existance of API Field + + runbook.url field must exist + """ + + assert 'url' in self.api_data['runbook'] + + + def test_api_field_type_runbook_url(self): + """ Test for type for API Field + + runbook.url field must be str + """ + + assert type(self.api_data['runbook']['url']) is str From 58216073d750a19b6a19de345d61d78f1e6b0abd Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 18:08:39 +0930 Subject: [PATCH 314/617] test(project_management): Project state Serializer Validation checks ref: #15 #248 #357 --- .../test_project_state_serializer.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 app/project_management/tests/unit/project_state/test_project_state_serializer.py diff --git a/app/project_management/tests/unit/project_state/test_project_state_serializer.py b/app/project_management/tests/unit/project_state/test_project_state_serializer.py new file mode 100644 index 000000000..aae0cefe7 --- /dev/null +++ b/app/project_management/tests/unit/project_state/test_project_state_serializer.py @@ -0,0 +1,69 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from project_management.serializers.project_states import ( + ProjectState, + ProjectStateModelSerializer +) + + + +class ProjectStateValidationAPI( + TestCase, +): + + model = ProjectState + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + + + def test_serializer_validation_can_create(self): + """Serializer Validation Check + + Ensure that a valid item can be creates + """ + + serializer = ProjectStateModelSerializer( + data={ + "organization": self.organization.id, + "name": 'a project' + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ProjectStateModelSerializer( + data={ + "organization": self.organization.id, + }, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 745983dfab4dfae07109fa175796d1d63a6a4b78 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 18:08:56 +0930 Subject: [PATCH 315/617] test(project_management): Project State API v2 ViewSet permission checks ref: #15 #248 #357 --- .../test_project_state_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/project_management/tests/unit/project_state/test_project_state_viewset.py diff --git a/app/project_management/tests/unit/project_state/test_project_state_viewset.py b/app/project_management/tests/unit/project_state/test_project_state_viewset.py new file mode 100644 index 000000000..029a70728 --- /dev/null +++ b/app/project_management/tests/unit/project_state/test_project_state_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from project_management.models.project_states import ProjectState + + + +class ProjectStatePermissionsAPI(TestCase, APIPermissions): + + model = ProjectState + + app_namespace = 'API' + + url_name = '_api_v2_project_state' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 91444172aaa68209fcc10f408f6e2acf8b5f5455 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 18:09:08 +0930 Subject: [PATCH 316/617] test(project_management): Project Type API field checks ref: #15 #248 #357 --- .../project_type/test_project_type_api_v2.py | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 app/project_management/tests/unit/project_type/test_project_type_api_v2.py diff --git a/app/project_management/tests/unit/project_type/test_project_type_api_v2.py b/app/project_management/tests/unit/project_type/test_project_type_api_v2.py new file mode 100644 index 000000000..3acff895a --- /dev/null +++ b/app/project_management/tests/unit/project_type/test_project_type_api_v2.py @@ -0,0 +1,168 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from assistance.models.knowledge_base import KnowledgeBase + +from project_management.models.project_types import ProjectType + +from settings.models.user_settings import UserSettings + + + +class ProjectTypeAPI( + TestCase, + APITenancyObject +): + + model = ProjectType + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + kb = KnowledgeBase.objects.create( + organization = self.organization, + title = 'kb article' + ) + + self.item = self.model.objects.create( + organization = self.organization, + name = 'a state', + model_notes = 'note', + runbook = kb, + ) + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + user_settings = UserSettings.objects.get(user = self.view_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_project_type-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_runbook(self): + """ Test for existance of API Field + + runbook field must exist + """ + + assert 'runbook' in self.api_data + + + def test_api_field_type_runbook(self): + """ Test for type for API Field + + runbook field must be dict + """ + + assert type(self.api_data['runbook']) is dict + + + + def test_api_field_exists_runbook_id(self): + """ Test for existance of API Field + + runbook.id field must exist + """ + + assert 'id' in self.api_data['runbook'] + + + def test_api_field_type_runbook_id(self): + """ Test for type for API Field + + runbook.id field must be int + """ + + assert type(self.api_data['runbook']['id']) is int + + + def test_api_field_exists_runbook_display_name(self): + """ Test for existance of API Field + + runbook.display_name field must exist + """ + + assert 'display_name' in self.api_data['runbook'] + + + def test_api_field_type_runbook_display_name(self): + """ Test for type for API Field + + runbook.display_name field must be str + """ + + assert type(self.api_data['runbook']['display_name']) is str + + + def test_api_field_exists_runbook_url(self): + """ Test for existance of API Field + + runbook.url field must exist + """ + + assert 'url' in self.api_data['runbook'] + + + def test_api_field_type_runbook_url(self): + """ Test for type for API Field + + runbook.url field must be str + """ + + assert type(self.api_data['runbook']['url']) is str From a09fb4c8cdcbed7aeb2bff65777f4d485645aa90 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 18:09:17 +0930 Subject: [PATCH 317/617] test(project_management): Project Type Serializer Validation checks ref: #15 #248 #357 --- .../test_project_type_serializer.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 app/project_management/tests/unit/project_type/test_project_type_serializer.py diff --git a/app/project_management/tests/unit/project_type/test_project_type_serializer.py b/app/project_management/tests/unit/project_type/test_project_type_serializer.py new file mode 100644 index 000000000..a8e539893 --- /dev/null +++ b/app/project_management/tests/unit/project_type/test_project_type_serializer.py @@ -0,0 +1,69 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from project_management.serializers.project_type import ( + ProjectType, + ProjectTypeModelSerializer +) + + + +class ProjectTypeValidationAPI( + TestCase, +): + + model = ProjectType + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + + + def test_serializer_validation_can_create(self): + """Serializer Validation Check + + Ensure that a valid item can be creates + """ + + serializer = ProjectTypeModelSerializer( + data={ + "organization": self.organization.id, + "name": 'a project' + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = ProjectTypeModelSerializer( + data={ + "organization": self.organization.id, + }, + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' From 82a06e57b17f806488bde079f991d96bc70ee72d Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 18:09:38 +0930 Subject: [PATCH 318/617] test(project_management): Project Type API v2 ViewSet permission checks ref: #15 #248 #357 --- .../project_type/test_project_type_viewset.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/project_management/tests/unit/project_type/test_project_type_viewset.py diff --git a/app/project_management/tests/unit/project_type/test_project_type_viewset.py b/app/project_management/tests/unit/project_type/test_project_type_viewset.py new file mode 100644 index 000000000..f41f7e21a --- /dev/null +++ b/app/project_management/tests/unit/project_type/test_project_type_viewset.py @@ -0,0 +1,173 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from project_management.models.project_types import ProjectType + + + +class ProjectTypePermissionsAPI(TestCase, APIPermissions): + + model = ProjectType + + app_namespace = 'API' + + url_name = '_api_v2_project_type' + + change_data = {'name': 'device-change'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one-add' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From fa3698aa2bc247447319b5eb27828b1aadfa7c54 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 19:58:55 +0930 Subject: [PATCH 319/617] fix(project_management): Dont use init to adjust read_only_fields for project ref: #248 #357 --- app/project_management/serializers/project.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 93ad8db1a..a08a84c92 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -109,10 +109,7 @@ class Meta: ] - - def __init__(self, instance=None, data=empty, **kwargs): - - super().__init__(instance=instance, data=data, **kwargs) + def get_field_names(self, declared_fields, info): if 'view' in self.context: @@ -123,6 +120,10 @@ def __init__(self, instance=None, data=empty, **kwargs): 'external_system', ] + fields = super().get_field_names(declared_fields, info) + + return fields + class ProjectViewSerializer(ProjectModelSerializer): From 5fd3123c9b1ceed08577fa2c029d265361dbd3fc Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 19:59:28 +0930 Subject: [PATCH 320/617] fix(project_management): use the post data or existing object for fetching edit organisation ref: #248 #357 --- app/project_management/viewsets/project.py | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py index 233ba3acf..8aef439c0 100644 --- a/app/project_management/viewsets/project.py +++ b/app/project_management/viewsets/project.py @@ -82,20 +82,30 @@ class ViewSet( ModelViewSet ): def get_serializer_class(self): + organization = None - user_default_organization = UserSettings.objects.get(user = self.request.user).default_organization + if 'organization' in self.request._full_data: - if user_default_organization: + organization = self.request._full_data['organization'] - if hasattr(user_default_organization, 'default_organization'): + elif self.queryset: + + if list(self.queryset) == 1: + obj = list(self.queryset)[0] - if self.has_organization_permission( - organization = user_default_organization.default_organization.id, - permissions_required = ['project_management.import_project'] - ) or self.request.user.is_superuser: + organization = obj.organization.id + + + if organization: + + if self.has_organization_permission( + organization = organization, + permissions_required = ['project_management.import_project'] + ) or self.request.user.is_superuser: + + self.is_import_user = True - self.is_import_user = True if ( self.action == 'list' From f53c6d0f6dc07f40e636bd3fdb46dd25a0c3a142 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 20:01:56 +0930 Subject: [PATCH 321/617] test(project_management): Project Serializer Validation clean up ref: #15 #248 #357 --- .../tests/unit/project/test_project_serializer.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/project_management/tests/unit/project/test_project_serializer.py b/app/project_management/tests/unit/project/test_project_serializer.py index 8416629c5..18a952842 100644 --- a/app/project_management/tests/unit/project/test_project_serializer.py +++ b/app/project_management/tests/unit/project/test_project_serializer.py @@ -119,8 +119,8 @@ class MockView: data={ "name": 'another project', "organization": self.organization.id, - 'external_ref': 1, - 'external_system': int(Project.Ticket_ExternalSystem.CUSTOM_1) + "external_ref": 1, + "external_system": int(Project.Ticket_ExternalSystem.CUSTOM_1) }, ) @@ -128,12 +128,6 @@ class MockView: serializer.save() - print(f'[Debug] instance {serializer.instance.__dict__}') - - for project in Project.objects.all(): - - print(f'[Trace] project found: {project.__dict__}') - assert ( serializer.instance.external_ref == 1 and serializer.instance.external_system == int(Project.Ticket_ExternalSystem.CUSTOM_1) From bad610be365f481b2f9dd732f481cbc85c894cfb Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 20:02:21 +0930 Subject: [PATCH 322/617] test(project_management): Project API v2 ViewSet permission checks for import user ref: #15 #248 #357 --- .../unit/project/test_project_viewset.py | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/app/project_management/tests/unit/project/test_project_viewset.py b/app/project_management/tests/unit/project/test_project_viewset.py index 082f7e91b..407560aa0 100644 --- a/app/project_management/tests/unit/project/test_project_viewset.py +++ b/app/project_management/tests/unit/project/test_project_viewset.py @@ -2,7 +2,8 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.test import TestCase +from django.shortcuts import reverse +from django.test import Client, TestCase from access.models import Organization, Team, TeamUsers, Permission @@ -109,6 +110,29 @@ def setUpTestData(self): delete_team.permissions.set([delete_permissions]) + import_permissions = Permission.objects.get( + codename = 'import_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + import_team = Team.objects.create( + team_name = 'import_team', + organization = organization, + ) + + import_team.permissions.set( [ import_permissions, add_permissions ] ) + + + self.import_user = User.objects.create_user(username="test_user_import", password="password") + teamuser = TeamUsers.objects.create( + team = import_team, + user = self.import_user + ) + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") @@ -133,6 +157,14 @@ def setUpTestData(self): } + self.add_data_import_fields = { + 'name': 'team-post', + 'organization': self.organization.id, + 'external_ref': 1, + 'external_system': int(Project.Ticket_ExternalSystem.CUSTOM_1) + } + + self.add_user = User.objects.create_user(username="test_user_add", password="password") teamuser = TeamUsers.objects.create( team = add_team, @@ -171,3 +203,57 @@ def setUpTestData(self): team = different_organization_team, user = self.different_organization_user ) + + + + def test_add_has_permission_no_import_fields(self): + """ Check correct permission for add + + Attempt to add as user with permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data=self.add_data_import_fields) + + assert ( + response.status_code == 201 + and response.data['external_ref'] is None + and response.data['external_system'] is None + ) + + + + def test_add_has_permission_import_fields(self): + """ Check correct permission for add + + Attempt to add as user with permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.import_user) + response = client.post(url, data=self.add_data_import_fields) + + assert ( + response.status_code == 201 + and response.data['external_ref'] == 1 + and response.data['external_system'] == int(Project.Ticket_ExternalSystem.CUSTOM_1) + ) From 6cb99609cd5ea51485ba4df81cb476d611700cf7 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 20:24:09 +0930 Subject: [PATCH 323/617] fix(project_management): use the post data dict for fetching edit organisation ref: #248 #357 --- app/project_management/viewsets/project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py index 8aef439c0..7a1f6428b 100644 --- a/app/project_management/viewsets/project.py +++ b/app/project_management/viewsets/project.py @@ -84,9 +84,9 @@ def get_serializer_class(self): organization = None - if 'organization' in self.request._full_data: + if 'organization' in self.request.data: - organization = self.request._full_data['organization'] + organization = self.request.data['organization'] elif self.queryset: From 7322667a99293ded5028ad54c7620c017e453d9a Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 20:50:23 +0930 Subject: [PATCH 324/617] fix(project_management): For Project use a separate Import Serializer ref: #248 #357 --- app/project_management/serializers/project.py | 20 ++++++++++--------- .../unit/project/test_project_serializer.py | 3 ++- app/project_management/viewsets/project.py | 5 ++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index a08a84c92..e4f459102 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -103,26 +103,28 @@ class Meta: read_only_fields = [ 'id', 'display_name', + 'external_ref', + 'external_system', 'created', 'modified', '_urls', ] - def get_field_names(self, declared_fields, info): - if 'view' in self.context: +class ProjectImportSerializer(ProjectModelSerializer): - if not self.context['view'].is_import_user: - self.Meta.read_only_fields += [ - 'external_ref', - 'external_system', - ] + class Meta(ProjectModelSerializer.Meta): - fields = super().get_field_names(declared_fields, info) - return fields + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] diff --git a/app/project_management/tests/unit/project/test_project_serializer.py b/app/project_management/tests/unit/project/test_project_serializer.py index 18a952842..6e44aef57 100644 --- a/app/project_management/tests/unit/project/test_project_serializer.py +++ b/app/project_management/tests/unit/project/test_project_serializer.py @@ -8,6 +8,7 @@ from project_management.serializers.project import ( Project, + ProjectImportSerializer, ProjectModelSerializer ) @@ -112,7 +113,7 @@ class MockView: is_import_user = True - serializer = ProjectModelSerializer( + serializer = ProjectImportSerializer( context = { 'view': MockView }, diff --git a/app/project_management/viewsets/project.py b/app/project_management/viewsets/project.py index 7a1f6428b..769e3cc13 100644 --- a/app/project_management/viewsets/project.py +++ b/app/project_management/viewsets/project.py @@ -4,6 +4,7 @@ from project_management.serializers.project import ( Project, + ProjectImportSerializer, ProjectModelSerializer, ProjectViewSerializer ) @@ -66,8 +67,6 @@ class ViewSet( ModelViewSet ): 'state', ] - is_import_user: bool = False - search_fields = [ 'name', 'description', @@ -104,7 +103,7 @@ def get_serializer_class(self): permissions_required = ['project_management.import_project'] ) or self.request.user.is_superuser: - self.is_import_user = True + return globals()[str( self.model._meta.verbose_name) + 'ImportSerializer'] if ( From 4336d90dc0f517ef7f76a1560fefc7b709c3f5e6 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 23 Oct 2024 21:23:10 +0930 Subject: [PATCH 325/617] chore(settings): remove extra fields declaration from external_links ref: #248 #360 --- app/settings/serializers/external_links.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/settings/serializers/external_links.py b/app/settings/serializers/external_links.py index 97d472bcf..32027c4a6 100644 --- a/app/settings/serializers/external_links.py +++ b/app/settings/serializers/external_links.py @@ -64,8 +64,6 @@ class Meta: model = ExternalLink - fields = '__all__' - fields = [ 'id', 'organization', From fb0905c44a5448731ac3bb5be76942f11026650c Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 11:44:44 +0930 Subject: [PATCH 326/617] fix(settings): Populate app_settings Meta ref: #248 #360 --- .../0007_alter_appsettings_options.py | 17 +++++++++++++++++ app/settings/models/app_settings.py | 15 +++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/settings/migrations/0007_alter_appsettings_options.py diff --git a/app/settings/migrations/0007_alter_appsettings_options.py b/app/settings/migrations/0007_alter_appsettings_options.py new file mode 100644 index 000000000..d2ddf1638 --- /dev/null +++ b/app/settings/migrations/0007_alter_appsettings_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-24 02:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0006_alter_appsettings_device_model_is_global_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='appsettings', + options={'ordering': ['owner_organization'], 'verbose_name': 'App Settings', 'verbose_name_plural': 'App Settings'}, + ), + ] diff --git a/app/settings/models/app_settings.py b/app/settings/models/app_settings.py index 50fd05c73..f486a2847 100644 --- a/app/settings/models/app_settings.py +++ b/app/settings/models/app_settings.py @@ -40,6 +40,17 @@ class AppSettings(AppSettingsCommonFields, SaveHistory): ValidationError: When software set as global and no organization has been specified """ + class Meta: + + ordering = [ + 'owner_organization' + ] + + verbose_name = 'App Settings' + + verbose_name_plural = 'App Settings' + + owner_organization = models.ForeignKey( Organization, blank= True, @@ -96,6 +107,10 @@ class AppSettings(AppSettingsCommonFields, SaveHistory): verbose_name = 'Global Organization' ) + table_fields: list = [] + + page_layout: list = [] + def clean(self): from django.core.exceptions import ValidationError From 86008e9cbd37110a905312be98462cf6dadec860 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 11:45:47 +0930 Subject: [PATCH 327/617] feat(settings): Add App Settings API v2 endpoint ref: #248 #360 --- app/api/urls.py | 2 + app/settings/serializers/app_settings.py | 87 ++++++++++++++++++++++++ app/settings/viewsets/app_settings.py | 81 ++++++++++++++++++++++ app/settings/viewsets/index.py | 10 +++ 4 files changed, 180 insertions(+) create mode 100644 app/settings/serializers/app_settings.py create mode 100644 app/settings/viewsets/app_settings.py diff --git a/app/api/urls.py b/app/api/urls.py index 92b5700db..c6b7c35c7 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -96,6 +96,7 @@ ) from settings.viewsets import ( + app_settings as app_settings_v2, external_link as external_link_v2, index as settings_index_v2, ) @@ -184,6 +185,7 @@ router.register('v2/itim/project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') +router.register('v2/settings/app_settings', app_settings_v2.ViewSet, basename='_api_v2_app_settings') router.register('v2/settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') router.register('v2/settings/cluster_type/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_type_notes') router.register('v2/settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') diff --git a/app/settings/serializers/app_settings.py b/app/settings/serializers/app_settings.py new file mode 100644 index 000000000..6e5c7e8aa --- /dev/null +++ b/app/settings/serializers/app_settings.py @@ -0,0 +1,87 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from settings.models.app_settings import AppSettings + + + +class AppSettingsBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_app_settings-detail", format="html" + ) + + class Meta: + + model = AppSettings + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class AppSettingsModelSerializer(AppSettingsBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_app_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + } + + + class Meta: + + model = AppSettings + + fields = '__all__' + + # fields = [ + # 'id', + # 'organization', + # 'display_name', + # 'name', + # 'template', + # 'colour', + # 'cluster', + # 'devices', + # 'software', + # 'model_notes', + # 'created', + # 'modified', + # '_urls', + # ] + + # read_only_fields = [ + # 'id', + # 'display_name', + # 'created', + # 'modified', + # '_urls', + # ] + + +class AppSettingsViewSerializer(AppSettingsModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/settings/viewsets/app_settings.py b/app/settings/viewsets/app_settings.py new file mode 100644 index 000000000..a679d0176 --- /dev/null +++ b/app/settings/viewsets/app_settings.py @@ -0,0 +1,81 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from settings.serializers.app_settings import ( + AppSettings, + AppSettingsModelSerializer, + AppSettingsViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create an app setting', + description="""Add a new device to the ITAM database. + If you attempt to create a device and a device with a matching name and uuid or name and serial number + is found within the database, it will not re-create it. The device will be returned within the message body. + """, + responses = { + 201: OpenApiResponse(description='Device created', response=AppSettingsViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing create permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete an app setting', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all app settings', + description='', + responses = { + 200: OpenApiResponse(description='', response=AppSettingsViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch app settings', + description='', + responses = { + 200: OpenApiResponse(description='', response=AppSettingsViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update an app setting', + description = '', + responses = { + 200: OpenApiResponse(description='', response=AppSettingsViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelViewSet): + + model = AppSettings + + # filterset_fields = [ + # 'cluster', + # 'devices', + # 'software', + # ] + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 15a3601dc..c9b10ced2 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -17,6 +17,15 @@ class Index(CommonViewSet): ] page_layout: list = [ + { + "name": "Application", + "links": [ + { + "name": "Settings", + "model": "app_settings" + } + ] + }, { "name": "Assistanace", "links": [ @@ -98,6 +107,7 @@ def list(self, request, pk=None): return Response( { + "app_settings": reverse('API:_api_v2_app_settings-list', request=request), "cluster_type": reverse('API:_api_v2_cluster_type-list', request=request), "device_model": reverse('API:_api_v2_device_model-list', request=request), "device_type": reverse('API:_api_v2_device_type-list', request=request), From 2077becc89edb1f557bd5d5d56d605bc93619fe8 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 11:56:49 +0930 Subject: [PATCH 328/617] fix(settings): Populate user_settings Meta ref: #248 #360 --- .../0008_alter_usersettings_options.py | 17 +++++++++++++++++ app/settings/models/user_settings.py | 11 +++++++++++ 2 files changed, 28 insertions(+) create mode 100644 app/settings/migrations/0008_alter_usersettings_options.py diff --git a/app/settings/migrations/0008_alter_usersettings_options.py b/app/settings/migrations/0008_alter_usersettings_options.py new file mode 100644 index 000000000..b4a73d376 --- /dev/null +++ b/app/settings/migrations/0008_alter_usersettings_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-24 02:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0007_alter_appsettings_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='usersettings', + options={'ordering': ['user'], 'verbose_name': 'User Settings', 'verbose_name_plural': 'User Settings'}, + ), + ] diff --git a/app/settings/models/user_settings.py b/app/settings/models/user_settings.py index ff4b4db70..1970be5cb 100644 --- a/app/settings/models/user_settings.py +++ b/app/settings/models/user_settings.py @@ -30,6 +30,17 @@ class Meta: class UserSettings(UserSettingsCommonFields): + class Meta: + + ordering = [ + 'user' + ] + + verbose_name = 'User Settings' + + verbose_name_plural = 'User Settings' + + user = models.ForeignKey( User, blank= False, From 00c2826d9adf21bb460ca24cf3275b90ee89b743 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 12:42:24 +0930 Subject: [PATCH 329/617] feat(settings): Add User Settings API v2 endpoint ref: #248 #360 --- app/api/urls.py | 2 + app/api/viewsets/common.py | 13 ++++ app/settings/models/user_settings.py | 5 ++ app/settings/serializers/user_settings.py | 87 +++++++++++++++++++++++ app/settings/viewsets/index.py | 7 ++ app/settings/viewsets/user_settings.py | 81 +++++++++++++++++++++ 6 files changed, 195 insertions(+) create mode 100644 app/settings/serializers/user_settings.py create mode 100644 app/settings/viewsets/user_settings.py diff --git a/app/api/urls.py b/app/api/urls.py index c6b7c35c7..78c6e75fd 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -99,6 +99,7 @@ app_settings as app_settings_v2, external_link as external_link_v2, index as settings_index_v2, + user_settings as user_settings_v2 ) @@ -199,6 +200,7 @@ router.register('v2/settings/project_state', project_state_v2.ViewSet, basename='_api_v2_project_state') router.register('v2/settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type') router.register('v2/settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') +router.register('v2/settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') urlpatterns = [ diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index eae3b646b..b1b5e8619 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -200,6 +200,19 @@ class ModelViewSet( pass + +class ModelRetrieveUpdateViewSet( + viewsets.mixins.RetrieveModelMixin, + viewsets.mixins.UpdateModelMixin, + viewsets.GenericViewSet, + ModelViewSetBase +): + """ Use for models that you wish to update and view ONLY!""" + + pass + + + class ReadOnlyModelViewSet( viewsets.ReadOnlyModelViewSet, ModelViewSetBase diff --git a/app/settings/models/user_settings.py b/app/settings/models/user_settings.py index 1970be5cb..87b127784 100644 --- a/app/settings/models/user_settings.py +++ b/app/settings/models/user_settings.py @@ -61,6 +61,11 @@ class Meta: ) + def get_organization(self): + + return self.default_organization + + @receiver(post_save, sender=User) def new_user_callback(sender, **kwargs): settings = UserSettings.objects.filter(user=kwargs['instance']) diff --git a/app/settings/serializers/user_settings.py b/app/settings/serializers/user_settings.py new file mode 100644 index 000000000..41456bbd3 --- /dev/null +++ b/app/settings/serializers/user_settings.py @@ -0,0 +1,87 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from settings.models.user_settings import UserSettings + + + +class UserSettingsBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_user_settings-detail", format="html" + ) + + class Meta: + + model = UserSettings + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class UserSettingsModelSerializer(UserSettingsBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + } + + + class Meta: + + model = UserSettings + + fields = '__all__' + + # fields = [ + # 'id', + # 'organization', + # 'display_name', + # 'name', + # 'template', + # 'colour', + # 'cluster', + # 'devices', + # 'software', + # 'model_notes', + # 'created', + # 'modified', + # '_urls', + # ] + + # read_only_fields = [ + # 'id', + # 'display_name', + # 'created', + # 'modified', + # '_urls', + # ] + + +class UserSettingsViewSerializer(UserSettingsModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index c9b10ced2..e52086437 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -118,5 +118,12 @@ def list(self, request, pk=None): "project_state": reverse('API:_api_v2_project_state-list', request=request), "project_type": reverse('API:_api_v2_project_type-list', request=request), "software_category": reverse('API:_api_v2_software_category-list', request=request), + "user_settings": reverse( + 'API:_api_v2_user_settings-detail', + request=request, + kwargs={ + 'pk': request.user.id + } + ), } ) diff --git a/app/settings/viewsets/user_settings.py b/app/settings/viewsets/user_settings.py new file mode 100644 index 000000000..8a5ad2d70 --- /dev/null +++ b/app/settings/viewsets/user_settings.py @@ -0,0 +1,81 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelRetrieveUpdateViewSet + +from settings.serializers.user_settings import ( + UserSettings, + UserSettingsModelSerializer, + UserSettingsViewSerializer +) + + + +@extend_schema_view( + # create=extend_schema( + # summary = 'Create an user setting', + # description="""Add a new device to the ITAM database. + # If you attempt to create a device and a device with a matching name and uuid or name and serial number + # is found within the database, it will not re-create it. The device will be returned within the message body. + # """, + # responses = { + # 201: OpenApiResponse(description='Device created', response=UserSettingsViewSerializer), + # 400: OpenApiResponse(description='Validation failed.'), + # 403: OpenApiResponse(description='User is missing create permissions'), + # } + # ), + # destroy = extend_schema( + # summary = 'Delete an user setting', + # description = '', + # responses = { + # 204: OpenApiResponse(description=''), + # 403: OpenApiResponse(description='User is missing delete permissions'), + # } + # ), + # list = extend_schema( + # summary = 'Fetch all user settings', + # description='', + # responses = { + # 200: OpenApiResponse(description='', response=UserSettingsViewSerializer), + # 403: OpenApiResponse(description='User is missing view permissions'), + # } + # ), + retrieve = extend_schema( + summary = 'Fetch user settings', + description='', + responses = { + 200: OpenApiResponse(description='', response=UserSettingsViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update an user setting', + description = '', + responses = { + 200: OpenApiResponse(description='', response=UserSettingsViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelRetrieveUpdateViewSet): + + model = UserSettings + + # filterset_fields = [ + # 'cluster', + # 'devices', + # 'software', + # ] + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] From 20dc72d56440d8c8c88bd52adf378102e9f8a859 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 13:32:07 +0930 Subject: [PATCH 330/617] feat(api): Added ability to specify table fields within the viewset. required for models that are external to centurion ref: #248 #360 --- app/api/react_ui_metadata.py | 4 ++-- app/api/viewsets/common.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index ab9df9e42..677a734e6 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -83,9 +83,9 @@ def determine_metadata(self, request, view): elif view.suffix == 'List': - if hasattr(view, 'model'): + if hasattr(view, 'table_fields'): - metadata['table_fields'] = view.model.table_fields + metadata['table_fields'] = view.get_table_fields() if view.documentation: diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index b1b5e8619..116008814 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -68,6 +68,13 @@ def allowed_methods(self): _Mandatory_, Permission check class """ + table_fields: list = [] + """ Table layout list + + _Optional_, used by metadata for the table fields and added to the HTTP/Options + method for detail view, Enables the UI can setup the table. + """ + view_description: str = None view_name: str = None @@ -105,6 +112,23 @@ def get_page_layout(self): return self.page_layout + def get_table_fields(self): + + if len(self.table_fields) < 1: + + if hasattr(self, 'model'): + + if hasattr(self.model, 'table_fields'): + + self.table_fields = self.model.table_fields + + else: + + self.table_fields = [] + + return self.table_fields + + def get_view_description(self, html=False) -> str: if not self.view_description: From ec16910ec6d7dcdb6823092995e11ea7851c7077 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 13:33:58 +0930 Subject: [PATCH 331/617] feat(settings): Add Celery Task Logs API v2 endpoint ref: #248 #360 --- app/api/react_ui_metadata.py | 6 ++ app/api/urls.py | 6 +- app/api/views/project_management/index.py | 4 +- app/api/views/settings/index.py | 2 +- app/core/serializers/celery_log.py | 70 +++++++++++++++ app/core/viewsets/celery_log.py | 102 ++++++++++++++++++++++ 6 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 app/core/serializers/celery_log.py create mode 100644 app/core/viewsets/celery_log.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 677a734e6..026368f40 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -197,6 +197,12 @@ def determine_metadata(self, request, view): "name": "system", "icon": "settings", "link": "/settings" + }, + { + "display_name": "Task Log", + "name": "celery_task_log", + # "icon": "settings", + "link": "/settings/celery_log" } ] } diff --git a/app/api/urls.py b/app/api/urls.py index 78c6e75fd..1b38c4f26 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -62,7 +62,8 @@ from core.viewsets import ( history as history_v2, notes as notes_v2, - manufacturer as manufacturer_v2 + manufacturer as manufacturer_v2, + celery_log as celery_log_v2 ) from itam.viewsets import ( @@ -202,6 +203,9 @@ router.register('v2/settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') router.register('v2/settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') + +router.register('v2/settings/celery_log', celery_log_v2.ViewSet, basename='_api_v2_celery_log') + urlpatterns = [ path("assistance", assistance.index.Index.as_view(), name="_api_assistance"), diff --git a/app/api/views/project_management/index.py b/app/api/views/project_management/index.py index 4286fdeb6..06d04255e 100644 --- a/app/api/views/project_management/index.py +++ b/app/api/views/project_management/index.py @@ -1,12 +1,14 @@ from django.utils.safestring import mark_safe +from drf_spectacular.utils import extend_schema + from rest_framework import generics, permissions, routers, views from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.reverse import reverse - +@extend_schema(deprecated=True) class Index(views.APIView): permission_classes = [ diff --git a/app/api/views/settings/index.py b/app/api/views/settings/index.py index 112469f82..2de2a4f15 100644 --- a/app/api/views/settings/index.py +++ b/app/api/views/settings/index.py @@ -10,7 +10,7 @@ from core.http.common import Http - +@extend_schema( deprecated = True ) class View(views.APIView): permission_classes = [ diff --git a/app/core/serializers/celery_log.py b/app/core/serializers/celery_log.py new file mode 100644 index 000000000..19d532d3b --- /dev/null +++ b/app/core/serializers/celery_log.py @@ -0,0 +1,70 @@ +import json + +from rest_framework.reverse import reverse +from rest_framework import serializers + +from django_celery_results.models import TaskResult + +from access.serializers.organization import OrganizationBaseSerializer + +from app.serializers.user import UserBaseSerializer + + + +class TaskResultBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_manufacturer-detail", format="html" + ) + + + class Meta: + + model = TaskResult + + fields = [ + 'id', + 'display_name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url', + ] + + +class TaskResultModelSerializer(TaskResultBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_manufacturer-detail", + request=self._context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + } + + + class Meta: + + model = TaskResult + + fields = '__all__' + + +class TaskResultViewSerializer(TaskResultModelSerializer): + + pass diff --git a/app/core/viewsets/celery_log.py b/app/core/viewsets/celery_log.py new file mode 100644 index 000000000..647e9e3c5 --- /dev/null +++ b/app/core/viewsets/celery_log.py @@ -0,0 +1,102 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from core.serializers.celery_log import ( + TaskResult, + TaskResultModelSerializer, + TaskResultViewSerializer +) + +from api.viewsets.common import ReadOnlyModelViewSet + + + + +@extend_schema_view( + list = extend_schema( + summary = 'Fetch all Celery Logs', + description='', + responses = { + 200: OpenApiResponse(description='', response=TaskResultViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single Celery Log', + description='', + responses = { + 200: OpenApiResponse(description='', response=TaskResultViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), +) +class ViewSet(ReadOnlyModelViewSet): + + filterset_fields = [ + 'periodic_task_name', + 'result', + 'status', + 'task_name', + 'worker', + ] + + search_fields = [ + 'result', + 'task_args', + 'task_name', + 'worker', + ] + + model = TaskResult + + page_layout: list = [ + { + "name": "Details", + "slug": "details", + "sections": [ + { + "layout": "double", + "left": [ + 'task_id', + 'periodic_task_name', + 'task_name', + 'status', + ], + "right": [ + 'worker', + 'task_kwargs', + 'date_created', + 'date_done', + 'result', + ] + }, + { + "layout": "single", + "fields": [ + "task_args" + ] + } + ] + }, + ] + + table_fields: list = [ + 'id', + 'task_id', + 'task_name', + 'status', + 'date_done', + 'date_created', + ] + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()['TaskResultViewSerializer'] + + + return globals()['TaskResultModelSerializer'] From 926349e04eff49f3ef4fb9bc0fdb7934e5eab5d5 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 14:13:00 +0930 Subject: [PATCH 332/617] feat(settings): Add get_organization function to app settings model ref: #248 #360 --- app/settings/models/app_settings.py | 6 ++++++ app/settings/serializers/app_settings.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/settings/models/app_settings.py b/app/settings/models/app_settings.py index f486a2847..ac0cc3db5 100644 --- a/app/settings/models/app_settings.py +++ b/app/settings/models/app_settings.py @@ -111,6 +111,12 @@ class Meta: page_layout: list = [] + + def get_organization(self): + + return self.global_organization + + def clean(self): from django.core.exceptions import ValidationError diff --git a/app/settings/serializers/app_settings.py b/app/settings/serializers/app_settings.py index 6e5c7e8aa..a743bec1d 100644 --- a/app/settings/serializers/app_settings.py +++ b/app/settings/serializers/app_settings.py @@ -84,4 +84,4 @@ class Meta: class AppSettingsViewSerializer(AppSettingsModelSerializer): - organization = OrganizationBaseSerializer( many = False, read_only = True ) + global_organization = OrganizationBaseSerializer( many = False, read_only = True ) From 768fd8d640c4d61f3b15905c0e517ed52c2055ba Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 14:14:03 +0930 Subject: [PATCH 333/617] test(settings): App Settings API field checks ref: #15 #248 #360 --- .../app_settings/test_app_settings_api_v2.py | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 app/settings/tests/unit/app_settings/test_app_settings_api_v2.py diff --git a/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py b/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py new file mode 100644 index 000000000..8fc037728 --- /dev/null +++ b/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py @@ -0,0 +1,252 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APICommonFields + +from settings.models.app_settings import AppSettings +from settings.models.user_settings import UserSettings + + + +class AppSettingsAPI( + TestCase, + APICommonFields +): + + model = AppSettings + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + self.item = AppSettings.objects.get( id = 1 ) + + self.item.global_organization = self.organization + + self.item.save() + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + user_settings = UserSettings.objects.get(user = self.view_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_app_settings-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_device_model_is_global(self): + """ Test for existance of API Field + + device_model_is_global field must exist + """ + + assert 'device_model_is_global' in self.api_data + + + def test_api_field_type_device_model_is_global(self): + """ Test for type for API Field + + device_model_is_global field must be bool + """ + + assert type(self.api_data['device_model_is_global']) is bool + + + + def test_api_field_exists_device_type_is_global(self): + """ Test for existance of API Field + + device_type_is_global field must exist + """ + + assert 'device_type_is_global' in self.api_data + + + def test_api_field_type_device_type_is_global(self): + """ Test for type for API Field + + device_type_is_global field must be bool + """ + + assert type(self.api_data['device_type_is_global']) is bool + + + + def test_api_field_exists_manufacturer_is_global(self): + """ Test for existance of API Field + + manufacturer_is_global field must exist + """ + + assert 'manufacturer_is_global' in self.api_data + + + def test_api_field_type_manufacturer_is_global(self): + """ Test for type for API Field + + manufacturer_is_global field must be bool + """ + + assert type(self.api_data['manufacturer_is_global']) is bool + + + + def test_api_field_exists_software_is_global(self): + """ Test for existance of API Field + + software_is_global field must exist + """ + + assert 'software_is_global' in self.api_data + + + def test_api_field_type_software_is_global(self): + """ Test for type for API Field + + software_is_global field must be bool + """ + + assert type(self.api_data['software_is_global']) is bool + + + + def test_api_field_exists_software_categories_is_global(self): + """ Test for existance of API Field + + software_categories_is_global field must exist + """ + + assert 'software_categories_is_global' in self.api_data + + + def test_api_field_type_software_categories_is_global(self): + """ Test for type for API Field + + software_categories_is_global field must be bool + """ + + assert type(self.api_data['software_categories_is_global']) is bool + + + + def test_api_field_exists_global_organization(self): + """ Test for existance of API Field + + global_organization field must exist + """ + + assert 'global_organization' in self.api_data + + + def test_api_field_type_global_organization(self): + """ Test for type for API Field + + global_organization field must be dict + """ + + assert type(self.api_data['global_organization']) is dict + + + def test_api_field_exists_global_organization_id(self): + """ Test for existance of API Field + + global_organization.id field must exist + """ + + assert 'id' in self.api_data['global_organization'] + + + def test_api_field_type_global_organization_id(self): + """ Test for type for API Field + + global_organization.id field must be dict + """ + + assert type(self.api_data['global_organization']['id']) is int + + + def test_api_field_exists_global_organization_display_name(self): + """ Test for existance of API Field + + global_organization.display_name field must exist + """ + + assert 'display_name' in self.api_data['global_organization'] + + + def test_api_field_type_global_organization_display_name(self): + """ Test for type for API Field + + global_organization.display_name field must be str + """ + + assert type(self.api_data['global_organization']['display_name']) is str + + + def test_api_field_exists_global_organization_url(self): + """ Test for existance of API Field + + global_organization.url field must exist + """ + + assert 'url' in self.api_data['global_organization'] + + + def test_api_field_type_global_organization_url(self): + """ Test for type for API Field + + global_organization.url field must be str + """ + + assert type(self.api_data['global_organization']['url']) is Hyperlink From 94045d136fe6f749161848f54a064165debf308d Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 14:57:42 +0930 Subject: [PATCH 334/617] test(settings): App Settings API v2 ViewSet permission checks ref: #15 #248 #360 --- .../app_settings/test_app_settings_viewset.py | 267 ++++++++++++++++++ app/settings/viewsets/app_settings.py | 4 +- 2 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 app/settings/tests/unit/app_settings/test_app_settings_viewset.py diff --git a/app/settings/tests/unit/app_settings/test_app_settings_viewset.py b/app/settings/tests/unit/app_settings/test_app_settings_viewset.py new file mode 100644 index 000000000..42ee833df --- /dev/null +++ b/app/settings/tests/unit/app_settings/test_app_settings_viewset.py @@ -0,0 +1,267 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase +from django import urls + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import ( + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +) + +from settings.models.app_settings import AppSettings + + + +class AppSettingsPermissionsAPI( + TestCase, + # APIPermissions + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +): + + model = AppSettings + + app_namespace = 'API' + + url_name = '_api_v2_app_settings' + + change_data = {'device_model_is_global': True} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.item = AppSettings.objects.get( id = 1 ) + + self.item.global_organization = self.organization + + self.item.save() + + + self.url_view_kwargs = {'pk': self.item.id} + + # self.url_kwargs = {} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + + def test_add_create_not_allowed(self): + """ Check correct permission for add + + Not allowed to add. + Ensure that the list view for HTTP/POST does not exist. + """ + + with pytest.raises(urls.exceptions.NoReverseMatch) as e: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + assert e.typename == 'NoReverseMatch' + + + def test_delete_has_permission(self): + """ Check correct permission for delete + + Delete item as user with delete permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.delete_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + # def test_view_has_permission(self): + # """ Check correct permission for view + + # Attempt to view as user with view permission + # """ + + # client = Client() + # url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + # client.force_login(self.view_user) + # response = client.get(url) + + # assert response.status_code == 403 + + + # def test_change_has_permission(self): + # """ Check correct permission for change + + # Make change with user who has change permission + # """ + + # client = Client() + # url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + # client.force_login(self.change_user) + # response = client.patch(url, data=self.change_data, content_type='application/json') + + # assert response.status_code == 403 + + + # def test_delete_has_permission(self): + # """ Check correct permission for delete + + # Delete item as user with delete permission + # """ + + # client = Client() + # url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + # client.force_login(self.delete_user) + # response = client.delete(url, data=self.delete_data) + + # assert response.status_code == 403 + diff --git a/app/settings/viewsets/app_settings.py b/app/settings/viewsets/app_settings.py index a679d0176..2e161c394 100644 --- a/app/settings/viewsets/app_settings.py +++ b/app/settings/viewsets/app_settings.py @@ -1,6 +1,6 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse -from api.viewsets.common import ModelViewSet +from api.viewsets.common import ModelRetrieveUpdateViewSet from settings.serializers.app_settings import ( AppSettings, @@ -57,7 +57,7 @@ } ), ) -class ViewSet(ModelViewSet): +class ViewSet(ModelRetrieveUpdateViewSet): model = AppSettings From 91377582940764705f083779c09f6dc53faec014 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 15:19:20 +0930 Subject: [PATCH 335/617] test(settings): User Settings API field checks ref: #15 #248 #360 --- app/settings/serializers/user_settings.py | 12 +- .../test_user_settings_api_v2.py | 160 ++++++++++++++++++ 2 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 app/settings/tests/unit/user_settings/test_user_settings_api_v2.py diff --git a/app/settings/serializers/user_settings.py b/app/settings/serializers/user_settings.py index 41456bbd3..0e2ea20d7 100644 --- a/app/settings/serializers/user_settings.py +++ b/app/settings/serializers/user_settings.py @@ -73,15 +73,11 @@ class Meta: # '_urls', # ] - # read_only_fields = [ - # 'id', - # 'display_name', - # 'created', - # 'modified', - # '_urls', - # ] + read_only_fields = [ + 'user', + ] class UserSettingsViewSerializer(UserSettingsModelSerializer): - organization = OrganizationBaseSerializer( many = False, read_only = True ) + default_organization = OrganizationBaseSerializer( many = False, read_only = True ) diff --git a/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py b/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py new file mode 100644 index 000000000..b450b0e99 --- /dev/null +++ b/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py @@ -0,0 +1,160 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APICommonFields + +from settings.models.app_settings import AppSettings +from settings.models.user_settings import UserSettings + + + +class UserSettingsAPI( + TestCase, + APICommonFields +): + + model = UserSettings + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + + + self.item = self.model.objects.get( id = self.view_user.id ) + + self.item.default_organization = self.organization + + self.item.save() + + + + user_settings = UserSettings.objects.get(user = self.view_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_user_settings-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_default_organization(self): + """ Test for existance of API Field + + default_organization field must exist + """ + + assert 'default_organization' in self.api_data + + + def test_api_field_type_default_organization(self): + """ Test for type for API Field + + default_organization field must be dict + """ + + assert type(self.api_data['default_organization']) is dict + + + def test_api_field_exists_default_organization_id(self): + """ Test for existance of API Field + + default_organization.id field must exist + """ + + assert 'id' in self.api_data['default_organization'] + + + def test_api_field_type_default_organization_id(self): + """ Test for type for API Field + + default_organization.id field must be dict + """ + + assert type(self.api_data['default_organization']['id']) is int + + + def test_api_field_exists_default_organization_display_name(self): + """ Test for existance of API Field + + default_organization.display_name field must exist + """ + + assert 'display_name' in self.api_data['default_organization'] + + + def test_api_field_type_default_organization_display_name(self): + """ Test for type for API Field + + default_organization.display_name field must be str + """ + + assert type(self.api_data['default_organization']['display_name']) is str + + + def test_api_field_exists_default_organization_url(self): + """ Test for existance of API Field + + default_organization.url field must exist + """ + + assert 'url' in self.api_data['default_organization'] + + + def test_api_field_type_default_organization_url(self): + """ Test for type for API Field + + default_organization.url field must be str + """ + + assert type(self.api_data['default_organization']['url']) is Hyperlink From 50edfd59972cc1d8af76de7f566ee9ae8c02a695 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 15:31:55 +0930 Subject: [PATCH 336/617] test(settings): User Settings API v2 ViewSet permission checks ref: #15 #248 #360 --- .../test_user_settings_viewset.py | 241 ++++++++++++++++++ app/settings/viewsets/index.py | 2 +- 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 app/settings/tests/unit/user_settings/test_user_settings_viewset.py diff --git a/app/settings/tests/unit/user_settings/test_user_settings_viewset.py b/app/settings/tests/unit/user_settings/test_user_settings_viewset.py new file mode 100644 index 000000000..77897e93f --- /dev/null +++ b/app/settings/tests/unit/user_settings/test_user_settings_viewset.py @@ -0,0 +1,241 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase +from django import urls + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import ( + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +) + +from settings.models.user_settings import UserSettings + + +class UserSettingsPermissionsAPI( + TestCase, + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +): + + model = UserSettings + + app_namespace = 'API' + + url_name = '_api_v2_user_settings' + + change_data = {'device_model_is_global': True} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + self.different_organization = different_organization + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + self.item = self.model.objects.get( id = 1 ) + + self.item.default_organization = self.organization + + self.item.save() + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + + def test_add_create_not_allowed(self): + """ Check correct permission for add + + Not allowed to add. + Ensure that the list view for HTTP/POST does not exist. + """ + + with pytest.raises(urls.exceptions.NoReverseMatch) as e: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + assert e.typename == 'NoReverseMatch' + + + + def test_delete_has_permission(self): + """ Check correct permission for delete + + Delete item as user with delete permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.delete_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + + def test_change_has_permission(self): + """ Check correct permission for change + + Make change with user who has change permission + """ + + + item = self.model.objects.get( id = self.change_user.id ) + + item.default_organization = self.organization + + item.save() + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs={'pk': item.id}) + + + client.force_login(self.change_user) + response = client.patch(url, data={'different_organization': self.different_organization.id}, content_type='application/json') + + assert response.status_code == 200 diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index e52086437..c32705eb3 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -107,7 +107,7 @@ def list(self, request, pk=None): return Response( { - "app_settings": reverse('API:_api_v2_app_settings-list', request=request), + "app_settings": reverse('API:_api_v2_app_settings-detail', request=request, kwargs={'pk': 1}), "cluster_type": reverse('API:_api_v2_cluster_type-list', request=request), "device_model": reverse('API:_api_v2_device_model-list', request=request), "device_type": reverse('API:_api_v2_device_type-list', request=request), From 4412ff14e7d60428a2e3f74fde0e3d236242312d Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 15:52:37 +0930 Subject: [PATCH 337/617] test(settings): Celery Log API field checks ref: #15 #248 #360 --- app/core/serializers/celery_log.py | 4 +- .../test_task_result_api_v2.py | 372 ++++++++++++++++++ app/settings/viewsets/index.py | 1 + 3 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 app/core/tests/unit/test_task_result/test_task_result_api_v2.py diff --git a/app/core/serializers/celery_log.py b/app/core/serializers/celery_log.py index 19d532d3b..9dc443626 100644 --- a/app/core/serializers/celery_log.py +++ b/app/core/serializers/celery_log.py @@ -20,7 +20,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_manufacturer-detail", format="html" + view_name="API:_api_v2_celery_log-detail", format="html" ) @@ -49,7 +49,7 @@ class TaskResultModelSerializer(TaskResultBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_manufacturer-detail", + '_self': reverse("API:_api_v2_celery_log-detail", request=self._context['view'].request, kwargs={ 'pk': item.pk diff --git a/app/core/tests/unit/test_task_result/test_task_result_api_v2.py b/app/core/tests/unit/test_task_result/test_task_result_api_v2.py new file mode 100644 index 000000000..1d40a6d77 --- /dev/null +++ b/app/core/tests/unit/test_task_result/test_task_result_api_v2.py @@ -0,0 +1,372 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from django_celery_results.models import TaskResult + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APICommonFields + + + +class CeleryTaskResultAPI( + TestCase, + APICommonFields +): + + model = TaskResult + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + + self.item = self.model.objects.create( + task_id = 'd15233ee-a14d-4135-afe5-e406b1b61330', + task_name = 'api.tasks.process_inventory', + task_args = '{"random": "value"}', + task_kwargs = 'sdas', + status = "SUCCESS", + worker = "debug-itsm@laptop2", + content_type = "application/json", + content_encoding = "utf-8", + result = "finished...", + traceback = "a trace", + meta = 'meta', + periodic_task_name = 'a name', + ) + + + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('API:_api_v2_celery_log-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + def test_api_field_exists_task_id(self): + """ Test for existance of API Field + + task_id field must exist + """ + + assert 'task_id' in self.api_data + + + def test_api_field_type_task_id(self): + """ Test for type for API Field + + task_id field must be str + """ + + assert type(self.api_data['task_id']) is str + + + + + def test_api_field_exists_periodic_task_name(self): + """ Test for existance of API Field + + periodic_task_name field must exist + """ + + assert 'periodic_task_name' in self.api_data + + + def test_api_field_type_periodic_task_name(self): + """ Test for type for API Field + + periodic_task_name field must be str + """ + + assert type(self.api_data['periodic_task_name']) is str + + + + def test_api_field_exists_task_name(self): + """ Test for existance of API Field + + task_name field must exist + """ + + assert 'task_name' in self.api_data + + + def test_api_field_type_task_name(self): + """ Test for type for API Field + + task_name field must be str + """ + + assert type(self.api_data['task_name']) is str + + + + def test_api_field_exists_task_args(self): + """ Test for existance of API Field + + task_args field must exist + """ + + assert 'task_args' in self.api_data + + + def test_api_field_type_task_args(self): + """ Test for type for API Field + + task_args field must be str + """ + + assert type(self.api_data['task_args']) is str + + + + def test_api_field_exists_task_kwargs(self): + """ Test for existance of API Field + + task_kwargs field must exist + """ + + assert 'task_kwargs' in self.api_data + + + def test_api_field_type_task_kwargs(self): + """ Test for type for API Field + + task_kwargs field must be str + """ + + assert type(self.api_data['task_kwargs']) is str + + + + def test_api_field_exists_task_kwargs(self): + """ Test for existance of API Field + + task_kwargs field must exist + """ + + assert 'task_kwargs' in self.api_data + + + def test_api_field_type_task_kwargs(self): + """ Test for type for API Field + + task_kwargs field must be str + """ + + assert type(self.api_data['task_kwargs']) is str + + + + def test_api_field_exists_status(self): + """ Test for existance of API Field + + status field must exist + """ + + assert 'status' in self.api_data + + + def test_api_field_type_status(self): + """ Test for type for API Field + + status field must be str + """ + + assert type(self.api_data['status']) is str + + + + def test_api_field_exists_worker(self): + """ Test for existance of API Field + + worker field must exist + """ + + assert 'worker' in self.api_data + + + def test_api_field_type_worker(self): + """ Test for type for API Field + + worker field must be str + """ + + assert type(self.api_data['worker']) is str + + + + def test_api_field_exists_content_type(self): + """ Test for existance of API Field + + content_type field must exist + """ + + assert 'content_type' in self.api_data + + + def test_api_field_type_content_type(self): + """ Test for type for API Field + + content_type field must be str + """ + + assert type(self.api_data['content_type']) is str + + + + def test_api_field_exists_content_encoding(self): + """ Test for existance of API Field + + content_encoding field must exist + """ + + assert 'content_encoding' in self.api_data + + + def test_api_field_type_content_encoding(self): + """ Test for type for API Field + + content_encoding field must be str + """ + + assert type(self.api_data['content_encoding']) is str + + + + def test_api_field_exists_result(self): + """ Test for existance of API Field + + result field must exist + """ + + assert 'result' in self.api_data + + + def test_api_field_type_result(self): + """ Test for type for API Field + + result field must be str + """ + + assert type(self.api_data['result']) is str + + + + def test_api_field_exists_date_created(self): + """ Test for existance of API Field + + date_created field must exist + """ + + assert 'date_created' in self.api_data + + + def test_api_field_type_date_created(self): + """ Test for type for API Field + + date_created field must be str + """ + + assert type(self.api_data['date_created']) is str + + + + def test_api_field_exists_date_done(self): + """ Test for existance of API Field + + date_done field must exist + """ + + assert 'date_done' in self.api_data + + + def test_api_field_type_date_done(self): + """ Test for type for API Field + + date_done field must be str + """ + + assert type(self.api_data['date_done']) is str + + + + def test_api_field_exists_traceback(self): + """ Test for existance of API Field + + traceback field must exist + """ + + assert 'traceback' in self.api_data + + + def test_api_field_type_traceback(self): + """ Test for type for API Field + + traceback field must be str + """ + + assert type(self.api_data['traceback']) is str + + + + def test_api_field_exists_meta(self): + """ Test for existance of API Field + + meta field must exist + """ + + assert 'meta' in self.api_data + + + def test_api_field_type_meta(self): + """ Test for type for API Field + + meta field must be str + """ + + assert type(self.api_data['meta']) is str diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index c32705eb3..10e381b09 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -108,6 +108,7 @@ def list(self, request, pk=None): return Response( { "app_settings": reverse('API:_api_v2_app_settings-detail', request=request, kwargs={'pk': 1}), + "celery_log": reverse('API:_api_v2_celery_log-list', request=request), "cluster_type": reverse('API:_api_v2_cluster_type-list', request=request), "device_model": reverse('API:_api_v2_device_model-list', request=request), "device_type": reverse('API:_api_v2_device_type-list', request=request), From cdacd70bf132f6066e5157c6a1fb1edd655eac76 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 24 Oct 2024 16:11:36 +0930 Subject: [PATCH 338/617] test(settings): Celery Log API v2 ViewSet permission checks ref: #15 #248 #360 --- .../test_task_result_viewset.py | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 app/core/tests/unit/test_task_result/test_task_result_viewset.py diff --git a/app/core/tests/unit/test_task_result/test_task_result_viewset.py b/app/core/tests/unit/test_task_result/test_task_result_viewset.py new file mode 100644 index 000000000..17b82852a --- /dev/null +++ b/app/core/tests/unit/test_task_result/test_task_result_viewset.py @@ -0,0 +1,522 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase +from django import urls + +from django_celery_results.models import TaskResult + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import ( + APIPermissionAdd, + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +) + +from settings.models.user_settings import UserSettings + + +class TaskResultPermissionsAPI( + TestCase, + APIPermissionAdd, + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +): + """These tests are custom tests of test of the same name. + + This model is View Only for any authenticated user. + """ + + model = TaskResult + + app_namespace = 'API' + + url_name = '_api_v2_celery_log' + + change_data = {'device_model_is_global': True} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + self.different_organization = different_organization + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + # self.item = self.model.objects.get( id = 1 ) + + # self.item.default_organization = self.organization + + # self.item.save() + + + self.item = self.model.objects.create( + task_id = 'd15233ee-a14d-4135-afe5-e406b1b61330', + task_name = 'api.tasks.process_inventory', + task_args = '{"random": "value"}', + task_kwargs = 'sdas', + status = "SUCCESS", + worker = "debug-itsm@laptop2", + content_type = "application/json", + content_encoding = "utf-8", + result = "finished...", + traceback = "a trace", + meta = 'meta', + periodic_task_name = 'a name', + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team-post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + def test_add_no_permission_denied(self): + """ Check correct permission for add + + Attempt to add as user with no permissions + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.no_permissions_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 405 + + + # @pytest.mark.skip(reason="ToDO: figure out why fails") + def test_add_different_organization_denied(self): + """ Check correct permission for add + + attempt to add as user from different organization + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.different_organization_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 405 + + + def test_add_permission_view_denied(self): + """ Check correct permission for add + + Attempt to add a user with view permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.view_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 405 + + + + + def test_add_has_permission(self): + """ Check correct permission for add + + Attempt to add as user with permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 405 + + + + + + + def test_change_no_permission_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user without permissions + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.no_permissions_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 + + + def test_change_different_organization_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user from different organization + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.different_organization_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 + + + def test_change_permission_view_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user with view permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 + + + def test_change_permission_add_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as user with add permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.add_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 + + + + def test_change_has_permission(self): + """ Check correct permission for change + + Make change with user who has change permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 + + + + + def test_delete_no_permission_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with no permissons + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.no_permissions_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 405 + + + def test_delete_different_organization_denied(self): + """ Check correct permission for delete + + Attempt to delete as user from different organization + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.different_organization_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 405 + + + + + def test_delete_permission_view_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with veiw permission only + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 405 + + + def test_delete_permission_add_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with add permission only + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.add_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 405 + + + def test_delete_permission_change_denied(self): + """ Check correct permission for delete + + Attempt to delete as user with change permission only + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 405 + + + + + + + + def test_delete_has_permission(self): + """ Check correct permission for delete + + Delete item as user with delete permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.delete_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 405 + + + def test_view_no_permission_denied(self): + """ Check correct permission for view + + Attempt to view with user missing permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.no_permissions_user) + response = client.get(url) + + assert response.status_code == 200 + + + def test_view_different_organizaiton_denied(self): + """ Check correct permission for view + + Attempt to view with user from different organization + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.different_organization_user) + response = client.get(url) + + assert response.status_code == 200 From 268e3294a27cb7516f81f6861b42722bcdf5b5da Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 18:02:23 +0930 Subject: [PATCH 339/617] feat(api): Setup API to be correctly versioned ref: #248 #365 --- Release-Notes.md | 4 +- app/access/serializers/organization.py | 6 +- app/access/serializers/team_user.py | 4 +- app/access/serializers/teams.py | 6 +- .../organization/test_organizaiton_api.py | 2 +- .../organization/test_organizaiton_api_v2.py | 2 +- .../test_organizaiton_permission_api.py | 2 +- app/access/tests/unit/team/test_team_api.py | 2 +- .../tests/unit/team/test_team_api_v2.py | 2 +- .../unit/team/test_team_permission_api.py | 2 +- .../unit/team_user/test_team_user_api_v2.py | 2 +- app/access/viewsets/index.py | 2 +- app/api/serializers/access.py | 14 +- app/api/serializers/config.py | 4 +- app/api/serializers/core/ticket.py | 4 +- app/api/serializers/core/ticket_category.py | 2 +- app/api/serializers/core/ticket_comment.py | 2 +- .../core/ticket_comment_category.py | 2 +- app/api/serializers/itam/device.py | 6 +- app/api/serializers/itam/software.py | 2 +- .../project_management/project_milestone.py | 2 +- .../project_management/project_state.py | 2 +- .../project_management/project_type.py | 2 +- .../project_management/projects.py | 6 +- .../unit/inventory/test_api_inventory.py | 14 +- .../test_inventory_permission_api.py | 10 +- app/api/urls.py | 146 --------------- app/api/urls_v2.py | 167 ++++++++++++++++++ app/api/views/assistance/index.py | 2 +- app/api/views/index.py | 22 +-- app/api/views/itim/index.py | 6 +- app/api/views/project_management/index.py | 2 +- app/api/views/settings/index.py | 10 +- app/api/viewsets/index.py | 25 ++- app/app/serializers/content_type.py | 4 +- app/app/serializers/permission.py | 4 +- app/app/serializers/user.py | 2 +- app/app/settings.py | 14 +- app/app/urls.py | 8 +- app/app/viewsets/base/index.py | 6 +- app/assistance/serializers/knowledge_base.py | 12 +- .../serializers/knowledge_base_category.py | 10 +- .../test_knowledge_base_api_v2.py | 2 +- .../test_knowledge_base_viewset.py | 2 +- .../test_knowledge_base_category_api_v2.py | 2 +- .../test_knowledge_base_category_viewset.py | 2 +- .../tests/unit/test_assistance_viewset.py | 2 +- app/assistance/viewsets/index.py | 4 +- .../serializers/config_group.py | 16 +- .../serializers/config_group_software.py | 6 +- .../config_groups/test_config_groups_api.py | 2 +- .../test_config_groups_api_v2.py | 2 +- .../test_config_groups_viewset.py | 2 +- .../test_config_groups_software_api_v2.py | 2 +- .../test_config_groups_software_viewset.py | 2 +- .../unit/test_config_management_viewset.py | 2 +- app/config_management/viewsets/index.py | 2 +- app/core/serializers/celery_log.py | 4 +- app/core/serializers/history.py | 4 +- app/core/serializers/manufacturer.py | 8 +- app/core/serializers/notes.py | 14 +- .../manufacturer/test_manufacturer_api_v2.py | 2 +- .../manufacturer/test_manufacturer_viewset.py | 2 +- .../unit/test_history/test_history_viewset.py | 2 +- .../unit/test_notes/test_notes_api_v2.py | 2 +- .../test_task_result_api_v2.py | 2 +- .../test_task_result_viewset.py | 2 +- .../unit/ticket/test_ticket_permission_api.py | 8 +- .../test_ticket_category_permission_api.py | 2 +- .../test_ticket_comment_permission_api.py | 8 +- ..._ticket_comment_category_permission_api.py | 2 +- app/itam/serializers/device.py | 18 +- app/itam/serializers/device_model.py | 4 +- app/itam/serializers/device_software.py | 4 +- app/itam/serializers/device_type.py | 4 +- app/itam/serializers/operating_system.py | 10 +- .../serializers/operating_system_version.py | 8 +- app/itam/serializers/software.py | 14 +- app/itam/serializers/software_category.py | 4 +- app/itam/serializers/software_version.py | 6 +- app/itam/tests/unit/device/test_device_api.py | 10 +- .../tests/unit/device/test_device_api_v2.py | 2 +- .../unit/device/test_device_permission_api.py | 2 +- .../tests/unit/device/test_device_viewset.py | 2 +- .../device_model/test_device_model_api_v2.py | 2 +- .../device_model/test_device_model_viewset.py | 2 +- .../test_device_software_api_v2.py | 2 +- .../test_device_software_viewset.py | 2 +- .../device_type/test_device_type_api_v2.py | 2 +- .../device_type/test_device_type_viewset.py | 2 +- .../test_operating_system_api_v2.py | 2 +- .../test_operating_system_viewset.py | 2 +- .../test_operating_system_version_api_v2.py | 2 +- .../test_operating_system_version_viewset.py | 2 +- .../tests/unit/software/test_software_api.py | 2 +- .../unit/software/test_software_api_v2.py | 21 ++- .../software/test_software_permission_api.py | 2 +- .../unit/software/test_software_viewset.py | 2 +- .../test_software_category_api_v2.py | 2 +- .../test_software_category_viewset.py | 2 +- .../test_software_version_api_v2.py | 2 +- .../test_software_version_viewset.py | 2 +- app/itam/tests/unit/test_itam_viewset.py | 2 +- app/itam/viewsets/index.py | 6 +- app/itim/serializers/cluster.py | 8 +- app/itim/serializers/cluster_type.py | 8 +- app/itim/serializers/port.py | 8 +- app/itim/serializers/service.py | 8 +- .../tests/unit/cluster/test_cluster_api_v2.py | 2 +- .../unit/cluster/test_cluster_viewset.py | 2 +- .../cluster_types/test_cluster_type_api_v2.py | 2 +- .../test_cluster_type_viewset.py | 2 +- app/itim/tests/unit/port/test_port_api_v2.py | 2 +- app/itim/tests/unit/port/test_port_viewset.py | 2 +- .../tests/unit/service/test_service_api_v2.py | 4 +- .../unit/service/test_service_viewset.py | 2 +- app/itim/tests/unit/test_itim_viewset.py | 2 +- app/itim/viewsets/index.py | 4 +- app/project_management/serializers/project.py | 10 +- .../serializers/project_milestone.py | 4 +- .../serializers/project_states.py | 4 +- .../serializers/project_type.py | 4 +- .../tests/unit/project/test_project_api_v2.py | 2 +- .../project/test_project_permission_api.py | 2 +- .../unit/project/test_project_viewset.py | 2 +- .../test_project_milestone_api_v2.py | 2 +- .../test_project_milestone_permission_api.py | 2 +- .../test_project_milestone_viewset.py | 2 +- .../test_project_state_api_v2.py | 2 +- .../test_project_state_permission_api.py | 2 +- .../test_project_state_viewset.py | 2 +- .../project_type/test_project_type_api_v2.py | 2 +- .../test_project_type_permission_api.py | 2 +- .../project_type/test_project_type_viewset.py | 2 +- .../unit/test_project_management_viewset.py | 2 +- app/project_management/viewsets/index.py | 2 +- app/settings/serializers/app_settings.py | 4 +- app/settings/serializers/external_links.py | 8 +- app/settings/serializers/user_settings.py | 4 +- .../app_settings/test_app_settings_api_v2.py | 2 +- .../app_settings/test_app_settings_viewset.py | 2 +- .../tests/unit/test_settings_viewset.py | 2 +- .../test_user_settings_api_v2.py | 2 +- .../test_user_settings_viewset.py | 2 +- app/settings/viewsets/index.py | 26 +-- app/templates/base.html.j2 | 2 +- 146 files changed, 510 insertions(+), 461 deletions(-) create mode 100644 app/api/urls_v2.py diff --git a/Release-Notes.md b/Release-Notes.md index cb973f061..e45be88e5 100644 --- a/Release-Notes.md +++ b/Release-Notes.md @@ -12,10 +12,12 @@ API redesign in preparation for moving the UI out of centurion to it's [own proj We are make the above possible by ensuring a more stringent test policy. -- New API will be at path `api/v2` and will remain until v2.0.0 release of Centurion on which the `api/v2` path will be moved to `api` +- New API will be at path `api/v2`. - API v1 is now **Feature frozen** with only bug fixes being completed. It's recommended that you move to and start using API v2 as this has feature parity with API v1. +- API v1 is **depreciated** + - Depreciation of **ALL** API urls. API v1 Will be [removed in v2.0.0](https://github.com/nofusscomputing/centurion_erp/issues/343) release of Centurion. diff --git a/app/access/serializers/organization.py b/app/access/serializers/organization.py index 72855314b..c57c3e30c 100644 --- a/app/access/serializers/organization.py +++ b/app/access/serializers/organization.py @@ -17,7 +17,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_organization-detail", format="html" + view_name="v2:_api_v2_organization-detail", format="html" ) class Meta: @@ -47,8 +47,8 @@ class OrganizationModelSerializer(OrganizationBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_organization-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), - 'teams': reverse("API:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}), + '_self': reverse("v2:_api_v2_organization-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'teams': reverse("v2:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}), } diff --git a/app/access/serializers/team_user.py b/app/access/serializers/team_user.py index 45c97f9e0..e9464a424 100644 --- a/app/access/serializers/team_user.py +++ b/app/access/serializers/team_user.py @@ -22,7 +22,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_organization_team_user-detail", + "v2:_api_v2_organization_team_user-detail", request=self.context['view'].request, kwargs={ 'organization_id': item.team.organization.id, @@ -58,7 +58,7 @@ def get_url(self, item): return { '_self': reverse( - 'API:_api_v2_organization_team_user-detail', + 'v2:_api_v2_organization_team_user-detail', request=self.context['view'].request, kwargs={ 'organization_id': item.team.organization.id, diff --git a/app/access/serializers/teams.py b/app/access/serializers/teams.py index 7bda46b18..42ba59519 100644 --- a/app/access/serializers/teams.py +++ b/app/access/serializers/teams.py @@ -23,7 +23,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_organization_team-detail", + "v2:_api_v2_organization_team-detail", request=self.context['view'].request, kwargs={ 'organization_id': item.organization.id, @@ -61,7 +61,7 @@ def get_url(self, item): return { '_self': reverse( - 'API:_api_v2_organization_team-detail', + 'v2:_api_v2_organization_team-detail', request=self.context['view'].request, kwargs={ 'organization_id': item.organization.id, @@ -69,7 +69,7 @@ def get_url(self, item): } ), 'users': reverse( - 'API:_api_v2_organization_team_user-list', + 'v2:_api_v2_organization_team_user-list', request=self.context['view'].request, kwargs={ 'organization_id': item.organization.id, diff --git a/app/access/tests/unit/organization/test_organizaiton_api.py b/app/access/tests/unit/organization/test_organizaiton_api.py index 904ba2bc1..2f1031990 100644 --- a/app/access/tests/unit/organization/test_organizaiton_api.py +++ b/app/access/tests/unit/organization/test_organizaiton_api.py @@ -17,7 +17,7 @@ class OrganizationAPI(TestCase): model = Organization - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_organization' diff --git a/app/access/tests/unit/organization/test_organizaiton_api_v2.py b/app/access/tests/unit/organization/test_organizaiton_api_v2.py index 60076c46c..be29c4bc2 100644 --- a/app/access/tests/unit/organization/test_organizaiton_api_v2.py +++ b/app/access/tests/unit/organization/test_organizaiton_api_v2.py @@ -21,7 +21,7 @@ class OrganizationAPI( model = Organization - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_organization' diff --git a/app/access/tests/unit/organization/test_organizaiton_permission_api.py b/app/access/tests/unit/organization/test_organizaiton_permission_api.py index dc96f60da..6f3bb582f 100644 --- a/app/access/tests/unit/organization/test_organizaiton_permission_api.py +++ b/app/access/tests/unit/organization/test_organizaiton_permission_api.py @@ -20,7 +20,7 @@ class OrganizationPermissionsAPI(TestCase, APIPermissionChange, APIPermissionVie model_name = 'organization' app_label = 'access' - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_organization' diff --git a/app/access/tests/unit/team/test_team_api.py b/app/access/tests/unit/team/test_team_api.py index ec8039c5a..c8fffe45a 100644 --- a/app/access/tests/unit/team/test_team_api.py +++ b/app/access/tests/unit/team/test_team_api.py @@ -21,7 +21,7 @@ class TeamAPI(TestCase): model = Team - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_team' diff --git a/app/access/tests/unit/team/test_team_api_v2.py b/app/access/tests/unit/team/test_team_api_v2.py index f7b2c0bd4..cd6fe825f 100644 --- a/app/access/tests/unit/team/test_team_api_v2.py +++ b/app/access/tests/unit/team/test_team_api_v2.py @@ -20,7 +20,7 @@ class TeamAPI( model = Team - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_organization_team' diff --git a/app/access/tests/unit/team/test_team_permission_api.py b/app/access/tests/unit/team/test_team_permission_api.py index 1ea0a1081..7345cae23 100644 --- a/app/access/tests/unit/team/test_team_permission_api.py +++ b/app/access/tests/unit/team/test_team_permission_api.py @@ -18,7 +18,7 @@ class TeamPermissionsAPI(TestCase, APIPermissions): model = Team - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_team' diff --git a/app/access/tests/unit/team_user/test_team_user_api_v2.py b/app/access/tests/unit/team_user/test_team_user_api_v2.py index 2b3cca096..634e565ac 100644 --- a/app/access/tests/unit/team_user/test_team_user_api_v2.py +++ b/app/access/tests/unit/team_user/test_team_user_api_v2.py @@ -20,7 +20,7 @@ class TeamUserAPI( model = TeamUsers - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_organization_team_user' diff --git a/app/access/viewsets/index.py b/app/access/viewsets/index.py index c93c9c004..552aaa6c6 100644 --- a/app/access/viewsets/index.py +++ b/app/access/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "organization": reverse('API:_api_v2_organization-list', request=request) + "organization": reverse('v2:_api_v2_organization-list', request=request) } ) diff --git a/app/api/serializers/access.py b/app/api/serializers/access.py index 44eadf19e..e520a5fdd 100644 --- a/app/api/serializers/access.py +++ b/app/api/serializers/access.py @@ -25,7 +25,7 @@ def get_url(self, obj): request = self.context.get('request') - return request.build_absolute_uri(reverse("API:_api_team", args=[obj.organization.id,obj.pk])) + return request.build_absolute_uri(reverse("v1:_api_team", args=[obj.organization.id,obj.pk])) @@ -48,7 +48,7 @@ def get_url(self, obj): team = Team.objects.get(pk=obj.id) - return request.build_absolute_uri(reverse('API:_api_team_permission', args=[team.organization_id,team.id])) + return request.build_absolute_uri(reverse('v1:_api_team_permission', args=[team.organization_id,team.id])) def validate(self, data): @@ -67,7 +67,7 @@ def team_url(self, obj): request = self.context.get('request') - return request.build_absolute_uri(reverse('API:_api_team', args=[obj.organization_id,obj.id])) + return request.build_absolute_uri(reverse('v1:_api_team', args=[obj.organization_id,obj.id])) class Meta: @@ -93,7 +93,7 @@ class Meta: class OrganizationListSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_organization", format="html" + view_name="v1:_api_organization", format="html" ) @@ -110,7 +110,7 @@ class Meta: class OrganizationSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_organization", format="html" + view_name="v1:_api_organization", format="html" ) team_url = serializers.SerializerMethodField('get_url') @@ -121,11 +121,11 @@ def get_url(self, obj): team = Team.objects.filter(pk=obj.id) - return request.build_absolute_uri(reverse('API:_api_organization_teams', args=[obj.id])) + return request.build_absolute_uri(reverse('v1:_api_organization_teams', args=[obj.id])) teams = TeamSerializer(source='team_set', many=True, read_only=False) - view_name="API:_api_organization" + view_name="v1:_api_organization" class Meta: diff --git a/app/api/serializers/config.py b/app/api/serializers/config.py index 84b74325f..b15bce927 100644 --- a/app/api/serializers/config.py +++ b/app/api/serializers/config.py @@ -28,7 +28,7 @@ def get_url(self, obj): request = self.context.get('request') - return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk])) + return request.build_absolute_uri(reverse("v1:_api_config_group", args=[obj.pk])) @@ -59,7 +59,7 @@ def get_url(self, obj): request = self.context.get('request') - return request.build_absolute_uri(reverse("API:_api_config_group", args=[obj.pk])) + return request.build_absolute_uri(reverse("v1:_api_config_group", args=[obj.pk])) diff --git a/app/api/serializers/core/ticket.py b/app/api/serializers/core/ticket.py index a855fa1a5..877f6f040 100644 --- a/app/api/serializers/core/ticket.py +++ b/app/api/serializers/core/ticket.py @@ -54,7 +54,7 @@ def get_url_ticket(self, item): return request.build_absolute_uri( reverse( - 'API:' + view_name + '-detail', + 'v1:' + view_name + '-detail', kwargs = kwargs ) ) @@ -100,7 +100,7 @@ def get_url_ticket_comments(self, item): return request.build_absolute_uri( reverse( - 'API:' + view_name + '-list', + 'v1:' + view_name + '-list', kwargs = kwargs ) ) diff --git a/app/api/serializers/core/ticket_category.py b/app/api/serializers/core/ticket_category.py index fc1517663..5c14ce62c 100644 --- a/app/api/serializers/core/ticket_category.py +++ b/app/api/serializers/core/ticket_category.py @@ -15,7 +15,7 @@ class TicketCategorySerializer( ): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_ticket_category-detail", format="html" + view_name="v1:_api_ticket_category-detail", format="html" ) diff --git a/app/api/serializers/core/ticket_comment.py b/app/api/serializers/core/ticket_comment.py index 4a21ea396..4ba36740f 100644 --- a/app/api/serializers/core/ticket_comment.py +++ b/app/api/serializers/core/ticket_comment.py @@ -38,7 +38,7 @@ def get_url_ticket_comment(self, item): return request.build_absolute_uri( - reverse('API:' + view_name + '-detail', + reverse('v1:' + view_name + '-detail', kwargs={ 'ticket_id': item.ticket.id, 'pk': item.id diff --git a/app/api/serializers/core/ticket_comment_category.py b/app/api/serializers/core/ticket_comment_category.py index dbb03d6d6..82a51c210 100644 --- a/app/api/serializers/core/ticket_comment_category.py +++ b/app/api/serializers/core/ticket_comment_category.py @@ -13,7 +13,7 @@ class TicketCommentCategorySerializer( ): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_ticket_comment_category-detail", format="html" + view_name="v1:_api_ticket_comment_category-detail", format="html" ) diff --git a/app/api/serializers/itam/device.py b/app/api/serializers/itam/device.py index 3c0760ecb..d3ce38cd9 100644 --- a/app/api/serializers/itam/device.py +++ b/app/api/serializers/itam/device.py @@ -13,7 +13,7 @@ class DeviceConfigGroupsSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_config_group", format="html" + view_name="v1:_api_config_group", format="html" ) class Meta: @@ -36,7 +36,7 @@ class Meta: class DeviceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( - view_name="API:device-detail", format="html" + view_name="v1:device-detail", format="html" ) config = serializers.SerializerMethodField('get_device_config') @@ -46,7 +46,7 @@ class DeviceSerializer(serializers.ModelSerializer): def get_device_config(self, device): request = self.context.get('request') - return request.build_absolute_uri(reverse('API:_api_device_config', args=[device.slug])) + return request.build_absolute_uri(reverse('v1:_api_device_config', args=[device.slug])) class Meta: diff --git a/app/api/serializers/itam/software.py b/app/api/serializers/itam/software.py index f3e2d430a..681cfd8df 100644 --- a/app/api/serializers/itam/software.py +++ b/app/api/serializers/itam/software.py @@ -7,7 +7,7 @@ class SoftwareSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( - view_name="API:software-detail", format="html" + view_name="v1:software-detail", format="html" ) class Meta: diff --git a/app/api/serializers/project_management/project_milestone.py b/app/api/serializers/project_management/project_milestone.py index 5d1de5f9e..f95178f32 100644 --- a/app/api/serializers/project_management/project_milestone.py +++ b/app/api/serializers/project_management/project_milestone.py @@ -19,7 +19,7 @@ def get_url_project_milestone(self, item): request = self.context.get('request') return request.build_absolute_uri( - reverse('API:_api_project_milestone-detail', + reverse('v1:_api_project_milestone-detail', kwargs={ 'project_id': item.project.id, 'pk': item.id diff --git a/app/api/serializers/project_management/project_state.py b/app/api/serializers/project_management/project_state.py index 5789c201e..e1c96184a 100644 --- a/app/api/serializers/project_management/project_state.py +++ b/app/api/serializers/project_management/project_state.py @@ -12,7 +12,7 @@ class ProjectStateSerializer( ): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_project_state-detail", format="html" + view_name="v1:_api_project_state-detail", format="html" ) diff --git a/app/api/serializers/project_management/project_type.py b/app/api/serializers/project_management/project_type.py index 5bbd90772..55101baa6 100644 --- a/app/api/serializers/project_management/project_type.py +++ b/app/api/serializers/project_management/project_type.py @@ -12,7 +12,7 @@ class ProjectTypeSerializer( ): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_project_state-detail", format="html" + view_name="v1:_api_project_state-detail", format="html" ) diff --git a/app/api/serializers/project_management/projects.py b/app/api/serializers/project_management/projects.py index 1e4c0b4b2..2250897fb 100644 --- a/app/api/serializers/project_management/projects.py +++ b/app/api/serializers/project_management/projects.py @@ -22,7 +22,7 @@ def get_url(self, item): request = self.context.get('request') - return request.build_absolute_uri(reverse("API:_api_projects-detail", args=[item.pk])) + return request.build_absolute_uri(reverse("v1:_api_projects-detail", args=[item.pk])) project_tasks_url = serializers.SerializerMethodField('get_url_project_tasks') @@ -34,7 +34,7 @@ def get_url_project_tasks(self, item): return request.build_absolute_uri( reverse( - 'API:_api_project_tasks-list', + 'v1:_api_project_tasks-list', kwargs={ 'project_id': item.id } @@ -50,7 +50,7 @@ def get_url_project_milestone(self, item): return request.build_absolute_uri( reverse( - 'API:_api_project_milestone-list', + 'v1:_api_project_milestone-list', kwargs={ 'project_id': item.id } diff --git a/app/api/tests/unit/inventory/test_api_inventory.py b/app/api/tests/unit/inventory/test_api_inventory.py index c9edf985e..5490d72ae 100644 --- a/app/api/tests/unit/inventory/test_api_inventory.py +++ b/app/api/tests/unit/inventory/test_api_inventory.py @@ -160,7 +160,7 @@ def test_inventory_function_called_permission_check(self, permission_check): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) response = client.post(url, data=self.inventory, content_type='application/json') @@ -182,7 +182,7 @@ def test_inventory_serializer_inventory_called(self, serializer): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) response = client.post(url, data=self.inventory, content_type='application/json') @@ -201,7 +201,7 @@ def test_inventory_serializer_inventory_details_called(self, serializer): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) response = client.post(url, data=self.inventory, content_type='application/json') @@ -220,7 +220,7 @@ def test_inventory_serializer_inventory_operating_system_called(self, serializer """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) response = client.post(url, data=self.inventory, content_type='application/json') @@ -239,7 +239,7 @@ def test_inventory_serializer_inventory_software_called(self, serializer): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) response = client.post(url, data=self.inventory, content_type='application/json') @@ -395,7 +395,7 @@ def test_api_inventory_valid_status_ok_existing_device(self): """ Successful inventory upload returns 200 for existing device""" client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) response = client.post(url, data=self.inventory, content_type='application/json') @@ -409,7 +409,7 @@ def test_api_inventory_invalid_status_bad_request(self): """ Incorrectly formated inventory upload returns 400 """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') mod_inventory = self.inventory.copy() diff --git a/app/api/tests/unit/inventory/test_inventory_permission_api.py b/app/api/tests/unit/inventory/test_inventory_permission_api.py index b5b45f649..ec2a910bf 100644 --- a/app/api/tests/unit/inventory/test_inventory_permission_api.py +++ b/app/api/tests/unit/inventory/test_inventory_permission_api.py @@ -201,7 +201,7 @@ def test_device_auth_add_user_anon_denied(self): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') response = client.put(url, data=self.inventory, content_type='application/json') @@ -218,7 +218,7 @@ def test_device_auth_add_no_permission_denied(self): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.no_permissions_user) @@ -236,7 +236,7 @@ def test_device_auth_add_different_organization_denied(self): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.different_organization_user) @@ -254,7 +254,7 @@ def test_device_auth_add_permission_view_denied(self): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.view_user) @@ -272,7 +272,7 @@ def test_device_auth_add_has_permission(self): """ client = Client() - url = reverse('API:_api_device_inventory') + url = reverse('v1:_api_device_inventory') client.force_login(self.add_user) diff --git a/app/api/urls.py b/app/api/urls.py index 1b38c4f26..a484561c7 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -29,81 +29,6 @@ from .views.itam import inventory -from api.viewsets import ( - index as v2 -) - -from app.viewsets.base import ( - index as base_index_v2, - content_type as content_type_v2, - permisson as permission_v2, - user as user_v2 -) - -from access.viewsets import ( - index as access_v2, - organization as organization_v2, - team as team_v2, - team_user as team_user_v2 -) - -from assistance.viewsets import ( - index as assistance_index_v2, - knowledge_base as knowledge_base_v2, - knowledge_base_category as knowledge_base_category_v2 -) - -from config_management.viewsets import ( - index as config_management_v2, - config_group as config_group_v2, - config_group_software as config_group_software_v2 -) - -from core.viewsets import ( - history as history_v2, - notes as notes_v2, - manufacturer as manufacturer_v2, - celery_log as celery_log_v2 -) - -from itam.viewsets import ( - index as itam_index_v2, - device as device_v2, - device_model as device_model_v2, - device_type as device_type_v2, - device_software as device_software_v2, - operating_system as operating_system_v2, - operating_system_version as operating_system_version_v2, - software as software_v2, - software_category as software_category_v2, - software_version as software_version_v2, -) - -from itim.viewsets import ( - index as itim_v2, - cluster as cluster_v2, - cluster_type as cluster_type_v2, - port as port_v2, - service as service_v2, - service_device as service_device_v2 -) - -from project_management.viewsets import ( - index as project_management_v2, - project as project_v2, - project_milestone as project_milestone_v2, - project_state as project_state_v2, - project_type as project_type_v2, -) - -from settings.viewsets import ( - app_settings as app_settings_v2, - external_link as external_link_v2, - index as settings_index_v2, - user_settings as user_settings_v2 -) - - app_name = "API" @@ -139,81 +64,10 @@ router.register('software', software.SoftwareViewSet, basename='software') -# API V2 -router.register('v2', v2.Index, basename='_api_v2_home') - -router.register('v2/access', access_v2.Index, basename='_api_v2_access_home') -router.register('v2/access/organization', organization_v2.ViewSet, basename='_api_v2_organization') -router.register('v2/access/organization/(?P[0-9]+)/team', team_v2.ViewSet, basename='_api_v2_organization_team') -router.register('v2/access/organization/(?P[0-9]+)/team/(?P[0-9]+)/user', team_user_v2.ViewSet, basename='_api_v2_organization_team_user') - -router.register('v2/assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') -router.register('v2/assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base') - -router.register('v2/base', base_index_v2.Index, basename='_api_v2_base_home') -router.register('v2/base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') -router.register('v2/base/permission', permission_v2.ViewSet, basename='_api_v2_permission') -router.register('v2/base/user', user_v2.ViewSet, basename='_api_v2_user') - -router.register('v2/config_management', config_management_v2.Index, basename='_api_v2_config_management_home') -router.register('v2/config_management/group', config_group_v2.ViewSet, basename='_api_v2_config_group') -router.register('v2/config_management/group/(?P[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child') -router.register('v2/config_management/group/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_config_group_notes') -router.register('v2/config_management/group/(?P[0-9]+)/software', config_group_software_v2.ViewSet, basename='_api_v2_config_group_software') - -router.register('v2/core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') - -router.register('v2/itam', itam_index_v2.Index, basename='_api_v2_itam_home') -router.register('v2/itam/device', device_v2.ViewSet, basename='_api_v2_device') -router.register('v2/itam/device/(?P[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software') -router.register('v2/itam/device/(?P[0-9]+)/service', service_device_v2.ViewSet, basename='_api_v2_service_device') -router.register('v2/itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') -router.register('v2/itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system') -router.register('v2/itam/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') -router.register('v2/itam/operating_system/(?P[0-9]+)/version', operating_system_version_v2.ViewSet, basename='_api_v2_operating_system_version') -router.register('v2/itam/software', software_v2.ViewSet, basename='_api_v2_software') -router.register('v2/itam/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') -router.register('v2/itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') - -router.register('v2/itim', itim_v2.Index, basename='_api_v2_itim_home') -router.register('v2/itim/cluster', cluster_v2.ViewSet, basename='_api_v2_cluster') -router.register('v2/itim/cluster/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_notes') -router.register('v2/itim/service', service_v2.ViewSet, basename='_api_v2_service') -router.register('v2/itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') - -router.register('v2/project_management', project_management_v2.Index, basename='_api_v2_project_management_home') -router.register('v2/project_management/project', project_v2.ViewSet, basename='_api_v2_project') -router.register('v2/project_management/project/(?P[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone') -router.register('v2/itim/project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') - -router.register('v2/settings', settings_index_v2.Index, basename='_api_v2_settings_home') -router.register('v2/settings/app_settings', app_settings_v2.ViewSet, basename='_api_v2_app_settings') -router.register('v2/settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') -router.register('v2/settings/cluster_type/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_type_notes') -router.register('v2/settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') -router.register('v2/settings/device_type', device_type_v2.ViewSet, basename='_api_v2_device_type') -router.register('v2/settings/external_link', external_link_v2.ViewSet, basename='_api_v2_external_link') -router.register('v2/settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') -router.register('v2/settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') -router.register('v2/settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') -router.register('v2/settings/port', port_v2.ViewSet, basename='_api_v2_port') -router.register('v2/settings/port/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_port_notes') -router.register('v2/settings/project_state', project_state_v2.ViewSet, basename='_api_v2_project_state') -router.register('v2/settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type') -router.register('v2/settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') -router.register('v2/settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') - - -router.register('v2/settings/celery_log', celery_log_v2.ViewSet, basename='_api_v2_celery_log') - urlpatterns = [ path("assistance", assistance.index.Index.as_view(), name="_api_assistance"), - # - # Sof Old Paths to be refactored - # - path("config//", itam_config.View.as_view(), name="_api_device_config"), path("configuration/", config.ConfigGroupsList.as_view(), name='_api_config_groups'), diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py new file mode 100644 index 000000000..d88ca648c --- /dev/null +++ b/app/api/urls_v2.py @@ -0,0 +1,167 @@ +from django.urls import path + +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + +from rest_framework.routers import DefaultRouter + +from api.viewsets import ( + index as v2 +) + +from app.viewsets.base import ( + index as base_index_v2, + content_type as content_type_v2, + permisson as permission_v2, + user as user_v2 +) + +from access.viewsets import ( + index as access_v2, + organization as organization_v2, + team as team_v2, + team_user as team_user_v2 +) + +from assistance.viewsets import ( + index as assistance_index_v2, + knowledge_base as knowledge_base_v2, + knowledge_base_category as knowledge_base_category_v2, +) + +from config_management.viewsets import ( + index as config_management_v2, + config_group as config_group_v2, + config_group_software as config_group_software_v2 +) + +from core.viewsets import ( + history as history_v2, + notes as notes_v2, + manufacturer as manufacturer_v2, + celery_log as celery_log_v2 +) + +from itam.viewsets import ( + index as itam_index_v2, + device as device_v2, + device_model as device_model_v2, + device_type as device_type_v2, + device_software as device_software_v2, + operating_system as operating_system_v2, + operating_system_version as operating_system_version_v2, + software as software_v2, + software_category as software_category_v2, + software_version as software_version_v2, +) + +from itim.viewsets import ( + index as itim_v2, + cluster as cluster_v2, + cluster_type as cluster_type_v2, + port as port_v2, + service as service_v2, + service_device as service_device_v2 +) + +from project_management.viewsets import ( + index as project_management_v2, + project as project_v2, + project_milestone as project_milestone_v2, + project_state as project_state_v2, + project_type as project_type_v2, +) + +from settings.viewsets import ( + app_settings as app_settings_v2, + external_link as external_link_v2, + index as settings_index_v2, + user_settings as user_settings_v2 +) + +app_name = "API" + + +router = DefaultRouter(trailing_slash=False) + + +router.register('', v2.Index, basename='_api_v2_home') + +router.register('access', access_v2.Index, basename='_api_v2_access_home') +router.register('access/organization', organization_v2.ViewSet, basename='_api_v2_organization') +router.register('access/organization/(?P[0-9]+)/team', team_v2.ViewSet, basename='_api_v2_organization_team') +router.register('access/organization/(?P[0-9]+)/team/(?P[0-9]+)/user', team_user_v2.ViewSet, basename='_api_v2_organization_team_user') + + +router.register('assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') +router.register('assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base') + + +router.register('base', base_index_v2.Index, basename='_api_v2_base_home') +router.register('base/content_type', content_type_v2.ViewSet, basename='_api_v2_content_type') +router.register('base/permission', permission_v2.ViewSet, basename='_api_v2_permission') +router.register('base/user', user_v2.ViewSet, basename='_api_v2_user') + + +router.register('config_management', config_management_v2.Index, basename='_api_v2_config_management_home') +router.register('config_management/group', config_group_v2.ViewSet, basename='_api_v2_config_group') +router.register('config_management/group/(?P[0-9]+)/child_group', config_group_v2.ViewSet, basename='_api_v2_config_group_child') +router.register('config_management/group/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_config_group_notes') +router.register('config_management/group/(?P[0-9]+)/software', config_group_software_v2.ViewSet, basename='_api_v2_config_group_software') + + +router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') + + +router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home') +router.register('itam/device', device_v2.ViewSet, basename='_api_v2_device') +router.register('itam/device/(?P[0-9]+)/software', device_software_v2.ViewSet, basename='_api_v2_device_software') +router.register('itam/device/(?P[0-9]+)/service', service_device_v2.ViewSet, basename='_api_v2_service_device') +router.register('itam/device/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_device_notes') +router.register('itam/operating_system', operating_system_v2.ViewSet, basename='_api_v2_operating_system') +router.register('itam/operating_system/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_operating_system_notes') +router.register('itam/operating_system/(?P[0-9]+)/version', operating_system_version_v2.ViewSet, basename='_api_v2_operating_system_version') +router.register('itam/software', software_v2.ViewSet, basename='_api_v2_software') +router.register('itam/software/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_software_notes') +router.register('itam/software/(?P[0-9]+)/version', software_version_v2.ViewSet, basename='_api_v2_software_version') + + +router.register('itim', itim_v2.Index, basename='_api_v2_itim_home') +router.register('itim/cluster', cluster_v2.ViewSet, basename='_api_v2_cluster') +router.register('itim/cluster/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_notes') +router.register('itim/service', service_v2.ViewSet, basename='_api_v2_service') +router.register('itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') + + +router.register('project_management', project_management_v2.Index, basename='_api_v2_project_management_home') +router.register('project_management/project', project_v2.ViewSet, basename='_api_v2_project') +router.register('project_management/project/(?P[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone') +router.register('itim/project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') + + +router.register('settings', settings_index_v2.Index, basename='_api_v2_settings_home') +router.register('settings/app_settings', app_settings_v2.ViewSet, basename='_api_v2_app_settings') +router.register('settings/celery_log', celery_log_v2.ViewSet, basename='_api_v2_celery_log') +router.register('settings/cluster_type', cluster_type_v2.ViewSet, basename='_api_v2_cluster_type') +router.register('settings/cluster_type/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_type_notes') +router.register('settings/device_model', device_model_v2.ViewSet, basename='_api_v2_device_model') +router.register('settings/device_type', device_type_v2.ViewSet, basename='_api_v2_device_type') +router.register('settings/external_link', external_link_v2.ViewSet, basename='_api_v2_external_link') +router.register('settings/knowledge_base_category', knowledge_base_category_v2.ViewSet, basename='_api_v2_knowledge_base_category') +router.register('settings/manufacturer', manufacturer_v2.ViewSet, basename='_api_v2_manufacturer') +router.register('settings/manufacturer/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_manufacturer_notes') +router.register('settings/port', port_v2.ViewSet, basename='_api_v2_port') +router.register('settings/port/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_port_notes') +router.register('settings/project_state', project_state_v2.ViewSet, basename='_api_v2_project_state') +router.register('settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type') +router.register('settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') +router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') + + +urlpatterns = [ + + path('schema', SpectacularAPIView.as_view(api_version='v2'), name='schema-v2',), + path('docs', SpectacularSwaggerView.as_view(url_name='schema-v2'), name='_api_v2_docs'), + +] + +urlpatterns += router.urls diff --git a/app/api/views/assistance/index.py b/app/api/views/assistance/index.py index 7099652bc..983af1bd2 100644 --- a/app/api/views/assistance/index.py +++ b/app/api/views/assistance/index.py @@ -29,7 +29,7 @@ def get_view_description(self, html=False) -> str: def get(self, request, *args, **kwargs): body: dict = { - 'requests': reverse('API:_api_assistance_request-list', request=request) + 'requests': reverse('v1:_api_assistance_request-list', request=request) } return Response(body) diff --git a/app/api/views/index.py b/app/api/views/index.py index be6f45063..605873590 100644 --- a/app/api/views/index.py +++ b/app/api/views/index.py @@ -18,10 +18,10 @@ class Index(viewsets.ViewSet): def get_view_name(self): - return "API Index" + return "API" def get_view_description(self, html=False) -> str: - text = "My REST API" + text = "Centurion ERP Rest API" if html: return mark_safe(f"

{text}

") else: @@ -32,15 +32,15 @@ def list(self, request, pk=None): API: dict = { # "teams": reverse("_api_teams", request=request), - 'assistance': reverse("API:_api_assistance", request=request), - "devices": reverse("API:device-list", request=request), - "config_groups": reverse("API:_api_config_groups", request=request), - 'itim': reverse("API:_api_itim", request=request), - "organizations": reverse("API:_api_orgs", request=request), - 'project_management': reverse("API:_api_project_management", request=request), - "settings": reverse('API:_settings', request=request), - "software": reverse("API:software-list", request=request), - 'v2': reverse("API:_api_v2_home-list", request=request) + 'assistance': reverse("v1:_api_assistance", request=request), + "devices": reverse("v1:device-list", request=request), + "config_groups": reverse("v1:_api_config_groups", request=request), + 'itim': reverse("v1:_api_itim", request=request), + "organizations": reverse("v1:_api_orgs", request=request), + 'project_management': reverse("v1:_api_project_management", request=request), + "settings": reverse('v1:_settings', request=request), + "software": reverse("v1:software-list", request=request), + 'v2': reverse("v2:_api_v2_home-list", request=request) } return Response( API ) diff --git a/app/api/views/itim/index.py b/app/api/views/itim/index.py index a82b16882..feb70477a 100644 --- a/app/api/views/itim/index.py +++ b/app/api/views/itim/index.py @@ -28,9 +28,9 @@ def get_view_description(self, html=False) -> str: def get(self, request, *args, **kwargs): body: dict = { - 'changes': reverse('API:_api_itim_change-list', request=request), - 'incidents': reverse('API:_api_itim_incident-list', request=request), - 'problems': reverse('API:_api_itim_problem-list', request=request), + 'changes': reverse('v1:_api_itim_change-list', request=request), + 'incidents': reverse('v1:_api_itim_incident-list', request=request), + 'problems': reverse('v1:_api_itim_problem-list', request=request), } return Response(body) diff --git a/app/api/views/project_management/index.py b/app/api/views/project_management/index.py index 06d04255e..ed423c959 100644 --- a/app/api/views/project_management/index.py +++ b/app/api/views/project_management/index.py @@ -30,7 +30,7 @@ def get_view_description(self, html=False) -> str: def get(self, request, *args, **kwargs): body: dict = { - 'projects': reverse('API:_api_projects-list', request=request) + 'projects': reverse('v1:_api_projects-list', request=request) } return Response(body) diff --git a/app/api/views/settings/index.py b/app/api/views/settings/index.py index 2de2a4f15..c366add7e 100644 --- a/app/api/views/settings/index.py +++ b/app/api/views/settings/index.py @@ -37,11 +37,11 @@ def get(self, request, *args, **kwargs): status = Http.Status.OK response_data: dict = { - "permissions": reverse('API:_settings_permissions', request=request), - "project_state": reverse('API:_api_project_state-list', request=request), - "project_type": reverse('API:_api_project_type-list', request=request), - "ticket_categories": reverse('API:_api_ticket_category-list', request=request), - "ticket_comment_categories": reverse('API:_api_ticket_comment_category-list', request=request) + "permissions": reverse('v1:_settings_permissions', request=request), + "project_state": reverse('v1:_api_project_state-list', request=request), + "project_type": reverse('v1:_api_project_type-list', request=request), + "ticket_categories": reverse('v1:_api_ticket_category-list', request=request), + "ticket_comment_categories": reverse('v1:_api_ticket_comment_category-list', request=request) } return Response(data=response_data,status=status) diff --git a/app/api/viewsets/index.py b/app/api/viewsets/index.py index a739d138b..998b04e34 100644 --- a/app/api/viewsets/index.py +++ b/app/api/viewsets/index.py @@ -16,26 +16,23 @@ class Index(CommonViewSet): 'OPTIONS' ] - view_description = """Centurion ERP API V2. + view_description = 'Centurion ERP API V2.' - This endpoint will move to path `/api/` on release of - v2.0.0 of Centurion ERP. - """ - - view_name = "API v2" + view_name = "v2" def list(self, request, *args, **kwargs): return Response( { - "access": reverse('API:_api_v2_access_home-list', request=request), - "assistance": reverse('API:_api_v2_assistance_home-list', request=request), - "base": reverse('API:_api_v2_base_home-list', request=request), - "itam": reverse('API:_api_v2_itam_home-list', request=request), - "itim": reverse('API:_api_v2_itim_home-list', request=request), - "config_management": reverse('API:_api_v2_config_management_home-list', request=request), - "project_management": reverse('API:_api_v2_project_management_home-list', request=request), - "settings": reverse('API:_api_v2_settings_home-list', request=request) + "access": reverse('v2:_api_v2_access_home-list', request=request), + "assistance": reverse('v2:_api_v2_assistance_home-list', request=request), + "docs": reverse('v2:_api_v2_docs', request=request), + "base": reverse('v2:_api_v2_base_home-list', request=request), + "itam": reverse('v2:_api_v2_itam_home-list', request=request), + "itim": reverse('v2:_api_v2_itim_home-list', request=request), + "config_management": reverse('v2:_api_v2_config_management_home-list', request=request), + "project_management": reverse('v2:_api_v2_project_management_home-list', request=request), + "settings": reverse('v2:_api_v2_settings_home-list', request=request) } ) diff --git a/app/app/serializers/content_type.py b/app/app/serializers/content_type.py index 79ae32dee..ae846077e 100644 --- a/app/app/serializers/content_type.py +++ b/app/app/serializers/content_type.py @@ -15,7 +15,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_content_type-detail", format="html" + view_name="v2:_api_v2_content_type-detail", format="html" ) class Meta: @@ -46,7 +46,7 @@ class ContentTypeViewSerializer(ContentTypeBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_content_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_content_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), } diff --git a/app/app/serializers/permission.py b/app/app/serializers/permission.py index 8b110bca3..eb8d45e7c 100644 --- a/app/app/serializers/permission.py +++ b/app/app/serializers/permission.py @@ -16,7 +16,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_permission-detail", format="html" + view_name="v2:_api_v2_permission-detail", format="html" ) class Meta: @@ -49,7 +49,7 @@ class PermissionViewSerializer(PermissionBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_permission-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_permission-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), } diff --git a/app/app/serializers/user.py b/app/app/serializers/user.py index 23a959b9a..b21b3387b 100644 --- a/app/app/serializers/user.py +++ b/app/app/serializers/user.py @@ -14,7 +14,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_user-detail", format="html" + view_name="v2:_api_v2_user-detail", format="html" ) class Meta: diff --git a/app/app/settings.py b/app/app/settings.py index cad22346d..7680eb7be 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -274,12 +274,19 @@ # 'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json' 'TEST_REQUEST_DEFAULT_FORMAT': 'json', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', + 'DEFAULT_VERSION': 'v1', + 'ALLOWED_VERSIONS': [ + 'v1', + 'v2' + ] } SPECTACULAR_SETTINGS = { - 'TITLE': 'ITSM API', - 'DESCRIPTION': """This UI is intended to serve as the API documentation. + 'TITLE': 'Centurion ERP API', + 'DESCRIPTION': """This UI exists to server the purpose of being the API documentation. +Centurion ERP's API is versioned, with [v1 Depreciated](/api/swagger) and [v2 as the current](/api/v2/docs). ## Authentication Access to the API is restricted and requires authentication. Available authentication methods are: @@ -308,10 +315,9 @@ ``` """, - 'VERSION': '1.0.0', + 'VERSION': '', 'SCHEMA_PATH_PREFIX': '/api/v2/|/api/', 'SERVE_INCLUDE_SCHEMA': False, - 'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', 'REDOC_DIST': 'SIDECAR', diff --git a/app/app/urls.py b/app/app/urls.py index 46bc92a9a..94446dd47 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -66,11 +66,15 @@ if settings.API_ENABLED: + urlpatterns += [ - path("api/", include("api.urls")), - path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path("api/", include("api.urls", namespace = 'v1')), + path('api/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'), path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + + path("api/v2/", include("api.urls_v2", namespace = 'v2')), + ] diff --git a/app/app/viewsets/base/index.py b/app/app/viewsets/base/index.py index 7cfda34cb..b2b47294b 100644 --- a/app/app/viewsets/base/index.py +++ b/app/app/viewsets/base/index.py @@ -24,8 +24,8 @@ def list(self, request, pk=None): return Response( { - "content_type": reverse('API:_api_v2_content_type-list', request=request), - "permission": reverse('API:_api_v2_permission-list', request=request), - "user": reverse('API:_api_v2_user-list', request=request) + "content_type": reverse('v2:_api_v2_content_type-list', request=request), + "permission": reverse('v2:_api_v2_permission-list', request=request), + "user": reverse('v2:_api_v2_user-list', request=request) } ) diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py index 71d4ff952..4d52558eb 100644 --- a/app/assistance/serializers/knowledge_base.py +++ b/app/assistance/serializers/knowledge_base.py @@ -26,7 +26,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_knowledge_base-detail", + "v2:_api_v2_knowledge_base-detail", request=self.context['view'].request, kwargs={ 'pk': item.pk @@ -63,29 +63,29 @@ def get_url(self, item): return { '_self': reverse( - 'API:_api_v2_knowledge_base-detail', + 'v2:_api_v2_knowledge_base-detail', request=self.context['view'].request, kwargs={ 'pk': item.pk } ), 'category': reverse( - 'API:_api_v2_knowledge_base_category-list', + 'v2:_api_v2_knowledge_base_category-list', request=self.context['view'].request, ), 'organization': reverse( - 'API:_api_v2_organization-list', + 'v2:_api_v2_organization-list', request=self.context['view'].request, ), 'team': reverse( - 'API:_api_v2_organization_team-list', + 'v2:_api_v2_organization_team-list', request=self.context['view'].request, kwargs={ 'organization_id': item.organization.id, } ), 'user': reverse( - 'API:_api_v2_user-list', + 'v2:_api_v2_user-list', request=self.context['view'].request, ) } diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py index 9519a2a31..ab5b976df 100644 --- a/app/assistance/serializers/knowledge_base_category.py +++ b/app/assistance/serializers/knowledge_base_category.py @@ -27,7 +27,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_knowledge_base_category-detail", + "v2:_api_v2_knowledge_base_category-detail", request=self.context['view'].request, kwargs={ 'pk': item.pk @@ -64,25 +64,25 @@ def get_url(self, item): return { '_self': reverse( - 'API:_api_v2_knowledge_base_category-detail', + 'v2:_api_v2_knowledge_base_category-detail', request=self.context['view'].request, kwargs={ 'pk': item.pk } ), 'organization': reverse( - 'API:_api_v2_organization-list', + 'v2:_api_v2_organization-list', request=self.context['view'].request, ), 'team': reverse( - 'API:_api_v2_organization_team-list', + 'v2:_api_v2_organization_team-list', request=self.context['view'].request, kwargs={ 'organization_id': item.organization.id, } ), 'user': reverse( - 'API:_api_v2_user-list', + 'v2:_api_v2_user-list', request=self.context['view'].request, ) } diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py index 8dfb98800..1f9f7b47f 100644 --- a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_api_v2.py @@ -21,7 +21,7 @@ class KnowledgeBaseAPI( model = KnowledgeBase - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_knowledge_base' diff --git a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py index 920f3e71c..2769c8999 100644 --- a/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py +++ b/app/assistance/tests/unit/knowledge_base/test_knowledge_base_viewset.py @@ -20,7 +20,7 @@ class KnowledgeBasePermissionsAPI(TestCase, APIPermissions): model = KnowledgeBase - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_knowledge_base' diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py index 649a92f03..5302225c6 100644 --- a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_api_v2.py @@ -21,7 +21,7 @@ class KnowledgeBaseCategoryAPI( model = KnowledgeBaseCategory - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_knowledge_base_category' diff --git a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py index cbf0e9cb5..4e196efb8 100644 --- a/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py +++ b/app/assistance/tests/unit/knowledge_base_category/test_knowledge_base_category_viewset.py @@ -16,7 +16,7 @@ class KnowledgeBaseCategoryPermissionsAPI(TestCase, APIPermissions): model = KnowledgeBaseCategory - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_knowledge_base_category' diff --git a/app/assistance/tests/unit/test_assistance_viewset.py b/app/assistance/tests/unit/test_assistance_viewset.py index 6b3b441d5..cb64ad27f 100644 --- a/app/assistance/tests/unit/test_assistance_viewset.py +++ b/app/assistance/tests/unit/test_assistance_viewset.py @@ -16,7 +16,7 @@ class AssistanceViewset( viewset = Index - route_name = 'API:_api_v2_assistance_home' + route_name = 'v2:_api_v2_assistance_home' @classmethod diff --git a/app/assistance/viewsets/index.py b/app/assistance/viewsets/index.py index 264dd650b..f2fd76ce8 100644 --- a/app/assistance/viewsets/index.py +++ b/app/assistance/viewsets/index.py @@ -25,7 +25,7 @@ def list(self, request, pk=None): return Response( { - "knowledge_base": reverse('API:_api_v2_knowledge_base-list', request=request), - "request": "ToDo" + "knowledge_base": reverse('v2:_api_v2_knowledge_base-list', request=request), + "request": reverse('v2:_api_v2_ticket_request-list', request=request), } ) diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 11dac2616..481f798a4 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -22,7 +22,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_config_group-detail", + "v2:_api_v2_config_group-detail", request=self.context['view'].request, kwargs={ 'pk': item.pk @@ -59,41 +59,41 @@ def get_url(self, item): return { '_self': reverse( - 'API:_api_v2_config_group-detail', + 'v2:_api_v2_config_group-detail', request = self.context['view'].request, kwargs = { 'pk': item.pk } ), 'child_groups': reverse( - 'API:_api_v2_config_group_child-list', + 'v2:_api_v2_config_group_child-list', request = self.context['view'].request, kwargs = { 'parent_group': item.pk } ), 'configgroups': reverse( - 'API:_api_v2_config_group-list', + 'v2:_api_v2_config_group-list', request = self.context['view'].request, ), 'group_software': reverse( - 'API:_api_v2_config_group_software-list', + 'v2:_api_v2_config_group_software-list', request=self.context['view'].request, kwargs = { 'group_id': item.pk } ), 'notes': reverse( - "API:_api_v2_config_group_notes-list", + "v2:_api_v2_config_group_notes-list", request=self._context['view'].request, kwargs={'group_id': item.pk} ), 'organization': reverse( - 'API:_api_v2_organization-list', + 'v2:_api_v2_organization-list', request=self.context['view'].request, ), 'parent': reverse( - 'API:_api_v2_config_group-list', + 'v2:_api_v2_config_group-list', request=self.context['view'].request, ), } diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py index bef851c63..f2c6b7d5b 100644 --- a/app/config_management/serializers/config_group_software.py +++ b/app/config_management/serializers/config_group_software.py @@ -26,7 +26,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_config_group-detail", + "v2:_api_v2_config_group-detail", request=self.context['view'].request, kwargs={ 'group_id': item.config_group.pk, @@ -64,7 +64,7 @@ def get_url(self, item): return { '_self': reverse( - 'API:_api_v2_config_group_software-detail', + 'v2:_api_v2_config_group_software-detail', request = self.context['view'].request, kwargs = { 'group_id': item.config_group.pk, @@ -72,7 +72,7 @@ def get_url(self, item): } ), 'organization': reverse( - 'API:_api_v2_organization-list', + 'v2:_api_v2_organization-list', request=self.context['view'].request, ), 'softwareversion': 'ToDo', diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_api.py b/app/config_management/tests/unit/config_groups/test_config_groups_api.py index 838e1bd21..65d35c9e9 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups_api.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups_api.py @@ -70,7 +70,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_config_group', kwargs=self.url_view_kwargs) + url = reverse('v1:_api_config_group', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py b/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py index 3d9d1b7fa..56066b00b 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py @@ -71,7 +71,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_config_group-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_config_group-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py b/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py index d948a1e6c..67795de0c 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups_viewset.py @@ -20,7 +20,7 @@ class ConfigGroupsPermissionsAPI(TestCase, APIPermissions): model = ConfigGroups - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_config_group' diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py index 85861be8d..fa205dd24 100644 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_api_v2.py @@ -85,7 +85,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_config_group_software-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_config_group_software-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py index c8df61dfe..1d92be790 100644 --- a/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py +++ b/app/config_management/tests/unit/config_groups_software/test_config_groups_software_viewset.py @@ -20,7 +20,7 @@ class ConfigGroupSoftwarePermissionsAPI(TestCase, APIPermissions): model = ConfigGroupSoftware - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_config_group_software' diff --git a/app/config_management/tests/unit/test_config_management_viewset.py b/app/config_management/tests/unit/test_config_management_viewset.py index c3b8768c2..8b4495c32 100644 --- a/app/config_management/tests/unit/test_config_management_viewset.py +++ b/app/config_management/tests/unit/test_config_management_viewset.py @@ -16,7 +16,7 @@ class ConfigManagementViewset( viewset = Index - route_name = 'API:_api_v2_config_management_home' + route_name = 'v2:_api_v2_config_management_home' @classmethod diff --git a/app/config_management/viewsets/index.py b/app/config_management/viewsets/index.py index 3674cea9f..a4a957008 100644 --- a/app/config_management/viewsets/index.py +++ b/app/config_management/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "group": reverse('API:_api_v2_config_group-list', request=request), + "group": reverse('v2:_api_v2_config_group-list', request=request), } ) diff --git a/app/core/serializers/celery_log.py b/app/core/serializers/celery_log.py index 9dc443626..e82d0e30a 100644 --- a/app/core/serializers/celery_log.py +++ b/app/core/serializers/celery_log.py @@ -20,7 +20,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_celery_log-detail", format="html" + view_name="v2:_api_v2_celery_log-detail", format="html" ) @@ -49,7 +49,7 @@ class TaskResultModelSerializer(TaskResultBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_celery_log-detail", + '_self': reverse("v2:_api_v2_celery_log-detail", request=self._context['view'].request, kwargs={ 'pk': item.pk diff --git a/app/core/serializers/history.py b/app/core/serializers/history.py index a6722fa8d..deaca3d18 100644 --- a/app/core/serializers/history.py +++ b/app/core/serializers/history.py @@ -19,7 +19,7 @@ def get_display_name(self, item): def get_my_url(self, item): - return reverse("API:_api_v2_model_history-detail", + return reverse("v2:_api_v2_model_history-detail", request=self._context['view'].request, kwargs={ 'model_class': self._kwargs['context']['view'].kwargs['model_class'], @@ -58,7 +58,7 @@ class HistoryModelSerializer(HistoryBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_model_history-detail", + '_self': reverse("v2:_api_v2_model_history-detail", request=self._context['view'].request, kwargs={ 'model_class': self._kwargs['context']['view'].kwargs['model_class'], diff --git a/app/core/serializers/manufacturer.py b/app/core/serializers/manufacturer.py index e4e369a7a..29dab0ffc 100644 --- a/app/core/serializers/manufacturer.py +++ b/app/core/serializers/manufacturer.py @@ -18,7 +18,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_manufacturer-detail", format="html" + view_name="v2:_api_v2_manufacturer-detail", format="html" ) @@ -47,21 +47,21 @@ class ManufacturerModelSerializer(ManufacturerBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_manufacturer-detail", + '_self': reverse("v2:_api_v2_manufacturer-detail", request=self._context['view'].request, kwargs={ 'pk': item.pk } ), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - # 'notes': reverse("API:_api_v2_manufacturer_notes-list", request=self._context['view'].request, kwargs={'manufacturer_id': item.pk}), + # 'notes': reverse("v2:_api_v2_manufacturer_notes-list", request=self._context['view'].request, kwargs={'manufacturer_id': item.pk}), } diff --git a/app/core/serializers/notes.py b/app/core/serializers/notes.py index 991a81dca..dcfa3426e 100644 --- a/app/core/serializers/notes.py +++ b/app/core/serializers/notes.py @@ -26,7 +26,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device-detail", format="html" + view_name="v2:_api_v2_device-detail", format="html" ) class Meta: @@ -59,7 +59,7 @@ def get_url(self, item): if 'group_id' in self._kwargs['context']['view'].kwargs: - _self = reverse("API:_api_v2_config_group_notes-detail", + _self = reverse("v2:_api_v2_config_group_notes-detail", request=self._context['view'].request, kwargs={ 'group_id': self._kwargs['context']['view'].kwargs['group_id'], @@ -69,7 +69,7 @@ def get_url(self, item): elif 'device_id' in self._kwargs['context']['view'].kwargs: - _self = reverse("API:_api_v2_device_notes-detail", + _self = reverse("v2:_api_v2_device_notes-detail", request=self._context['view'].request, kwargs={ 'device_id': self._kwargs['context']['view'].kwargs['device_id'], @@ -79,7 +79,7 @@ def get_url(self, item): elif 'operating_system_id' in self._kwargs['context']['view'].kwargs: - _self = reverse("API:_api_v2_operating_system_notes-detail", + _self = reverse("v2:_api_v2_operating_system_notes-detail", request=self._context['view'].request, kwargs={ 'operating_system_id': self._kwargs['context']['view'].kwargs['operating_system_id'], @@ -89,7 +89,7 @@ def get_url(self, item): elif 'service_id' in self._kwargs['context']['view'].kwargs: - _self = reverse("API:_api_v2_service_notes-detail", + _self = reverse("v2:_api_v2_service_notes-detail", request=self._context['view'].request, kwargs={ 'service_id': self._kwargs['context']['view'].kwargs['service_id'], @@ -99,7 +99,7 @@ def get_url(self, item): elif 'project_id' in self._kwargs['context']['view'].kwargs: - _self = reverse("API:_api_v2_project_notes-detail", + _self = reverse("v2:_api_v2_project_notes-detail", request=self._context['view'].request, kwargs={ 'project_id': self._kwargs['context']['view'].kwargs['project_id'], @@ -109,7 +109,7 @@ def get_url(self, item): elif 'software_id' in self._kwargs['context']['view'].kwargs: - _self = reverse("API:_api_v2_software_notes-detail", + _self = reverse("v2:_api_v2_software_notes-detail", request=self._context['view'].request, kwargs={ 'software_id': self._kwargs['context']['view'].kwargs['software_id'], diff --git a/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py b/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py index fee76eaf7..ff192d2ed 100644 --- a/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py +++ b/app/core/tests/unit/manufacturer/test_manufacturer_api_v2.py @@ -63,7 +63,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_manufacturer-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_manufacturer-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py b/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py index 66447711f..9a1aa77a9 100644 --- a/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py +++ b/app/core/tests/unit/manufacturer/test_manufacturer_viewset.py @@ -20,7 +20,7 @@ class ManufacturerPermissionsAPI(TestCase, APIPermissions): model = Manufacturer - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_manufacturer' diff --git a/app/core/tests/unit/test_history/test_history_viewset.py b/app/core/tests/unit/test_history/test_history_viewset.py index 975cad517..db8d7adf9 100644 --- a/app/core/tests/unit/test_history/test_history_viewset.py +++ b/app/core/tests/unit/test_history/test_history_viewset.py @@ -23,7 +23,7 @@ class HistoryPermissionsAPI(TestCase, APIPermissions): model = History - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_model_history' diff --git a/app/core/tests/unit/test_notes/test_notes_api_v2.py b/app/core/tests/unit/test_notes/test_notes_api_v2.py index f32af6887..6222e4e30 100644 --- a/app/core/tests/unit/test_notes/test_notes_api_v2.py +++ b/app/core/tests/unit/test_notes/test_notes_api_v2.py @@ -95,7 +95,7 @@ def setUpTestData(self): client = Client() - url = reverse('API:_api_v2_device_notes-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_device_notes-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/core/tests/unit/test_task_result/test_task_result_api_v2.py b/app/core/tests/unit/test_task_result/test_task_result_api_v2.py index 1d40a6d77..93575aa91 100644 --- a/app/core/tests/unit/test_task_result/test_task_result_api_v2.py +++ b/app/core/tests/unit/test_task_result/test_task_result_api_v2.py @@ -76,7 +76,7 @@ def setUpTestData(self): self.url_view_kwargs = {'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_celery_log-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_celery_log-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/core/tests/unit/test_task_result/test_task_result_viewset.py b/app/core/tests/unit/test_task_result/test_task_result_viewset.py index 17b82852a..a0b80c3c6 100644 --- a/app/core/tests/unit/test_task_result/test_task_result_viewset.py +++ b/app/core/tests/unit/test_task_result/test_task_result_viewset.py @@ -34,7 +34,7 @@ class TaskResultPermissionsAPI( model = TaskResult - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_celery_log' diff --git a/app/core/tests/unit/ticket/test_ticket_permission_api.py b/app/core/tests/unit/ticket/test_ticket_permission_api.py index 24134c60d..740a05bf9 100644 --- a/app/core/tests/unit/ticket/test_ticket_permission_api.py +++ b/app/core/tests/unit/ticket/test_ticket_permission_api.py @@ -186,7 +186,7 @@ class ChangeTicketPermissionsAPI(TicketPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.CHANGE.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_itim_change-detail' @@ -202,7 +202,7 @@ class IncidentTicketPermissionsAPI(TicketPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.INCIDENT.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_itim_incident-detail' @@ -218,7 +218,7 @@ class ProblemTicketPermissionsAPI(TicketPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.PROBLEM.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_itim_problem-detail' @@ -234,7 +234,7 @@ class RequestTicketPermissionsAPI(TicketPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.REQUEST.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_assistance_request-detail' diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_permission_api.py b/app/core/tests/unit/ticket_category/test_ticket_category_permission_api.py index ce5f333e3..7ad322f43 100644 --- a/app/core/tests/unit/ticket_category/test_ticket_category_permission_api.py +++ b/app/core/tests/unit/ticket_category/test_ticket_category_permission_api.py @@ -18,7 +18,7 @@ class TicketCategoryPermissionsAPI(TestCase, APIPermissions): model = TicketCategory - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_ticket_category-detail' diff --git a/app/core/tests/unit/ticket_comment/test_ticket_comment_permission_api.py b/app/core/tests/unit/ticket_comment/test_ticket_comment_permission_api.py index caa4bc34c..c802635b3 100644 --- a/app/core/tests/unit/ticket_comment/test_ticket_comment_permission_api.py +++ b/app/core/tests/unit/ticket_comment/test_ticket_comment_permission_api.py @@ -251,7 +251,7 @@ class ChangeCommentTicketPermissionsAPI(TicketCommentPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.CHANGE.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_itim_change_ticket_comments-detail' @@ -267,7 +267,7 @@ class IncidentTicketCommentPermissionsAPI(TicketCommentPermissionsAPI, TestCase) ticket_type_enum: int = int(Ticket.TicketType.INCIDENT.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_itim_incident_ticket_comments-detail' @@ -283,7 +283,7 @@ class ProblemTicketCommentPermissionsAPI(TicketCommentPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.PROBLEM.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_itim_problem_ticket_comments-detail' @@ -299,7 +299,7 @@ class RequestTicketCommentPermissionsAPI(TicketCommentPermissionsAPI, TestCase): ticket_type_enum: int = int(Ticket.TicketType.REQUEST.value) - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_assistance_request_ticket_comments-detail' diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py index fe0f35ebd..34d39953c 100644 --- a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_permission_api.py @@ -18,7 +18,7 @@ class TicketCommentCategoryPermissionsAPI(TestCase, APIPermissions): model = TicketCommentCategory - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_ticket_comment_category-detail' diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 710fa938b..86bd85538 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -27,7 +27,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device-detail", format="html" + view_name="v2:_api_v2_device-detail", format="html" ) class Meta: @@ -55,21 +55,21 @@ class DeviceModelSerializer(DeviceBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), - 'device_model': reverse("API:_api_v2_device_model-list", request=self._context['view'].request), - 'device_type': reverse("API:_api_v2_device_type-list", request=self._context['view'].request), - 'external_links': reverse("API:_api_v2_external_link-list", request=self._context['view'].request) + '?devices=true', + '_self': reverse("v2:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'device_model': reverse("v2:_api_v2_device_model-list", request=self._context['view'].request), + 'device_type': reverse("v2:_api_v2_device_type-list", request=self._context['view'].request), + 'external_links': reverse("v2:_api_v2_external_link-list", request=self._context['view'].request) + '?devices=true', 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), - 'service': reverse("API:_api_v2_service_device-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), - 'software': reverse("API:_api_v2_device_software-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'notes': reverse("v2:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'service': reverse("v2:_api_v2_service_device-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'software': reverse("v2:_api_v2_device_software-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), } diff --git a/app/itam/serializers/device_model.py b/app/itam/serializers/device_model.py index 637112031..6b8e4e8fb 100644 --- a/app/itam/serializers/device_model.py +++ b/app/itam/serializers/device_model.py @@ -20,7 +20,7 @@ def get_display_name(self, item): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device_model-detail", format="html" + view_name="v2:_api_v2_device_model-detail", format="html" ) class Meta: @@ -50,7 +50,7 @@ class DeviceModelModelSerializer(DeviceModelBaseSerializer): def get_url(self, obj): return { - '_self': reverse("API:_api_v2_device_model-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) + '_self': reverse("v2:_api_v2_device_model-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) } class Meta: diff --git a/app/itam/serializers/device_software.py b/app/itam/serializers/device_software.py index 9b03bdcab..73cb536a4 100644 --- a/app/itam/serializers/device_software.py +++ b/app/itam/serializers/device_software.py @@ -26,7 +26,7 @@ def get_display_name(self, item): url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device_software-detail", format="html" + view_name="v2:_api_v2_device_software-detail", format="html" ) @@ -58,7 +58,7 @@ def get_url(self, obj): return { '_self': reverse( - "API:_api_v2_device_software-detail", + "v2:_api_v2_device_software-detail", request=self._context['view'].request, kwargs={ 'device_id': self._context['view'].kwargs['device_id'], diff --git a/app/itam/serializers/device_type.py b/app/itam/serializers/device_type.py index 8a08841ba..2b5a755ce 100644 --- a/app/itam/serializers/device_type.py +++ b/app/itam/serializers/device_type.py @@ -16,7 +16,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device_type-detail", format="html" + view_name="v2:_api_v2_device_type-detail", format="html" ) class Meta: @@ -46,7 +46,7 @@ class DeviceTypeModelSerializer(DeviceTypeBaseSerializer): def get_url(self, obj): return { - '_self': reverse("API:_api_v2_device_type-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) + '_self': reverse("v2:_api_v2_device_type-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) } diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index 4565a9703..64b2cce5e 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_operating_system-detail", format="html" + view_name="v2:_api_v2_operating_system-detail", format="html" ) class Meta: @@ -51,18 +51,18 @@ class OperatingSystemModelSerializer(OperatingSystemBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_operating_system-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_operating_system-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_operating_system_notes-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), + 'notes': reverse("v2:_api_v2_operating_system_notes-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), 'tickets': 'ToDo', - 'version': reverse("API:_api_v2_operating_system_version-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), + 'version': reverse("v2:_api_v2_operating_system_version-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), } diff --git a/app/itam/serializers/operating_system_version.py b/app/itam/serializers/operating_system_version.py index c1c42e40e..ce9e4cbd3 100644 --- a/app/itam/serializers/operating_system_version.py +++ b/app/itam/serializers/operating_system_version.py @@ -25,7 +25,7 @@ def get_display_name(self, item): def my_url(self, item): return reverse( - "API:_api_v2_operating_system_version-detail", + "v2:_api_v2_operating_system_version-detail", request=self.context['view'].request, kwargs={ 'operating_system_id': self._context['view'].kwargs['operating_system_id'], @@ -64,7 +64,7 @@ def get_url(self, item): return { '_self': reverse( - "API:_api_v2_operating_system_version-detail", + "v2:_api_v2_operating_system_version-detail", request=self._context['view'].request, kwargs={ 'operating_system_id': self._context['view'].kwargs['operating_system_id'], @@ -72,7 +72,7 @@ def get_url(self, item): } ), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, @@ -80,7 +80,7 @@ def get_url(self, item): } ), 'notes': reverse( - "API:_api_v2_operating_system_notes-list", + "v2:_api_v2_operating_system_notes-list", request=self._context['view'].request, kwargs={ 'operating_system_id': item.pk diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 5e117636b..5ca93fee9 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_software-detail", format="html" + view_name="v2:_api_v2_software-detail", format="html" ) class Meta: @@ -49,21 +49,21 @@ class SoftwareModelSerializer(SoftwareBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_software-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), - 'external_links': reverse("API:_api_v2_external_link-list", request=self._context['view'].request) + '?software=true', + '_self': reverse("v2:_api_v2_software-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + 'external_links': reverse("v2:_api_v2_external_link-list", request=self._context['view'].request) + '?software=true', 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_software_notes-list", request=self._context['view'].request, kwargs={'software_id': item.pk}), - 'publisher': reverse("API:_api_v2_manufacturer-list", request=self._context['view'].request), + 'notes': reverse("v2:_api_v2_software_notes-list", request=self._context['view'].request, kwargs={'software_id': item.pk}), + 'publisher': reverse("v2:_api_v2_manufacturer-list", request=self._context['view'].request), 'services': 'ToDo', 'version': reverse( - "API:_api_v2_software_version-list", + "v2:_api_v2_software_version-list", request=self._context['view'].request, kwargs={ 'software_id': item.pk diff --git a/app/itam/serializers/software_category.py b/app/itam/serializers/software_category.py index 340ae0ced..747e87b00 100644 --- a/app/itam/serializers/software_category.py +++ b/app/itam/serializers/software_category.py @@ -18,7 +18,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_software_category-detail", format="html" + view_name="v2:_api_v2_software_category-detail", format="html" ) class Meta: @@ -48,7 +48,7 @@ class SoftwareCategoryModelSerializer(SoftwareCategoryBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_software_category-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_software_category-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': 'ToDo', 'notes': 'ToDo', } diff --git a/app/itam/serializers/software_version.py b/app/itam/serializers/software_version.py index 77b1d778b..f5514acd9 100644 --- a/app/itam/serializers/software_version.py +++ b/app/itam/serializers/software_version.py @@ -22,7 +22,7 @@ def get_display_name(self, item): def my_url(self, item): return reverse( - "API:_api_v2_software_version-detail", + "v2:_api_v2_software_version-detail", request=self.context['view'].request, kwargs={ 'software_id': item.software.pk, @@ -58,7 +58,7 @@ def get_url(self, item): return { '_self': reverse( - "API:_api_v2_software_version-detail", + "v2:_api_v2_software_version-detail", request=self._context['view'].request, kwargs={ 'software_id': item.software.pk, @@ -66,7 +66,7 @@ def get_url(self, item): } ), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, diff --git a/app/itam/tests/unit/device/test_device_api.py b/app/itam/tests/unit/device/test_device_api.py index 34c03bb9d..dd7f6f496 100644 --- a/app/itam/tests/unit/device/test_device_api.py +++ b/app/itam/tests/unit/device/test_device_api.py @@ -199,7 +199,7 @@ def setUpTestData(self): client = Client() - url = reverse('API:device-detail', kwargs=self.url_view_kwargs) + url = reverse('v1:device-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) @@ -420,7 +420,7 @@ def test_api_create_device_existing_uuid_matches_status_200(self): """ client = Client() - url = reverse('API:device-list') + url = reverse('v1:device-list') client.force_login(self.add_user) @@ -444,7 +444,7 @@ def test_api_create_device_existing_uuid_matches_correct_item(self): """ client = Client() - url = reverse('API:device-list') + url = reverse('v1:device-list') client.force_login(self.add_user) @@ -468,7 +468,7 @@ def test_api_create_device_existing_serial_number_matches_status_200(self): """ client = Client() - url = reverse('API:device-list') + url = reverse('v1:device-list') client.force_login(self.add_user) @@ -492,7 +492,7 @@ def test_api_create_device_existing_serial_number_matches_correct_item(self): """ client = Client() - url = reverse('API:device-list') + url = reverse('v1:device-list') client.force_login(self.add_user) diff --git a/app/itam/tests/unit/device/test_device_api_v2.py b/app/itam/tests/unit/device/test_device_api_v2.py index e665fc021..dc1f1fbf9 100644 --- a/app/itam/tests/unit/device/test_device_api_v2.py +++ b/app/itam/tests/unit/device/test_device_api_v2.py @@ -90,7 +90,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_device-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_device-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/device/test_device_permission_api.py b/app/itam/tests/unit/device/test_device_permission_api.py index f2699cd56..1b26667ae 100644 --- a/app/itam/tests/unit/device/test_device_permission_api.py +++ b/app/itam/tests/unit/device/test_device_permission_api.py @@ -17,7 +17,7 @@ class DevicePermissionsAPI(TestCase, APIPermissions): model = Device - app_namespace = 'API' + app_namespace = 'v1' url_name = 'device-detail' diff --git a/app/itam/tests/unit/device/test_device_viewset.py b/app/itam/tests/unit/device/test_device_viewset.py index 741137529..f1293ab82 100644 --- a/app/itam/tests/unit/device/test_device_viewset.py +++ b/app/itam/tests/unit/device/test_device_viewset.py @@ -16,7 +16,7 @@ class DevicePermissionsAPI(TestCase, APIPermissions): model = Device - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_device' diff --git a/app/itam/tests/unit/device_model/test_device_model_api_v2.py b/app/itam/tests/unit/device_model/test_device_model_api_v2.py index 3ebbc1a57..5a2b2e074 100644 --- a/app/itam/tests/unit/device_model/test_device_model_api_v2.py +++ b/app/itam/tests/unit/device_model/test_device_model_api_v2.py @@ -72,7 +72,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_device_model-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_device_model-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/device_model/test_device_model_viewset.py b/app/itam/tests/unit/device_model/test_device_model_viewset.py index aabbdb520..bc1e3dbb3 100644 --- a/app/itam/tests/unit/device_model/test_device_model_viewset.py +++ b/app/itam/tests/unit/device_model/test_device_model_viewset.py @@ -16,7 +16,7 @@ class DeviceModelPermissionsAPI(TestCase, APIPermissions): model = DeviceModel - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_device_model' diff --git a/app/itam/tests/unit/device_software/test_device_software_api_v2.py b/app/itam/tests/unit/device_software/test_device_software_api_v2.py index c485d1850..f7e9ed96b 100644 --- a/app/itam/tests/unit/device_software/test_device_software_api_v2.py +++ b/app/itam/tests/unit/device_software/test_device_software_api_v2.py @@ -106,7 +106,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_device_software-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_device_software-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/device_software/test_device_software_viewset.py b/app/itam/tests/unit/device_software/test_device_software_viewset.py index b9fa6950a..4c1f038fb 100644 --- a/app/itam/tests/unit/device_software/test_device_software_viewset.py +++ b/app/itam/tests/unit/device_software/test_device_software_viewset.py @@ -17,7 +17,7 @@ class DeviceSoftwarePermissionsAPI(TestCase, APIPermissions): model = DeviceSoftware - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_device_software' diff --git a/app/itam/tests/unit/device_type/test_device_type_api_v2.py b/app/itam/tests/unit/device_type/test_device_type_api_v2.py index b9669b5b4..c03c01be2 100644 --- a/app/itam/tests/unit/device_type/test_device_type_api_v2.py +++ b/app/itam/tests/unit/device_type/test_device_type_api_v2.py @@ -63,7 +63,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_device_type-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_device_type-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/device_type/test_device_type_viewset.py b/app/itam/tests/unit/device_type/test_device_type_viewset.py index fed7edb34..c2db4d215 100644 --- a/app/itam/tests/unit/device_type/test_device_type_viewset.py +++ b/app/itam/tests/unit/device_type/test_device_type_viewset.py @@ -16,7 +16,7 @@ class DeviceTypePermissionsAPI(TestCase, APIPermissions): model = DeviceType - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_device_type' diff --git a/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py b/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py index c118017cf..f7bcc6eea 100644 --- a/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py +++ b/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py @@ -74,7 +74,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_operating_system-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_operating_system-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/operating_system/test_operating_system_viewset.py b/app/itam/tests/unit/operating_system/test_operating_system_viewset.py index ddc770528..021ff8694 100644 --- a/app/itam/tests/unit/operating_system/test_operating_system_viewset.py +++ b/app/itam/tests/unit/operating_system/test_operating_system_viewset.py @@ -16,7 +16,7 @@ class OperatingSystemPermissionsAPI(TestCase, APIPermissions): model = OperatingSystem - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_operating_system' diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py index e98eb0f3a..3c3905d9e 100644 --- a/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version_api_v2.py @@ -72,7 +72,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_operating_system_version-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_operating_system_version-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py b/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py index 6c60f7336..bfa7091cf 100644 --- a/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py +++ b/app/itam/tests/unit/operating_system_version/test_operating_system_version_viewset.py @@ -16,7 +16,7 @@ class OperatingSystemVersionPermissionsAPI(TestCase, APIPermissions): model = OperatingSystemVersion - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_operating_system_version' diff --git a/app/itam/tests/unit/software/test_software_api.py b/app/itam/tests/unit/software/test_software_api.py index ad01f72a3..70c0cf09f 100644 --- a/app/itam/tests/unit/software/test_software_api.py +++ b/app/itam/tests/unit/software/test_software_api.py @@ -21,7 +21,7 @@ class SoftwareAPI(TestCase): model = Software - app_namespace = 'API' + app_namespace = 'v1' url_name = 'software-detail' diff --git a/app/itam/tests/unit/software/test_software_api_v2.py b/app/itam/tests/unit/software/test_software_api_v2.py index ae48866f3..589349459 100644 --- a/app/itam/tests/unit/software/test_software_api_v2.py +++ b/app/itam/tests/unit/software/test_software_api_v2.py @@ -80,7 +80,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_software-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_software-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) @@ -308,3 +308,22 @@ def test_api_field_type_urls_notes(self): """ assert type(self.api_data['_urls']['version']) is str + + + + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str diff --git a/app/itam/tests/unit/software/test_software_permission_api.py b/app/itam/tests/unit/software/test_software_permission_api.py index 4378819c3..30cca90dd 100644 --- a/app/itam/tests/unit/software/test_software_permission_api.py +++ b/app/itam/tests/unit/software/test_software_permission_api.py @@ -18,7 +18,7 @@ class SoftwarePermissionsAPI(TestCase, APIPermissions): model = Software - app_namespace = 'API' + app_namespace = 'v1' url_name = 'software-detail' diff --git a/app/itam/tests/unit/software/test_software_viewset.py b/app/itam/tests/unit/software/test_software_viewset.py index 1025ea7c9..d2d1eebc4 100644 --- a/app/itam/tests/unit/software/test_software_viewset.py +++ b/app/itam/tests/unit/software/test_software_viewset.py @@ -16,7 +16,7 @@ class SoftwarePermissionsAPI(TestCase, APIPermissions): model = Software - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_software' diff --git a/app/itam/tests/unit/software_category/test_software_category_api_v2.py b/app/itam/tests/unit/software_category/test_software_category_api_v2.py index 6ca334d83..07a2a58fe 100644 --- a/app/itam/tests/unit/software_category/test_software_category_api_v2.py +++ b/app/itam/tests/unit/software_category/test_software_category_api_v2.py @@ -72,7 +72,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_software_category-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_software_category-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/software_category/test_software_category_viewset.py b/app/itam/tests/unit/software_category/test_software_category_viewset.py index cd21c5605..fdf0d700f 100644 --- a/app/itam/tests/unit/software_category/test_software_category_viewset.py +++ b/app/itam/tests/unit/software_category/test_software_category_viewset.py @@ -16,7 +16,7 @@ class SoftwareCategoryPermissionsAPI(TestCase, APIPermissions): model = SoftwareCategory - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_software_category' diff --git a/app/itam/tests/unit/software_version/test_software_version_api_v2.py b/app/itam/tests/unit/software_version/test_software_version_api_v2.py index 62fcc007d..2bc2d1d23 100644 --- a/app/itam/tests/unit/software_version/test_software_version_api_v2.py +++ b/app/itam/tests/unit/software_version/test_software_version_api_v2.py @@ -71,7 +71,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_software_version-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_software_version-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itam/tests/unit/software_version/test_software_version_viewset.py b/app/itam/tests/unit/software_version/test_software_version_viewset.py index 2b309ae54..1e9feffdb 100644 --- a/app/itam/tests/unit/software_version/test_software_version_viewset.py +++ b/app/itam/tests/unit/software_version/test_software_version_viewset.py @@ -16,7 +16,7 @@ class SoftwareVersionPermissionsAPI(TestCase, APIPermissions): model = SoftwareVersion - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_software_version' diff --git a/app/itam/tests/unit/test_itam_viewset.py b/app/itam/tests/unit/test_itam_viewset.py index ae28710fd..128ab06c8 100644 --- a/app/itam/tests/unit/test_itam_viewset.py +++ b/app/itam/tests/unit/test_itam_viewset.py @@ -16,7 +16,7 @@ class ItamViewset( viewset = Index - route_name = 'API:_api_v2_itam_home' + route_name = 'v2:_api_v2_itam_home' @classmethod diff --git a/app/itam/viewsets/index.py b/app/itam/viewsets/index.py index 833d0c8de..96833e072 100644 --- a/app/itam/viewsets/index.py +++ b/app/itam/viewsets/index.py @@ -25,8 +25,8 @@ def list(self, request, pk=None): return Response( { - "device": reverse('API:_api_v2_device-list', request=request), - "operating_system": reverse('API:_api_v2_operating_system-list', request=request), - "software": reverse('API:_api_v2_software-list', request=request) + "device": reverse('v2:_api_v2_device-list', request=request), + "operating_system": reverse('v2:_api_v2_operating_system-list', request=request), + "software": reverse('v2:_api_v2_software-list', request=request) } ) diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index c45c2a380..86fcbe1f9 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_cluster-detail", format="html" + view_name="v2:_api_v2_cluster-detail", format="html" ) class Meta: @@ -48,16 +48,16 @@ class ClusterModelSerializer(ClusterBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_cluster-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_cluster-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), + 'notes': reverse("v2:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), 'tickets': 'ToDo' } diff --git a/app/itim/serializers/cluster_type.py b/app/itim/serializers/cluster_type.py index c84ed120a..524a3fac2 100644 --- a/app/itim/serializers/cluster_type.py +++ b/app/itim/serializers/cluster_type.py @@ -18,7 +18,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_cluster_type-detail", format="html" + view_name="v2:_api_v2_cluster_type-detail", format="html" ) class Meta: @@ -47,16 +47,16 @@ class ClusterTypeModelSerializer(ClusterTypeBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_cluster_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_cluster_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_cluster_type_notes-list", request=self._context['view'].request, kwargs={'cluster_type_id': item.pk}), + 'notes': reverse("v2:_api_v2_cluster_type_notes-list", request=self._context['view'].request, kwargs={'cluster_type_id': item.pk}), } diff --git a/app/itim/serializers/port.py b/app/itim/serializers/port.py index 32ceb9bdd..28eb9bd22 100644 --- a/app/itim/serializers/port.py +++ b/app/itim/serializers/port.py @@ -18,7 +18,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_port-detail", format="html" + view_name="v2:_api_v2_port-detail", format="html" ) name = serializers.SerializerMethodField('get_display_name') @@ -50,16 +50,16 @@ class PortModelSerializer(PortBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_port-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_port-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_port_notes-list", request=self._context['view'].request, kwargs={'port_id': item.pk}), + 'notes': reverse("v2:_api_v2_port_notes-list", request=self._context['view'].request, kwargs={'port_id': item.pk}), } diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index 56e3e0bff..aeecd36b4 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -21,7 +21,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_service-detail", format="html" + view_name="v2:_api_v2_service-detail", format="html" ) class Meta: @@ -50,16 +50,16 @@ class ServiceModelSerializer(ServiceBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_service-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_service-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_service_notes-list", request=self._context['view'].request, kwargs={'service_id': item.pk}), + 'notes': reverse("v2:_api_v2_service_notes-list", request=self._context['view'].request, kwargs={'service_id': item.pk}), 'tickets': 'ToDo' } diff --git a/app/itim/tests/unit/cluster/test_cluster_api_v2.py b/app/itim/tests/unit/cluster/test_cluster_api_v2.py index 091dc9dc3..237c1715c 100644 --- a/app/itim/tests/unit/cluster/test_cluster_api_v2.py +++ b/app/itim/tests/unit/cluster/test_cluster_api_v2.py @@ -95,7 +95,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_cluster-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_cluster-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itim/tests/unit/cluster/test_cluster_viewset.py b/app/itim/tests/unit/cluster/test_cluster_viewset.py index 6b7dce1ab..5f40216d3 100644 --- a/app/itim/tests/unit/cluster/test_cluster_viewset.py +++ b/app/itim/tests/unit/cluster/test_cluster_viewset.py @@ -16,7 +16,7 @@ class ClusterPermissionsAPI(TestCase, APIPermissions): model = Cluster - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_cluster' diff --git a/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py b/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py index f796c4563..93bfbaaae 100644 --- a/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py +++ b/app/itim/tests/unit/cluster_types/test_cluster_type_api_v2.py @@ -65,7 +65,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_cluster_type-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_cluster_type-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py b/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py index 7fd3a3404..7b4ab8285 100644 --- a/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py +++ b/app/itim/tests/unit/cluster_types/test_cluster_type_viewset.py @@ -16,7 +16,7 @@ class ClusterTypePermissionsAPI(TestCase, APIPermissions): model = ClusterType - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_cluster_type' diff --git a/app/itim/tests/unit/port/test_port_api_v2.py b/app/itim/tests/unit/port/test_port_api_v2.py index 4ce1ef32a..9a8b38f59 100644 --- a/app/itim/tests/unit/port/test_port_api_v2.py +++ b/app/itim/tests/unit/port/test_port_api_v2.py @@ -66,7 +66,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_port-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_port-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/itim/tests/unit/port/test_port_viewset.py b/app/itim/tests/unit/port/test_port_viewset.py index 98a94c656..fc61ae979 100644 --- a/app/itim/tests/unit/port/test_port_viewset.py +++ b/app/itim/tests/unit/port/test_port_viewset.py @@ -16,7 +16,7 @@ class PortPermissionsAPI(TestCase, APIPermissions): model = Port - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_port' diff --git a/app/itim/tests/unit/service/test_service_api_v2.py b/app/itim/tests/unit/service/test_service_api_v2.py index 97a8568e6..f8fabbad5 100644 --- a/app/itim/tests/unit/service/test_service_api_v2.py +++ b/app/itim/tests/unit/service/test_service_api_v2.py @@ -108,7 +108,7 @@ def setUpTestData(self): ) client = Client() - url = reverse('API:_api_v2_service-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_service-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) @@ -117,7 +117,7 @@ def setUpTestData(self): self.api_data = response.data - url = reverse('API:_api_v2_service-detail', kwargs={'pk': self.item_two.id}) + url = reverse('v2:_api_v2_service-detail', kwargs={'pk': self.item_two.id}) client.force_login(self.view_user) response = client.get(url) diff --git a/app/itim/tests/unit/service/test_service_viewset.py b/app/itim/tests/unit/service/test_service_viewset.py index 5282ead73..279dbd253 100644 --- a/app/itim/tests/unit/service/test_service_viewset.py +++ b/app/itim/tests/unit/service/test_service_viewset.py @@ -18,7 +18,7 @@ class ServicePermissionsAPI(TestCase, APIPermissions): model = Service - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_service' diff --git a/app/itim/tests/unit/test_itim_viewset.py b/app/itim/tests/unit/test_itim_viewset.py index 012f6cc48..d1e0f4dc5 100644 --- a/app/itim/tests/unit/test_itim_viewset.py +++ b/app/itim/tests/unit/test_itim_viewset.py @@ -16,7 +16,7 @@ class ITIMViewset( viewset = Index - route_name = 'API:_api_v2_itim_home' + route_name = 'v2:_api_v2_itim_home' @classmethod diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py index 2f7ebaea9..a00e2d1c6 100644 --- a/app/itim/viewsets/index.py +++ b/app/itim/viewsets/index.py @@ -26,9 +26,9 @@ def list(self, request, pk=None): return Response( { "change": "ToDo", - "cluster": reverse('API:_api_v2_cluster-list', request=request), + "cluster": reverse('v2:_api_v2_cluster-list', request=request), "incident": "ToDo", "problem": "ToDo", - "service": reverse('API:_api_v2_service-list', request=request), + "service": reverse('v2:_api_v2_service-list', request=request), } ) diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index e4f459102..5a4aae5ac 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -25,7 +25,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_project-detail", format="html" + view_name="v2:_api_v2_project-detail", format="html" ) class Meta: @@ -55,17 +55,17 @@ class ProjectModelSerializer(ProjectBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_project-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_project-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'milestone': reverse("API:_api_v2_project_milestone-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), - 'notes': reverse("API:_api_v2_project_notes-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), + 'milestone': reverse("v2:_api_v2_project_milestone-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), + 'notes': reverse("v2:_api_v2_project_notes-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), 'tickets': 'ToDo' } diff --git a/app/project_management/serializers/project_milestone.py b/app/project_management/serializers/project_milestone.py index 04915f315..e1eba818b 100644 --- a/app/project_management/serializers/project_milestone.py +++ b/app/project_management/serializers/project_milestone.py @@ -22,7 +22,7 @@ def get_display_name(self, item): def get_url(self, item): return reverse( - "API:_api_v2_project_milestone-detail", + "v2:_api_v2_project_milestone-detail", request=self._context['view'].request, kwargs={ 'project_id': item.project.id, @@ -58,7 +58,7 @@ def get_url(self, item): return { '_self': reverse( - "API:_api_v2_project_milestone-detail", + "v2:_api_v2_project_milestone-detail", request=self._context['view'].request, kwargs={ 'project_id': item.project.id, diff --git a/app/project_management/serializers/project_states.py b/app/project_management/serializers/project_states.py index 665564cdb..52a657aae 100644 --- a/app/project_management/serializers/project_states.py +++ b/app/project_management/serializers/project_states.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_project_state-detail", format="html" + view_name="v2:_api_v2_project_state-detail", format="html" ) @@ -51,7 +51,7 @@ def get_url(self, item): return { '_self': reverse( - "API:_api_v2_project_state-detail", + "v2:_api_v2_project_state-detail", request=self._context['view'].request, kwargs={ 'pk': item.pk diff --git a/app/project_management/serializers/project_type.py b/app/project_management/serializers/project_type.py index 1ee9c15bb..a8e79b04e 100644 --- a/app/project_management/serializers/project_type.py +++ b/app/project_management/serializers/project_type.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_project_type-detail", format="html" + view_name="v2:_api_v2_project_type-detail", format="html" ) @@ -50,7 +50,7 @@ def get_url(self, item): return { '_self': reverse( - "API:_api_v2_project_type-detail", + "v2:_api_v2_project_type-detail", request=self._context['view'].request, kwargs={ 'pk': item.pk diff --git a/app/project_management/tests/unit/project/test_project_api_v2.py b/app/project_management/tests/unit/project/test_project_api_v2.py index d9e3ee9af..4b30dada5 100644 --- a/app/project_management/tests/unit/project/test_project_api_v2.py +++ b/app/project_management/tests/unit/project/test_project_api_v2.py @@ -106,7 +106,7 @@ def setUpTestData(self): self.url_view_kwargs = {'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_project-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_project-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/project_management/tests/unit/project/test_project_permission_api.py b/app/project_management/tests/unit/project/test_project_permission_api.py index fe0f35ebd..34d39953c 100644 --- a/app/project_management/tests/unit/project/test_project_permission_api.py +++ b/app/project_management/tests/unit/project/test_project_permission_api.py @@ -18,7 +18,7 @@ class TicketCommentCategoryPermissionsAPI(TestCase, APIPermissions): model = TicketCommentCategory - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_ticket_comment_category-detail' diff --git a/app/project_management/tests/unit/project/test_project_viewset.py b/app/project_management/tests/unit/project/test_project_viewset.py index 407560aa0..ea8fc7d03 100644 --- a/app/project_management/tests/unit/project/test_project_viewset.py +++ b/app/project_management/tests/unit/project/test_project_viewset.py @@ -17,7 +17,7 @@ class ProjectPermissionsAPI(TestCase, APIPermissions): model = Project - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_project' diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py index c6fd6639f..87629a4bc 100644 --- a/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_api_v2.py @@ -85,7 +85,7 @@ def setUpTestData(self): self.url_view_kwargs = {'project_id': project.id, 'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_project_milestone-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_project_milestone-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_permission_api.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_permission_api.py index 47d9d3940..f8f3ce271 100644 --- a/app/project_management/tests/unit/project_milestone/test_project_milestone_permission_api.py +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_permission_api.py @@ -20,7 +20,7 @@ class ProjectMilestonePermissionsAPI(TestCase, APIPermissions): model = ProjectMilestone - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_project_milestone-detail' diff --git a/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py b/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py index d2f60755d..d8c29d1f1 100644 --- a/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py +++ b/app/project_management/tests/unit/project_milestone/test_project_milestone_viewset.py @@ -16,7 +16,7 @@ class ProjectMilestonePermissionsAPI(TestCase, APIPermissions): model = ProjectMilestone - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_project_milestone' diff --git a/app/project_management/tests/unit/project_state/test_project_state_api_v2.py b/app/project_management/tests/unit/project_state/test_project_state_api_v2.py index 9fe2a2be2..9dd72d0d3 100644 --- a/app/project_management/tests/unit/project_state/test_project_state_api_v2.py +++ b/app/project_management/tests/unit/project_state/test_project_state_api_v2.py @@ -106,7 +106,7 @@ def setUpTestData(self): self.url_view_kwargs = {'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_project_state-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_project_state-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/project_management/tests/unit/project_state/test_project_state_permission_api.py b/app/project_management/tests/unit/project_state/test_project_state_permission_api.py index 65f1c41ed..adbeb0738 100644 --- a/app/project_management/tests/unit/project_state/test_project_state_permission_api.py +++ b/app/project_management/tests/unit/project_state/test_project_state_permission_api.py @@ -19,7 +19,7 @@ class ProjectStatePermissionsAPI(TestCase, APIPermissions): model = ProjectState - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_project_state-detail' diff --git a/app/project_management/tests/unit/project_state/test_project_state_viewset.py b/app/project_management/tests/unit/project_state/test_project_state_viewset.py index 029a70728..7b45f1f17 100644 --- a/app/project_management/tests/unit/project_state/test_project_state_viewset.py +++ b/app/project_management/tests/unit/project_state/test_project_state_viewset.py @@ -16,7 +16,7 @@ class ProjectStatePermissionsAPI(TestCase, APIPermissions): model = ProjectState - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_project_state' diff --git a/app/project_management/tests/unit/project_type/test_project_type_api_v2.py b/app/project_management/tests/unit/project_type/test_project_type_api_v2.py index 3acff895a..969c530be 100644 --- a/app/project_management/tests/unit/project_type/test_project_type_api_v2.py +++ b/app/project_management/tests/unit/project_type/test_project_type_api_v2.py @@ -85,7 +85,7 @@ def setUpTestData(self): self.url_view_kwargs = {'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_project_type-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_project_type-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/project_management/tests/unit/project_type/test_project_type_permission_api.py b/app/project_management/tests/unit/project_type/test_project_type_permission_api.py index 23272b79f..7254151d6 100644 --- a/app/project_management/tests/unit/project_type/test_project_type_permission_api.py +++ b/app/project_management/tests/unit/project_type/test_project_type_permission_api.py @@ -19,7 +19,7 @@ class ProjectTypePermissionsAPI(TestCase, APIPermissions): model = ProjectType - app_namespace = 'API' + app_namespace = 'v1' url_name = '_api_project_type-detail' diff --git a/app/project_management/tests/unit/project_type/test_project_type_viewset.py b/app/project_management/tests/unit/project_type/test_project_type_viewset.py index f41f7e21a..5f68f5d92 100644 --- a/app/project_management/tests/unit/project_type/test_project_type_viewset.py +++ b/app/project_management/tests/unit/project_type/test_project_type_viewset.py @@ -16,7 +16,7 @@ class ProjectTypePermissionsAPI(TestCase, APIPermissions): model = ProjectType - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_project_type' diff --git a/app/project_management/tests/unit/test_project_management_viewset.py b/app/project_management/tests/unit/test_project_management_viewset.py index 121a7b50d..5af1bd472 100644 --- a/app/project_management/tests/unit/test_project_management_viewset.py +++ b/app/project_management/tests/unit/test_project_management_viewset.py @@ -17,7 +17,7 @@ class ProjectManagementViewset( viewset = Index - route_name = 'API:_api_v2_project_management_home' + route_name = 'v2:_api_v2_project_management_home' @classmethod diff --git a/app/project_management/viewsets/index.py b/app/project_management/viewsets/index.py index 0914da618..c9d6fa476 100644 --- a/app/project_management/viewsets/index.py +++ b/app/project_management/viewsets/index.py @@ -25,6 +25,6 @@ def list(self, request, pk=None): return Response( { - "project": reverse('API:_api_v2_project-list', request=request), + "project": reverse('v2:_api_v2_project-list', request=request), } ) diff --git a/app/settings/serializers/app_settings.py b/app/settings/serializers/app_settings.py index a743bec1d..b55500e88 100644 --- a/app/settings/serializers/app_settings.py +++ b/app/settings/serializers/app_settings.py @@ -17,7 +17,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_app_settings-detail", format="html" + view_name="v2:_api_v2_app_settings-detail", format="html" ) class Meta: @@ -47,7 +47,7 @@ class AppSettingsModelSerializer(AppSettingsBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_app_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_app_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), } diff --git a/app/settings/serializers/external_links.py b/app/settings/serializers/external_links.py index 32027c4a6..2039a8db1 100644 --- a/app/settings/serializers/external_links.py +++ b/app/settings/serializers/external_links.py @@ -17,7 +17,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_external_link-detail", format="html" + view_name="v2:_api_v2_external_link-detail", format="html" ) class Meta: @@ -47,16 +47,16 @@ class ExternalLinkModelSerializer(ExternalLinkBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_external_link-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_external_link-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), 'history': reverse( - "API:_api_v2_model_history-list", + "v2:_api_v2_model_history-list", request=self._context['view'].request, kwargs={ 'model_class': self.Meta.model._meta.model_name, 'model_id': item.pk } ), - 'notes': reverse("API:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'notes': reverse("v2:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), } diff --git a/app/settings/serializers/user_settings.py b/app/settings/serializers/user_settings.py index 0e2ea20d7..5536ee3c6 100644 --- a/app/settings/serializers/user_settings.py +++ b/app/settings/serializers/user_settings.py @@ -17,7 +17,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_user_settings-detail", format="html" + view_name="v2:_api_v2_user_settings-detail", format="html" ) class Meta: @@ -47,7 +47,7 @@ class UserSettingsModelSerializer(UserSettingsBaseSerializer): def get_url(self, item): return { - '_self': reverse("API:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': reverse("v2:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), } diff --git a/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py b/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py index 8fc037728..94e89ff17 100644 --- a/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py +++ b/app/settings/tests/unit/app_settings/test_app_settings_api_v2.py @@ -75,7 +75,7 @@ def setUpTestData(self): self.url_view_kwargs = {'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_app_settings-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_app_settings-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/settings/tests/unit/app_settings/test_app_settings_viewset.py b/app/settings/tests/unit/app_settings/test_app_settings_viewset.py index 42ee833df..dd6e0830a 100644 --- a/app/settings/tests/unit/app_settings/test_app_settings_viewset.py +++ b/app/settings/tests/unit/app_settings/test_app_settings_viewset.py @@ -28,7 +28,7 @@ class AppSettingsPermissionsAPI( model = AppSettings - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_app_settings' diff --git a/app/settings/tests/unit/test_settings_viewset.py b/app/settings/tests/unit/test_settings_viewset.py index 3d3c69e29..db9effb6b 100644 --- a/app/settings/tests/unit/test_settings_viewset.py +++ b/app/settings/tests/unit/test_settings_viewset.py @@ -17,7 +17,7 @@ class SettingsViewset( viewset = Index - route_name = 'API:_api_v2_settings_home' + route_name = 'v2:_api_v2_settings_home' @classmethod diff --git a/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py b/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py index b450b0e99..eddb30468 100644 --- a/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py +++ b/app/settings/tests/unit/user_settings/test_user_settings_api_v2.py @@ -78,7 +78,7 @@ def setUpTestData(self): self.url_view_kwargs = {'pk': self.item.id} client = Client() - url = reverse('API:_api_v2_user_settings-detail', kwargs=self.url_view_kwargs) + url = reverse('v2:_api_v2_user_settings-detail', kwargs=self.url_view_kwargs) client.force_login(self.view_user) diff --git a/app/settings/tests/unit/user_settings/test_user_settings_viewset.py b/app/settings/tests/unit/user_settings/test_user_settings_viewset.py index 77897e93f..85044dd8a 100644 --- a/app/settings/tests/unit/user_settings/test_user_settings_viewset.py +++ b/app/settings/tests/unit/user_settings/test_user_settings_viewset.py @@ -26,7 +26,7 @@ class UserSettingsPermissionsAPI( model = UserSettings - app_namespace = 'API' + app_namespace = 'v2' url_name = '_api_v2_user_settings' diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index 10e381b09..eda9f543e 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -107,20 +107,20 @@ def list(self, request, pk=None): return Response( { - "app_settings": reverse('API:_api_v2_app_settings-detail', request=request, kwargs={'pk': 1}), - "celery_log": reverse('API:_api_v2_celery_log-list', request=request), - "cluster_type": reverse('API:_api_v2_cluster_type-list', request=request), - "device_model": reverse('API:_api_v2_device_model-list', request=request), - "device_type": reverse('API:_api_v2_device_type-list', request=request), - "external_link": reverse('API:_api_v2_external_link-list', request=request), - "knowledge_base_category": reverse('API:_api_v2_knowledge_base_category-list', request=request), - "manufacturer": reverse('API:_api_v2_manufacturer-list', request=request), - "port": reverse('API:_api_v2_port-list', request=request), - "project_state": reverse('API:_api_v2_project_state-list', request=request), - "project_type": reverse('API:_api_v2_project_type-list', request=request), - "software_category": reverse('API:_api_v2_software_category-list', request=request), + "app_settings": reverse('v2:_api_v2_app_settings-detail', request=request, kwargs={'pk': 1}), + "celery_log": reverse('v2:_api_v2_celery_log-list', request=request), + "cluster_type": reverse('v2:_api_v2_cluster_type-list', request=request), + "device_model": reverse('v2:_api_v2_device_model-list', request=request), + "device_type": reverse('v2:_api_v2_device_type-list', request=request), + "external_link": reverse('v2:_api_v2_external_link-list', request=request), + "knowledge_base_category": reverse('v2:_api_v2_knowledge_base_category-list', request=request), + "manufacturer": reverse('v2:_api_v2_manufacturer-list', request=request), + "port": reverse('v2:_api_v2_port-list', request=request), + "project_state": reverse('v2:_api_v2_project_state-list', request=request), + "project_type": reverse('v2:_api_v2_project_type-list', request=request), + "software_category": reverse('v2:_api_v2_software_category-list', request=request), "user_settings": reverse( - 'API:_api_v2_user_settings-detail', + 'v2:_api_v2_user_settings-detail', request=request, kwargs={ 'pk': request.user.id diff --git a/app/templates/base.html.j2 b/app/templates/base.html.j2 index ac1b20b49..cb6b8b529 100644 --- a/app/templates/base.html.j2 +++ b/app/templates/base.html.j2 @@ -133,7 +133,7 @@ section h2 span svg { {% include 'icons/api.svg' %} - + {% include 'icons/swagger_docs.svg' %} From d137ea663a523aa673056f765e797d6570b64d8f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 19:54:29 +0930 Subject: [PATCH 340/617] docs(api): Add filter and remove footer schemas from swagger ui ref: #248 #265 --- app/app/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/app/settings.py b/app/app/settings.py index 7680eb7be..345224c3a 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -320,6 +320,10 @@ 'SERVE_INCLUDE_SCHEMA': False, 'SWAGGER_UI_DIST': 'SIDECAR', 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', + "SWAGGER_UI_SETTINGS": '''{ + filter: true, + defaultModelsExpandDepth: -1, + }''', 'REDOC_DIST': 'SIDECAR', 'PREPROCESSING_HOOKS': [ 'drf_spectacular.hooks.preprocess_exclude_path_format' From 68c22966bc8ddef148e1e12952772c51e2cfd6df Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 20:19:31 +0930 Subject: [PATCH 341/617] feat(core): Add Base Ticket Serializer and ViewSet ref: #248 #365 --- app/core/serializers/ticket.py | 166 +++++++++++++++++++++++++++++++ app/core/viewsets/ticket.py | 172 +++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 app/core/serializers/ticket.py create mode 100644 app/core/viewsets/ticket.py diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py new file mode 100644 index 000000000..7ac063c40 --- /dev/null +++ b/app/core/serializers/ticket.py @@ -0,0 +1,166 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer +from access.serializers.teams import TeamBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from core.models.ticket.ticket import Ticket + +from core.fields.badge import Badge, BadgeField + + + +class TicketBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + + url = serializers.SerializerMethodField('my_url') + + def my_url(self, item): + + context = self.context.copy() + + return reverse( + "v2:_api_v2_ticket_" + str(item.get_ticket_type_display()).lower() + "-detail", + request=context['view'].request, + kwargs={ + 'pk': item.pk + } + ) + + + class Meta: + + model = Ticket + + fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + +class TicketModelSerializer(TicketBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + context = self.context.copy() + + return { + '_self': reverse( + "v2:_api_v2_ticket_" + str(item.get_ticket_type_display()).lower() + "-detail", + request=context['view'].request, + kwargs={ + 'pk': item.pk + } + ), + } + + + duration = serializers.IntegerField(source='duration_ticket', read_only=True) + + status_badge = BadgeField(label='Status') + + + class Meta: + """Ticket Model Base Meta + + This class specifically has only `id` in fields and all remaining fields + as ready only so as to prevent using this serializer directly. The intent + is that for each ticket type there is a seperate serializer for that ticket + type. + + These serializers are for items that are common for ALL tickets. + """ + + model = Ticket + + fields = [ + 'id', + '_urls', + ] + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'title', + 'description', + 'estimate', + 'duration', + 'urgency', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + def is_valid(self, *, raise_exception=False): + + is_valid: bool = False + + is_valid = super().is_valid(raise_exception=raise_exception) + + try: + + self.validated_data['ticket_type'] = self._context['view'].ticket_type_id + + except: + + is_valid = False + + + return is_valid + + + +class TicketViewSerializer(TicketModelSerializer): + + assigned_users = UserBaseSerializer(many=True, label='Assigned Users') + + assigned_teams = TeamBaseSerializer(many=True) + + opened_by = UserBaseSerializer() + + subscribed_users = UserBaseSerializer(many=True) + + subscribed_teams = TeamBaseSerializer(many=True) + + organization = OrganizationBaseSerializer(many=False, read_only=True) diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py new file mode 100644 index 000000000..e17cc71f5 --- /dev/null +++ b/app/core/viewsets/ticket.py @@ -0,0 +1,172 @@ +from api.viewsets.common import ModelViewSet + +from core.serializers.ticket import ( + Ticket, +) + +from settings.models.user_settings import UserSettings + + +class TicketViewSet(ModelViewSet): + + filterset_fields = [ + 'category', + 'external_system', + 'impact', + 'milestone', + 'organization', + 'priority', + 'project', + 'status', + 'urgency', + ] + + search_fields = [ + 'title', + 'description', + ] + + model = Ticket + + _ticket_type_id: int = None + + _ticket_type: str = None + """Name for type of ticket + + String is Camel Cased. + """ + + + def get_queryset(self): + + self.get_ticket_type() + + queryset = super().get_queryset().filter( + ticket_type = self._ticket_type_id + ) + + self.queryset = queryset + + return self.queryset + + + def get_ticket_type(self) -> None: + + ticket_type_id: int = None + + if self._ticket_type_id is None: + + ticket_types = [e for e in Ticket.TicketType] + + for i in range( 1, len(ticket_types) ): + + if self._ticket_type.lower() == str(ticket_types[i - 1].label).lower(): + + ticket_type_id = i + + break + + self._ticket_type_id = ticket_type_id + + + def get_serializer_class(self): + + serializer_prefix:str = None + + self.get_ticket_type() + + serializer_prefix = self._ticket_type + + + if ( + self.action == 'create' + or self.action == 'list' + ): + + + if ( + self.action == 'create' + or self.action == 'list' + ): + + user_settings = UserSettings.objects.get( + user = self.request.user + ) + + organization = user_settings.default_organization.id + + + if ( + self.action == 'create' + ): + + if self.request.data is not None: + + if 'organization' in self.request.data: + + organization = int(self.request.data['organization']) + + + if ( # Must be first as the priority to pickup + self._ticket_type + and self.action != 'list' + and self.action != 'retrieve' + ): + + + if self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.import_ticket_request' + ] + ): + + serializer_prefix = self._ticket_type + 'Import' + + elif self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.triage_ticket_request' + ] + ): + + serializer_prefix = self._ticket_type + 'Triage' + + elif self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.change_ticket_request' + ] + ): + + serializer_prefix = self._ticket_type + 'Change' + + elif self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.add_ticket_request' + ] + ): + + serializer_prefix = self._ticket_type + 'Add' + + elif self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.view_ticket_request' + ] + ): + + serializer_prefix = self._ticket_type + 'View' + + + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[serializer_prefix + 'TicketViewSerializer'] + + + return globals()[serializer_prefix + 'TicketModelSerializer'] From 5fe2269e98aff872d6a8e89434b9836c2f1f0d60 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 20:21:25 +0930 Subject: [PATCH 342/617] feat(api): Custom exception UnknownTicketType for use when attempting to detect ticket type ref: #248 #365 --- app/api/exceptions.py | 8 ++++++++ app/core/serializers/ticket.py | 3 +++ app/core/viewsets/ticket.py | 6 ++++++ 3 files changed, 17 insertions(+) create mode 100644 app/api/exceptions.py diff --git a/app/api/exceptions.py b/app/api/exceptions.py new file mode 100644 index 000000000..42f168c51 --- /dev/null +++ b/app/api/exceptions.py @@ -0,0 +1,8 @@ +from rest_framework.exceptions import APIException +from rest_framework import status + + +class UnknownTicketType(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = 'Unable to determin the ticket type.' + default_code = 'unknown_ticket_type' diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 7ac063c40..5ae45510d 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -6,6 +6,7 @@ from app.serializers.user import UserBaseSerializer +from api.exceptions import UnknownTicketType from core.models.ticket.ticket import Ticket from core.fields.badge import Badge, BadgeField @@ -146,6 +147,8 @@ def is_valid(self, *, raise_exception=False): is_valid = False + raise UnknownTicketType() + return is_valid diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index e17cc71f5..5f4809b2b 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -1,3 +1,4 @@ +from api.exceptions import UnknownTicketType from api.viewsets.common import ModelViewSet from core.serializers.ticket import ( @@ -69,6 +70,11 @@ def get_ticket_type(self) -> None: self._ticket_type_id = ticket_type_id + if self._ticket_type_id is None: + + raise UnknownTicketType() + + def get_serializer_class(self): serializer_prefix:str = None From d8e64372418e26533329dff22809fd3048eea97d Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 20:28:08 +0930 Subject: [PATCH 343/617] feat(assistance): Add Request Ticket API v2 endpoint ref: #248 #365 --- app/api/urls_v2.py | 2 + app/assistance/serializers/request.py | 225 ++++++++++++++++++++++++++ app/assistance/viewsets/request.py | 95 +++++++++++ 3 files changed, 322 insertions(+) create mode 100644 app/assistance/serializers/request.py create mode 100644 app/assistance/viewsets/request.py diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index d88ca648c..75a1637ea 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -26,6 +26,7 @@ index as assistance_index_v2, knowledge_base as knowledge_base_v2, knowledge_base_category as knowledge_base_category_v2, + request as request_ticket_v2, ) from config_management.viewsets import ( @@ -94,6 +95,7 @@ router.register('assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') router.register('assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base') +router.register('assistance/ticket/request', request_ticket_v2.ViewSet, basename='_api_v2_ticket_request') router.register('base', base_index_v2.Index, basename='_api_v2_base_home') diff --git a/app/assistance/serializers/request.py b/app/assistance/serializers/request.py new file mode 100644 index 000000000..6e79bdf50 --- /dev/null +++ b/app/assistance/serializers/request.py @@ -0,0 +1,225 @@ +from rest_framework import serializers + +from app.serializers.user import UserBaseSerializer + +from core.serializers.ticket import ( + Ticket, + TicketBaseSerializer, + TicketModelSerializer, + TicketViewSerializer +) + + + +class RequestTicketBaseSerializer( + TicketBaseSerializer +): + + class Meta( TicketBaseSerializer.Meta ): + + pass + + + +class RequestTicketModelSerializer( + RequestTicketBaseSerializer, + TicketModelSerializer, +): + + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Request]) + + class Meta( TicketModelSerializer.Meta ): + + fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'title', + 'description', + 'estimate', + 'duration', + 'urgency', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'external_ref', + 'external_system', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class RequestAddTicketModelSerializer( + RequestTicketModelSerializer, +): + """Serializer for `Add` user + + Args: + RequestTicketModelSerializer (class): Model Serializer + """ + + + class Meta(RequestTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class RequestChangeTicketModelSerializer( + RequestTicketModelSerializer, +): + """Serializer for `Change` user + + Args: + RequestTicketModelSerializer (class): Request Model Serializer + """ + + class Meta(RequestTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class RequestTriageTicketModelSerializer( + RequestTicketModelSerializer, +): + """Serializer for `Triage` user + + Args: + RequestTicketModelSerializer (class): Request Model Serializer + """ + + + class Meta(RequestTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'created', + 'modified', + 'status_badge', + 'estimate', + 'duration', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + '_urls', + ] + + + +class RequestImportTicketModelSerializer( + RequestTicketModelSerializer, +): + """Serializer for `Import` user + + Args: + RequestTicketModelSerializer (class): Request Model Serializer + """ + + class Meta(RequestTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'display_name', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class RequestTicketViewSerializer( + RequestTicketModelSerializer, + TicketViewSerializer, +): + + pass diff --git a/app/assistance/viewsets/request.py b/app/assistance/viewsets/request.py new file mode 100644 index 000000000..ea31e8c22 --- /dev/null +++ b/app/assistance/viewsets/request.py @@ -0,0 +1,95 @@ +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiResponse, + PolymorphicProxySerializer, +) + +from assistance.serializers.request import ( + RequestAddTicketModelSerializer, + RequestChangeTicketModelSerializer, + RequestTriageTicketModelSerializer, + RequestImportTicketModelSerializer, + RequestTicketModelSerializer, + RequestTicketViewSerializer +) + +from core.viewsets.ticket import TicketViewSet + +from settings.models.user_settings import UserSettings + + + +@extend_schema_view( + create = extend_schema( + versions = [ + 'v2' + ], + summary = 'Create a Request Ticket', + description="""Ticket API requests depend upon the users permission. + To view an examaple of a request, select the correct schema _Link above example, called schema_. + +Responses from the API are the same for all users when the request returns + status `HTTP/20x`. + """, + request = PolymorphicProxySerializer( + component_name = None, + serializers=[ + RequestImportTicketModelSerializer, + RequestAddTicketModelSerializer, + RequestChangeTicketModelSerializer, + RequestTriageTicketModelSerializer, + ], + resource_type_field_name=None, + many = False + ), + responses = { + 201: OpenApiResponse(description='Created', response=RequestTicketViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a Request Ticket', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all Request Tickets', + description='', + responses = { + 200: OpenApiResponse(description='', response=RequestTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a Request Ticket', + description='', + responses = { + 200: OpenApiResponse(description='', response=RequestTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a Request Ticket', + description = '', + responses = { + 200: OpenApiResponse(description='', response=RequestTicketViewSerializer), + 400: OpenApiResponse(description='Validation failed.'), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(TicketViewSet): + """Request Ticket + + This class exists only for the purpose of swagger for documentation. + + Args: + TicketViewSet (class): Base Ticket ViewSet. + """ + + _ticket_type: str = 'Request' From 726a3ac40693a9e2170d94ae3a9f3173cd4ad0a0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 20:28:56 +0930 Subject: [PATCH 344/617] fix(api): Correct inheritance order for ModelViewSet ref: #248 #365 --- app/api/viewsets/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index 116008814..7c278986a 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -217,8 +217,8 @@ def get_serializer_class(self): class ModelViewSet( + ModelViewSetBase, viewsets.ModelViewSet, - ModelViewSetBase ): pass From 0612c2350d115a3d38a7ed45ac1a03b13989f3cc Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 20:29:34 +0930 Subject: [PATCH 345/617] fix(api): Ensure read-only fields have choices added to metadata ref: #248 #365 --- app/api/react_ui_metadata.py | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 026368f40..6a4a06768 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -1,8 +1,12 @@ +from django.utils.encoding import force_str + from rest_framework import serializers from rest_framework_json_api.metadata import JSONAPIMetadata from rest_framework.request import clone_request from rest_framework.utils.field_mapping import ClassLookupDict +from rest_framework_json_api.utils import get_related_resource_type + from app.serializers.user import User, UserBaseSerializer from core.fields.badge import BadgeField @@ -210,3 +214,82 @@ def determine_metadata(self, request, view): 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 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) + + field_info["required"] = getattr(field, "required", False) + + 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 ( + # not field_info.get("read_only") + not field_info.get("relationship_resource") + and 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 + ) + + return field_info \ No newline at end of file From f5eb4c25b2cb1cb468a7236646f078f326973c3b Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 20:54:50 +0930 Subject: [PATCH 346/617] feat(core): Add Ticket Category API v2 endpoint ref: #248 #365 --- app/core/serializers/ticket.py | 3 + app/core/serializers/ticket_category.py | 71 ++++++++++++++++++++++ app/core/viewsets/ticket.py | 8 +++ app/core/viewsets/ticket_category.py | 79 +++++++++++++++++++++++++ app/settings/viewsets/index.py | 5 ++ 5 files changed, 166 insertions(+) create mode 100644 app/core/serializers/ticket_category.py create mode 100644 app/core/viewsets/ticket_category.py diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 5ae45510d..6b85cd7c7 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -10,6 +10,7 @@ from core.models.ticket.ticket import Ticket from core.fields.badge import Badge, BadgeField +from core.serializers.ticket_category import TicketCategoryBaseSerializer @@ -160,6 +161,8 @@ class TicketViewSerializer(TicketModelSerializer): assigned_teams = TeamBaseSerializer(many=True) + category = TicketCategoryBaseSerializer() + opened_by = UserBaseSerializer() subscribed_users = UserBaseSerializer(many=True) diff --git a/app/core/serializers/ticket_category.py b/app/core/serializers/ticket_category.py new file mode 100644 index 000000000..d4f1a0a16 --- /dev/null +++ b/app/core/serializers/ticket_category.py @@ -0,0 +1,71 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.models.ticket.ticket_category import TicketCategory + + + +class TicketCategoryBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="v2:_api_v2_ticket_category-detail", format="html" + ) + + class Meta: + + model = TicketCategory + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + +class TicketCategoryModelSerializer(TicketCategoryBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("API:_api_v2_ticket_category-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + } + + + class Meta: + + model = TicketCategory + + fields = '__all__' + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCategoryViewSerializer(TicketCategoryModelSerializer): + + organization = OrganizationBaseSerializer(many=False, read_only=True) diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 5f4809b2b..9c2b52e58 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -1,6 +1,14 @@ from api.exceptions import UnknownTicketType from api.viewsets.common import ModelViewSet +from assistance.serializers.request import ( + RequestAddTicketModelSerializer, + RequestChangeTicketModelSerializer, + RequestTriageTicketModelSerializer, + RequestImportTicketModelSerializer, + RequestTicketModelSerializer, + RequestTicketViewSerializer +) from core.serializers.ticket import ( Ticket, ) diff --git a/app/core/viewsets/ticket_category.py b/app/core/viewsets/ticket_category.py new file mode 100644 index 000000000..348919d3e --- /dev/null +++ b/app/core/viewsets/ticket_category.py @@ -0,0 +1,79 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from api.viewsets.common import ModelViewSet + +from core.serializers.ticket_category import ( + TicketCategory, + TicketCategoryModelSerializer, + TicketCategoryViewSerializer +) + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a ticket category', + description='', + responses = { + 201: OpenApiResponse(description='Created', response=TicketCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a ticket category', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all ticket categories', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single ticket category', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a ticket category', + description = '', + responses = { + 200: OpenApiResponse(description='', response=TicketCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelViewSet): + + filterset_fields = [ + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = TicketCategory + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index eda9f543e..f36563d96 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -50,6 +50,10 @@ class Index(CommonViewSet): { "name": "External Links", "model": "external_link" + }, + { + "name": "Ticket Category", + "model": "ticket_category" } ] }, @@ -119,6 +123,7 @@ def list(self, request, pk=None): "project_state": reverse('v2:_api_v2_project_state-list', request=request), "project_type": reverse('v2:_api_v2_project_type-list', request=request), "software_category": reverse('v2:_api_v2_software_category-list', request=request), + "ticket_category": reverse('v2:_api_v2_ticket_category-list', request=request), "user_settings": reverse( 'v2:_api_v2_user_settings-detail', request=request, From 34a9d202c3c823f92a937719e696a7d4f5b555f8 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 23:05:20 +0930 Subject: [PATCH 347/617] fix(core): Correct ticket comment model name ref: #248 #365 --- app/core/lib/slash_commands/duration.py | 2 +- .../0014_alter_ticketcomment_options.py | 17 +++++++++++++++++ app/core/models/ticket/ticket_comment.py | 5 +++-- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 app/core/migrations/0014_alter_ticketcomment_options.py diff --git a/app/core/lib/slash_commands/duration.py b/app/core/lib/slash_commands/duration.py index 6dca0c918..db54409f3 100644 --- a/app/core/lib/slash_commands/duration.py +++ b/app/core/lib/slash_commands/duration.py @@ -82,7 +82,7 @@ def command_duration(self, match) -> str: user = self.opened_by, ) - elif str(self._meta.verbose_name).lower() == 'comment': + elif str(self._meta.verbose_name).lower().replace(' ', '_') == 'ticket_comment': self.duration = duration diff --git a/app/core/migrations/0014_alter_ticketcomment_options.py b/app/core/migrations/0014_alter_ticketcomment_options.py new file mode 100644 index 000000000..6adff2e1f --- /dev/null +++ b/app/core/migrations/0014_alter_ticketcomment_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2024-10-26 13:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_alter_manufacturer_modified'), + ] + + operations = [ + migrations.AlterModelOptions( + name='ticketcomment', + options={'ordering': ['created', 'ticket', 'parent_id'], 'verbose_name': 'Comment', 'verbose_name_plural': 'Comments'}, + ), + ] diff --git a/app/core/models/ticket/ticket_comment.py b/app/core/models/ticket/ticket_comment.py index c2f95fe76..e424c1e1d 100644 --- a/app/core/models/ticket/ticket_comment.py +++ b/app/core/models/ticket/ticket_comment.py @@ -22,15 +22,16 @@ class TicketComment( class Meta: ordering = [ + 'created', 'ticket', 'parent_id' ] unique_together = ('external_system', 'external_ref',) - verbose_name = "Comment" + verbose_name = "Ticket Comment" - verbose_name_plural = "Comments" + verbose_name_plural = "Ticket Comments" From a4bfb3a7e8a18de981a38b4cd7ace2460fc1e88f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 26 Oct 2024 23:07:10 +0930 Subject: [PATCH 348/617] fix(core): Add Ticket Category API v2 endpoint to urls ref: #248 #365 --- app/api/urls_v2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 75a1637ea..b652d3ba5 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -36,10 +36,11 @@ ) from core.viewsets import ( + celery_log as celery_log_v2, history as history_v2, - notes as notes_v2, manufacturer as manufacturer_v2, - celery_log as celery_log_v2 + notes as notes_v2, + ticket_category, ) from itam.viewsets import ( @@ -156,6 +157,7 @@ router.register('settings/project_state', project_state_v2.ViewSet, basename='_api_v2_project_state') router.register('settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type') router.register('settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') +router.register('settings/ticket_category', ticket_category.ViewSet, basename='_api_v2_ticket_category') router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') From 4542301446ad00ad8cc08839b00c1a64026c4bbd Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 14:34:58 +0930 Subject: [PATCH 349/617] feat(core): Add Ticket Comment API v2 endpoint ref: #248 #365 --- app/api/urls_v2.py | 4 + app/app/settings.py | 1 + app/assistance/viewsets/request.py | 2 +- app/core/serializers/ticket.py | 1 + app/core/serializers/ticket_comment.py | 427 +++++++++++++++++++++++++ app/core/viewsets/ticket.py | 1 + app/core/viewsets/ticket_comment.py | 181 +++++++++++ 7 files changed, 616 insertions(+), 1 deletion(-) create mode 100644 app/core/serializers/ticket_comment.py create mode 100644 app/core/viewsets/ticket_comment.py diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index b652d3ba5..cc198edef 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -41,6 +41,8 @@ manufacturer as manufacturer_v2, notes as notes_v2, ticket_category, + ticket_comment, + ) from itam.viewsets import ( @@ -97,6 +99,8 @@ router.register('assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') router.register('assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base') router.register('assistance/ticket/request', request_ticket_v2.ViewSet, basename='_api_v2_ticket_request') +router.register('assistance/ticket/request/(?P[0-9]+)/comments', ticket_comment.ViewSet, basename='_api_v2_ticket_request_comments') +router.register('assistance/ticket/request/(?P[0-9]+)/comments/(?P[0-9]+)/threads', ticket_comment.ViewSet, basename='_api_v2_ticket_request_comment_threads') router.register('base', base_index_v2.Index, basename='_api_v2_base_home') diff --git a/app/app/settings.py b/app/app/settings.py index 345224c3a..d7e1cf873 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -323,6 +323,7 @@ "SWAGGER_UI_SETTINGS": '''{ filter: true, defaultModelsExpandDepth: -1, + deepLinking: true, }''', 'REDOC_DIST': 'SIDECAR', 'PREPROCESSING_HOOKS': [ diff --git a/app/assistance/viewsets/request.py b/app/assistance/viewsets/request.py index ea31e8c22..d77b848bf 100644 --- a/app/assistance/viewsets/request.py +++ b/app/assistance/viewsets/request.py @@ -33,7 +33,7 @@ status `HTTP/20x`. """, request = PolymorphicProxySerializer( - component_name = None, + component_name = 'Ticket', serializers=[ RequestImportTicketModelSerializer, RequestAddTicketModelSerializer, diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 6b85cd7c7..ab2268964 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -73,6 +73,7 @@ def get_url(self, item): 'pk': item.pk } ), + 'comments': reverse('v2:_api_v2_ticket_' + str(item.get_ticket_type_display()).lower() + '_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), } diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py new file mode 100644 index 000000000..1e3dd58e9 --- /dev/null +++ b/app/core/serializers/ticket_comment.py @@ -0,0 +1,427 @@ +from rest_framework.reverse import reverse + +from rest_framework import serializers +from rest_framework.fields import empty + +from access.serializers.organization import OrganizationBaseSerializer +from access.serializers.teams import TeamBaseSerializer + +from api.exceptions import UnknownTicketType + +from app.serializers.user import UserBaseSerializer + +from core.models.ticket.ticket_comment import Ticket, TicketComment + + + +class TicketCommentBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="API:_api_v2_device-detail", format="html" + ) + + class Meta: + + model = TicketComment + + fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'name', + 'url', + ] + + + +class TicketCommentModelSerializer(TicketCommentBaseSerializer): + """Base class for Ticket Comment Model + + Args: + TicketCommentBaseSerializer (class): Base class for ALL commment types. + + Raises: + UnknownTicketType: Ticket type is undetermined. + """ + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + request = self.context.get('request') + + if item.ticket: + + ticket_type_name = item.ticket.get_ticket_type_display() + + ticket_id = item.ticket.id + + else: + + raise UnknownTicketType() + + + urls: dict = { + '_self': reverse( + 'API:_api_v2_ticket_' + str(ticket_type_name).lower().replace(' ', '_') + '_comments-detail', + request = self._context['view'].request, + kwargs={ + 'ticket_id': ticket_id, + 'pk': item.id + } + ) + } + + threads = TicketComment.objects.filter(parent = item.id, ticket = ticket_id) + + if len(threads) > 0: + + urls.update({ + 'threads': reverse( + 'API:_api_v2_ticket_' + str(ticket_type_name).lower().replace(' ', '_') + '_comment_threads-list', + request = self._context['view'].request, + kwargs={ + 'ticket_id': ticket_id, + 'parent_id': item.id + } + ) + }) + + return urls + + + class Meta: + + model = TicketComment + + fields = '__all__' + + fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + 'body', + 'private', + 'duration', + 'category', + 'template', + 'is_template', + 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + 'private', + 'duration', + 'category', + 'template', + 'is_template', + 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + + + def __init__(self, instance=None, data=empty, **kwargs): + + if 'context' in self._kwargs: + + if 'view' in self._kwargs['context']: + + if 'ticket_id' in self._kwargs['context']['view'].kwargs: + + ticket = Ticket.objects.get(pk=int(self._kwargs['context']['view'].kwargs['ticket_id'])) + self.fields.fields['organization'].initial = ticket.organization.id + + self.fields.fields['ticket'].initial = ticket.id + + self.fields.fields['comment_type'].initial = TicketComment.CommentType.COMMENT + + self.fields.fields['user'].initial = kwargs['context']['request']._user.id + + super().__init__(instance=instance, data=data, **kwargs) + + + +class TicketCommentITILModelSerializer(TicketCommentModelSerializer): + """ITIL Comment Model Base + + This serializer is the base for ALL ITIL comment Types. + + Args: + TicketCommentModelSerializer (class): Base comment class for ALL comments + """ + + class Meta(TicketCommentModelSerializer.Meta): + + fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + 'body', + 'private', + 'duration', + 'category', + 'template', + 'is_template', + 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + 'body', + 'private', + 'duration', + 'category', + 'template', + 'is_template', + 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCommentITILFollowUpModelSerializer(TicketCommentITILModelSerializer): + """ITIL Followup Comment + + Args: + TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. + """ + + class Meta(TicketCommentITILModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + # 'body', + 'private', + 'duration', + # 'category', + # 'template', + 'is_template', + # 'source', + 'status', + # 'responsible_user', + # 'responsible_team', + 'user', + # 'planned_start_date', + # 'planned_finish_date', + # # 'real_start_date', + # 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCommentITILTaskModelSerializer(TicketCommentITILModelSerializer): + """ITIL Task Comment + + Args: + TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. + """ + + class Meta(TicketCommentITILModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + # 'body', + 'private', + 'duration', + # 'category', + # 'template', + 'is_template', + 'source', + 'status', + # 'responsible_user', + # 'responsible_team', + 'user', + # 'planned_start_date', + # 'planned_finish_date', + # 'real_start_date', + # 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCommentITILSolutionModelSerializer(TicketCommentITILModelSerializer): + """ITIL Solution Comment + + Args: + TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. + """ + + class Meta(TicketCommentITILModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + 'comment_type', + 'private', + 'duration', + 'is_template', + 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCommentImportModelSerializer(TicketCommentModelSerializer): + """Import User Serializer + + Args: + TicketCommentModelSerializer (class): Base class for ALL comment types. + """ + + class Meta(TicketCommentModelSerializer.Meta): + + read_only_fields = [ + 'id', + # 'parent', + # 'ticket', + # 'external_ref', + # 'external_system', + # 'comment_type', + # 'body', + # 'private', + 'duration', + # 'category', + # 'template', + # 'is_template', + # 'source', + # 'status', + # 'responsible_user', + # 'responsible_team', + # 'user', + # 'planned_start_date', + # 'planned_finish_date', + # 'real_start_date', + # 'real_finish_date', + # 'organization', + # 'date_closed', + # 'created', + # 'modified', + '_urls', + ] + + + +class TicketCommentViewSerializer(TicketCommentModelSerializer): + + organization = OrganizationBaseSerializer( many = False, read_only = True ) + + user = UserBaseSerializer() + + responsible_user = UserBaseSerializer() + + responsible_team = TeamBaseSerializer() diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 9c2b52e58..063d06672 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -9,6 +9,7 @@ RequestTicketModelSerializer, RequestTicketViewSerializer ) + from core.serializers.ticket import ( Ticket, ) diff --git a/app/core/viewsets/ticket_comment.py b/app/core/viewsets/ticket_comment.py new file mode 100644 index 000000000..934780684 --- /dev/null +++ b/app/core/viewsets/ticket_comment.py @@ -0,0 +1,181 @@ +from django.db.models import Q +from django.shortcuts import get_object_or_404 + +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, PolymorphicProxySerializer + +from rest_framework import generics, viewsets +from rest_framework.response import Response + +from access.mixin import OrganizationMixin + +from api.views.mixin import OrganizationPermissionAPI +from api.viewsets.common import ModelViewSet + +from core.serializers.ticket_comment import ( + Ticket, + TicketComment, + TicketCommentImportModelSerializer, + TicketCommentITILFollowUpModelSerializer, + TicketCommentITILSolutionModelSerializer, + TicketCommentITILTaskModelSerializer, + TicketCommentModelSerializer, + TicketCommentViewSerializer +) + +from settings.models.user_settings import UserSettings + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a ticket comment', + description="""Ticket Comment API requests depend upon the users permission and comment type. + To view an examaple of a request, select the correct schema _Link above example, called schema_. + +Responses from the API are the same for all users when the request returns + status `HTTP/20x`. + """, + request = PolymorphicProxySerializer( + component_name = 'TicketComment', + serializers=[ + TicketCommentImportModelSerializer, + TicketCommentITILFollowUpModelSerializer, + TicketCommentITILSolutionModelSerializer, + TicketCommentITILTaskModelSerializer, + TicketCommentModelSerializer + ], + resource_type_field_name=None, + many = False + ), + responses = { + 201: OpenApiResponse(description='Created', response=TicketCommentViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a ticket comment', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all ticket comments', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketCommentViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single ticket comment', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketCommentViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a ticket comment', + description = '', + responses = { + 200: OpenApiResponse(description='', response=TicketCommentViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelViewSet): + + filterset_fields = [ + 'category', + 'external_system', + 'organization', + ] + + search_fields = [ + 'body', + ] + + model = TicketComment + + + def get_queryset(self): + + queryset = super().get_queryset() + + if 'parent_id' in self.kwargs: + + queryset = queryset.filter(parent=self.kwargs['parent_id']) + + else: + + queryset = queryset.filter(parent=None) + + + if 'ticket_id' in self.kwargs: + + queryset = queryset.filter(ticket=self.kwargs['ticket_id']) + + if 'pk' in self.kwargs: + + queryset = queryset.filter(pk = self.kwargs['pk']) + + self.queryset = queryset + + return self.queryset + + + def get_serializer_class(self): + + organization:int = None + + serializer_prefix:str = 'TicketComment' + + if ( + self.action == 'create' + ): + + ticket = Ticket.objects.get(pk = int(self.kwargs['ticket_id'])) + + organization = int(ticket.organization.id) + + if organization: + + if self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.import_ticketcomment' + ] + ): + + serializer_prefix = serializer_prefix + 'Import' + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str(serializer_prefix).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str(serializer_prefix).replace(' ', '') + 'ModelSerializer'] + + + + def get_view_name(self): + + if hasattr(self, 'kwargs'): + + if 'parent_id' in self.kwargs: + + if self.detail: + return "Ticket Comment Thread" + + return 'Ticket Comment Threads' + + if self.detail: + return "Ticket Comment" + + return 'Ticket Comments' From d57d4ad96a23f33a846307d234cc4013fefbf383 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:49:18 +0930 Subject: [PATCH 350/617] feat(config_management): Add url function to Config Groups model ref: #248 #365 #366 --- app/config_management/models/groups.py | 9 +++++++++ .../serializers/config_group.py | 16 ++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index dc9b24c1c..21b48a127 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -3,6 +3,8 @@ from django.db import models from django.forms import ValidationError +from rest_framework.reverse import reverse + from access.fields import * from access.models import TenancyObject @@ -238,6 +240,13 @@ def count_children(self) -> int: return count + def get_url( self, request = None ) -> str: + + if request: + + return reverse("v2:_api_v2_config_group-detail", request=request, kwargs={'pk': self.id}) + + return reverse("v2:_api_v2_config_group-detail", kwargs={'pk': self.id}) @property diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 481f798a4..7c43424b1 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -21,13 +21,7 @@ def get_display_name(self, item): def get_url(self, item): - return reverse( - "v2:_api_v2_config_group-detail", - request=self.context['view'].request, - kwargs={ - 'pk': item.pk - } - ) + return item.get_url( request = self._context['view'].request ) class Meta: @@ -58,13 +52,7 @@ class ConfigGroupModelSerializer(ConfigGroupBaseSerializer): def get_url(self, item): return { - '_self': reverse( - 'v2:_api_v2_config_group-detail', - request = self.context['view'].request, - kwargs = { - 'pk': item.pk - } - ), + '_self': item.get_url( request = self._context['view'].request ), 'child_groups': reverse( 'v2:_api_v2_config_group_child-list', request = self.context['view'].request, From 43f90251b08d8087629d63999a8dcab7721db81a Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:50:01 +0930 Subject: [PATCH 351/617] feat(itam): Add url function to Device model ref: #248 #365 #366 --- app/itam/models/device.py | 10 ++++++++++ app/itam/serializers/device.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 2ffc9e28b..492086cac 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -7,6 +7,7 @@ from django.forms import ValidationError from rest_framework import serializers +from rest_framework.reverse import reverse from access.fields import * from access.models import TenancyObject @@ -310,6 +311,15 @@ def validate_hostname_format(self): ] + def get_url( self, request = None ) -> str: + + if request: + + return reverse("v2:_api_v2_device-detail", request=request, kwargs={'pk': self.id}) + + return reverse("v2:_api_v2_device-detail", kwargs={'pk': self.id}) + + def save( self, force_insert=False, force_update=False, using=None, update_fields=None ): diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 86bd85538..1bac79073 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -55,7 +55,7 @@ class DeviceModelSerializer(DeviceBaseSerializer): def get_url(self, item): return { - '_self': reverse("v2:_api_v2_device-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': item.get_url( request = self._context['view'].request ), 'device_model': reverse("v2:_api_v2_device_model-list", request=self._context['view'].request), 'device_type': reverse("v2:_api_v2_device_type-list", request=self._context['view'].request), 'external_links': reverse("v2:_api_v2_external_link-list", request=self._context['view'].request) + '?devices=true', From e6a5e446ab6b62526eae4eb78616eefabcf9d0fb Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:50:39 +0930 Subject: [PATCH 352/617] feat(itam): Add url function to Operating System model ref: #248 #365 #366 --- app/itam/models/operating_system.py | 11 +++++++++++ app/itam/serializers/operating_system.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 5cb7afbcd..8f592eb21 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -1,5 +1,7 @@ from django.db import models +from rest_framework.reverse import reverse + from access.fields import * from access.models import TenancyObject @@ -147,6 +149,15 @@ class Meta: ] + def get_url( self, request = None ) -> str: + + if request: + + return reverse("v2:_api_v2_operating_system-detail", request=request, kwargs={'pk': self.id}) + + return reverse("v2:_api_v2_operating_system-detail", kwargs={'pk': self.id}) + + def __str__(self): return self.name diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index 64b2cce5e..15e128a84 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -51,7 +51,7 @@ class OperatingSystemModelSerializer(OperatingSystemBaseSerializer): def get_url(self, item): return { - '_self': reverse("v2:_api_v2_operating_system-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': item.get_url( request = self._context['view'].request ), 'history': reverse( "v2:_api_v2_model_history-list", request=self._context['view'].request, From 0eef42b3e66bfb46b14eff5f034b6999506bf791 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:51:04 +0930 Subject: [PATCH 353/617] feat(itam): Add url function to Software model ref: #248 #365 #366 --- app/itam/models/software.py | 11 +++++++++++ app/itam/serializers/software.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 7cd116d63..59ac3c135 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -1,5 +1,7 @@ from django.db import models +from rest_framework.reverse import reverse + from access.fields import * from access.models import TenancyObject @@ -231,6 +233,15 @@ def clean(self): self.is_global = app_settings.software_is_global + def get_url( self, request = None ) -> str: + + if request: + + return reverse("v2:_api_v2_software-detail", request=request, kwargs={'pk': self.id}) + + return reverse("v2:_api_v2_software-detail", kwargs={'pk': self.id}) + + def __str__(self): return self.name diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 5ca93fee9..6d43a943d 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -49,7 +49,7 @@ class SoftwareModelSerializer(SoftwareBaseSerializer): def get_url(self, item): return { - '_self': reverse("v2:_api_v2_software-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': item.get_url( request = self._context['view'].request ), 'external_links': reverse("v2:_api_v2_external_link-list", request=self._context['view'].request) + '?software=true', 'history': reverse( "v2:_api_v2_model_history-list", From faa368331cc2c5da9ea3afe98c793d78f16f1468 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:51:38 +0930 Subject: [PATCH 354/617] feat(itim): Add url function to Cluster model ref: #248 #365 #366 --- app/itim/models/clusters.py | 11 +++++++++++ app/itim/serializers/cluster.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index 23b2b3516..f9702096c 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -2,6 +2,8 @@ from django.db import models from django.forms import ValidationError +from rest_framework.reverse import reverse + from access.fields import * from access.models import Team, TenancyObject @@ -281,6 +283,15 @@ class Meta: ] + def get_url( self, request = None ) -> str: + + if request: + + return reverse("v2:_api_v2_cluster-detail", request=request, kwargs={'pk': self.id}) + + return reverse("v2:_api_v2_cluster-detail", kwargs={'pk': self.id}) + + @property def rendered_config(self): diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index 86fcbe1f9..7e9c2c8cf 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -19,7 +19,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="v2:_api_v2_cluster-detail", format="html" + view_name="v2:_api_v2_cluster-detail", ) class Meta: @@ -48,7 +48,7 @@ class ClusterModelSerializer(ClusterBaseSerializer): def get_url(self, item): return { - '_self': reverse("v2:_api_v2_cluster-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': item.get_url( request = self._context['view'].request ), 'history': reverse( "v2:_api_v2_model_history-list", request=self._context['view'].request, From 8f13047a1f7fa05fa856c54df712ef76db86c047 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:52:04 +0930 Subject: [PATCH 355/617] feat(itim): Add url function to Service model ref: #248 #365 #366 --- app/itim/models/services.py | 11 +++++++++++ app/itim/serializers/service.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index d5daff4d9..c79d26da3 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -4,6 +4,8 @@ from django.db import models from django.forms import ValidationError +from rest_framework.reverse import reverse + from access.fields import * from access.models import Team, TenancyObject @@ -330,6 +332,15 @@ def validate_config_key_variable(value): ] + def get_url( self, request = None ) -> str: + + if request: + + return reverse("v2:_api_v2_service-detail", request=request, kwargs={'pk': self.id}) + + return reverse("v2:_api_v2_service-detail", kwargs={'pk': self.id}) + + @property def config_variables(self): diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index aeecd36b4..ba35e3bdc 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -50,7 +50,7 @@ class ServiceModelSerializer(ServiceBaseSerializer): def get_url(self, item): return { - '_self': reverse("v2:_api_v2_service-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), + '_self': item.get_url( request = self._context['view'].request ), 'history': reverse( "v2:_api_v2_model_history-list", request=self._context['view'].request, From 0b00193bed2ed30145640c24f32e8715e832c2da Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:52:50 +0930 Subject: [PATCH 356/617] feat(core): Add url function to Ticket Linked Items model ref: #248 #365 #366 --- app/core/models/ticket/ticket_linked_items.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/core/models/ticket/ticket_linked_items.py b/app/core/models/ticket/ticket_linked_items.py index a8e508cce..fa1132b23 100644 --- a/app/core/models/ticket/ticket_linked_items.py +++ b/app/core/models/ticket/ticket_linked_items.py @@ -1,5 +1,7 @@ from django.db import models +from rest_framework.reverse import reverse + from .ticket_enum_values import TicketValues from access.models import TenancyObject @@ -72,6 +74,29 @@ class Modules(models.IntegerChoices): 'created' ] + + def get_url( self, request = None ) -> str: + + if request: + + return reverse( + "v2:_api_v2_ticket_linked_item-detail", + request=request, + kwargs={ + 'ticket_id': self.ticket.id, + 'pk': self.id + } + ) + + return reverse( + "v2:_api_v2_ticket_linked_item-detail", + kwargs={ + 'ticket_id': self.ticket.id, + 'pk': self.id + } + ) + + def __str__(self) -> str: item_type: str = None From db8a815dc0228e8ea38d58197e97ddb81d4cb7ce Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 17:55:22 +0930 Subject: [PATCH 357/617] feat(core): Add Ticket Linked Item API v2 endpoint ref: #248 #365 --- app/api/urls_v2.py | 3 +- app/core/serializers/ticket.py | 1 + app/core/serializers/ticket_linked_item.py | 193 +++++++++++++++++++++ app/core/viewsets/ticket_linked_item.py | 106 +++++++++++ 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 app/core/serializers/ticket_linked_item.py create mode 100644 app/core/viewsets/ticket_linked_item.py diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index cc198edef..5ab9153ad 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -42,6 +42,7 @@ notes as notes_v2, ticket_category, ticket_comment, + ticket_linked_item, ) @@ -117,7 +118,7 @@ router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') - +router.register('core/ticket/(?P[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item') router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('itam/device', device_v2.ViewSet, basename='_api_v2_device') diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index ab2268964..884e59da7 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -74,6 +74,7 @@ def get_url(self, item): } ), 'comments': reverse('v2:_api_v2_ticket_' + str(item.get_ticket_type_display()).lower() + '_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), + 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), } diff --git a/app/core/serializers/ticket_linked_item.py b/app/core/serializers/ticket_linked_item.py new file mode 100644 index 000000000..ddc8540f4 --- /dev/null +++ b/app/core/serializers/ticket_linked_item.py @@ -0,0 +1,193 @@ +from rest_framework.fields import empty +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from assistance.serializers.request import TicketBaseSerializer + +from core.fields.badge import BadgeField +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_linked_items import TicketLinkedItem + + + +class TicketLinkedItemBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + + url = serializers.SerializerMethodField('my_url') + + def my_url(self, item): + + return item.get_url( request = self._context['view'].request ) + + + created = serializers.DateTimeField( source = 'ticket.created', read_only = True ) + + class Meta: + + model = TicketLinkedItem + + fields = [ + 'id', + 'display_name', + 'created' + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created' + 'url', + ] + + +class TicketLinkedItemModelSerializer(TicketLinkedItemBaseSerializer): + + item = serializers.SerializerMethodField('get_item') + + def get_item(self, item) -> dict: + + base_serializer: dict = None + + if item.item_type == TicketLinkedItem.Modules.CLUSTER: + + from itim.serializers.cluster import Cluster, ClusterBaseSerializer + + base_serializer = ClusterBaseSerializer + + model = Cluster + + elif item.item_type == TicketLinkedItem.Modules.CONFIG_GROUP: + + from config_management.serializers.config_group import ConfigGroups, ConfigGroupBaseSerializer + + base_serializer = ConfigGroupBaseSerializer + + model = ConfigGroups + + elif item.item_type == TicketLinkedItem.Modules.DEVICE: + + from itam.serializers.device import Device, DeviceBaseSerializer + + base_serializer = DeviceBaseSerializer + + model = Device + + elif item.item_type == TicketLinkedItem.Modules.OPERATING_SYSTEM: + + from itam.serializers.operating_system import OperatingSystem, OperatingSystemBaseSerializer + + base_serializer = OperatingSystemBaseSerializer + + model = OperatingSystem + + elif item.item_type == TicketLinkedItem.Modules.SERVICE: + + from itim.serializers.service import Service, ServiceBaseSerializer + + base_serializer = ServiceBaseSerializer + + model = Service + + elif item.item_type == TicketLinkedItem.Modules.SOFTWARE: + + from itam.serializers.software import Software, SoftwareBaseSerializer + + base_serializer = SoftwareBaseSerializer + + model = Software + + + if not base_serializer: + + return { + 'id': int(item.item) + } + + + try: + + model = model.objects.get( + pk = int(item.item) + ) + + except: + + return {} + + + return base_serializer( + model, + context=self._context + ).data + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': item.get_url( request = self._context['view'].request ) + } + + + class Meta: + + model = TicketLinkedItem + + fields = [ + 'id', + 'display_name', + 'item', + 'item_type', + 'ticket', + 'organization', + 'created', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'item', + 'item_type', + 'ticket', + 'organization', + 'created', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid( raise_exception = raise_exception ) + + + if 'view' in self._context: + + ticket = Ticket.objects.get(pk = int(self._context['view'].kwargs['ticket_id']) ) + + self.validated_data['ticket'] = ticket + + self.validated_data['organization_id'] = ticket.organization.id + + + return is_valid + + + +class TicketLinkedItemViewSerializer(TicketLinkedItemModelSerializer): + + + organization = OrganizationBaseSerializer(many=False, read_only=True) + + ticket = TicketBaseSerializer(read_only = True) diff --git a/app/core/viewsets/ticket_linked_item.py b/app/core/viewsets/ticket_linked_item.py new file mode 100644 index 000000000..d7b68f082 --- /dev/null +++ b/app/core/viewsets/ticket_linked_item.py @@ -0,0 +1,106 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from core.serializers.ticket_linked_item import ( + TicketLinkedItem, + TicketLinkedItemModelSerializer, + TicketLinkedItemViewSerializer +) + +from api.views.mixin import OrganizationPermissionAPI + +from api.viewsets.common import ModelViewSet + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a Ticket Linked Item', + description='', + responses = { + 201: OpenApiResponse(description='Created', response=TicketLinkedItemViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a Ticket Linked Item', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all Ticket Linked Items', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketLinkedItemViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single Ticket Linked Item', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketLinkedItemViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a Ticket Linked Item', + description = '', + responses = { + 200: OpenApiResponse(description='', response=TicketLinkedItemViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelViewSet): + + filterset_fields = [ + 'item_type', + 'organization', + ] + + search_fields = [] + + model = TicketLinkedItem + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + + + def get_queryset(self): + + if 'ticket_id' in self.kwargs: + + self.queryset = TicketLinkedItem.objects.filter(ticket=self.kwargs['ticket_id']).order_by('id') + + elif 'item_id' in self.kwargs: + + item_type: int = None + + for choice in list(map(lambda c: c.name, TicketLinkedItem.Modules)): + + if str(getattr(TicketLinkedItem.Modules, 'DEVICE').label).lower() == self.kwargs['item_class']: + + item_type = getattr(TicketLinkedItem.Modules, 'DEVICE').value + + self.queryset = TicketLinkedItem.objects.filter( + item=int(self.kwargs['item_id']), + item_type = item_type + ) + + if 'pk' in self.kwargs: + + self.queryset = self.queryset.filter(pk = self.kwargs['pk']) + + return self.queryset From da5d19cbcb8e142c6cd61ca176d9d5d468a7da14 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 18:15:59 +0930 Subject: [PATCH 358/617] fix: Dont attempt to access request within serializers when no context is present ref: #248 #365 #366 --- .../serializers/config_group.py | 20 +++++++++++++++++-- app/itam/serializers/device.py | 10 +++++++++- app/itam/serializers/operating_system.py | 10 +++++++++- app/itam/serializers/software.py | 10 +++++++++- app/itim/serializers/cluster.py | 10 +++++++++- app/itim/serializers/service.py | 10 +++++++++- 6 files changed, 63 insertions(+), 7 deletions(-) diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 7c43424b1..a48175c0e 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -21,7 +21,15 @@ def get_display_name(self, item): def get_url(self, item): - return item.get_url( request = self._context['view'].request ) + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + + return item.get_url( request = request ) class Meta: @@ -51,8 +59,16 @@ class ConfigGroupModelSerializer(ConfigGroupBaseSerializer): def get_url(self, item): + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + return { - '_self': item.get_url( request = self._context['view'].request ), + '_self': item.get_url( request = request ), 'child_groups': reverse( 'v2:_api_v2_config_group_child-list', request = self.context['view'].request, diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 1bac79073..5d4cdc97f 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -54,8 +54,16 @@ class DeviceModelSerializer(DeviceBaseSerializer): def get_url(self, item): + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + return { - '_self': item.get_url( request = self._context['view'].request ), + '_self': item.get_url( request = request ), 'device_model': reverse("v2:_api_v2_device_model-list", request=self._context['view'].request), 'device_type': reverse("v2:_api_v2_device_type-list", request=self._context['view'].request), 'external_links': reverse("v2:_api_v2_external_link-list", request=self._context['view'].request) + '?devices=true', diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index 15e128a84..2fff96c77 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -50,8 +50,16 @@ class OperatingSystemModelSerializer(OperatingSystemBaseSerializer): def get_url(self, item): + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + return { - '_self': item.get_url( request = self._context['view'].request ), + '_self': item.get_url( request = request ), 'history': reverse( "v2:_api_v2_model_history-list", request=self._context['view'].request, diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 6d43a943d..7026a2b71 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -48,8 +48,16 @@ class SoftwareModelSerializer(SoftwareBaseSerializer): def get_url(self, item): + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + return { - '_self': item.get_url( request = self._context['view'].request ), + '_self': item.get_url( request = request ), 'external_links': reverse("v2:_api_v2_external_link-list", request=self._context['view'].request) + '?software=true', 'history': reverse( "v2:_api_v2_model_history-list", diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index 7e9c2c8cf..73d5342d8 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -47,8 +47,16 @@ class ClusterModelSerializer(ClusterBaseSerializer): def get_url(self, item): + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + return { - '_self': item.get_url( request = self._context['view'].request ), + '_self': item.get_url( request = request ), 'history': reverse( "v2:_api_v2_model_history-list", request=self._context['view'].request, diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index ba35e3bdc..1893bf00d 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -49,8 +49,16 @@ class ServiceModelSerializer(ServiceBaseSerializer): def get_url(self, item): + request = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + return { - '_self': item.get_url( request = self._context['view'].request ), + '_self': item.get_url( request = request ), 'history': reverse( "v2:_api_v2_model_history-list", request=self._context['view'].request, From c36d36be0be2289286a63429f67be7cf25c35b84 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 18:52:55 +0930 Subject: [PATCH 359/617] feat(core): Add Related Ticket API v2 endpoint ref: #248 #365 --- app/api/urls_v2.py | 3 + app/api/viewsets/common.py | 13 +++ app/core/models/ticket/ticket.py | 34 ++++++- app/core/serializers/ticket.py | 1 + app/core/serializers/ticket_related.py | 119 +++++++++++++++++++++++++ app/core/viewsets/related_ticket.py | 78 ++++++++++++++++ 6 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 app/core/serializers/ticket_related.py create mode 100644 app/core/viewsets/related_ticket.py diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 5ab9153ad..3c31b0e03 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -43,6 +43,7 @@ ticket_category, ticket_comment, ticket_linked_item, + related_ticket, ) @@ -119,6 +120,8 @@ router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') router.register('core/ticket/(?P[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item') +router.register('core/ticket/(?P[0-9]+)/related_ticket', related_ticket.ViewSet, basename='_api_v2_ticket_related') + router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home') router.register('itam/device', device_v2.ViewSet, basename='_api_v2_device') diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index 7c278986a..bebe7a030 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -225,6 +225,19 @@ class ModelViewSet( +class ModelListRetrieveDeleteViewSet( + viewsets.mixins.ListModelMixin, + viewsets.mixins.RetrieveModelMixin, + viewsets.mixins.DestroyModelMixin, + viewsets.GenericViewSet, + ModelViewSetBase +): + """ Use for models that you wish to delete and view ONLY!""" + + pass + + + class ModelRetrieveUpdateViewSet( viewsets.mixins.RetrieveModelMixin, viewsets.mixins.UpdateModelMixin, diff --git a/app/core/models/ticket/ticket.py b/app/core/models/ticket/ticket.py index 322f3cc5e..0f97559b7 100644 --- a/app/core/models/ticket/ticket.py +++ b/app/core/models/ticket/ticket.py @@ -3,6 +3,8 @@ from django.db.models import Q, signals, Sum from django.forms import ValidationError +from rest_framework.reverse import reverse + from .ticket_enum_values import TicketValues from access.fields import AutoCreatedField, AutoLastModifiedField @@ -1103,6 +1105,11 @@ class Meta: 'id' ] + verbose_name = 'Related Ticket' + + verbose_name_plural = 'Related Tickets' + + class Related(models.IntegerChoices): RELATED = '1', 'Related' @@ -1159,9 +1166,32 @@ class Related(models.IntegerChoices): ] - # def __str__(self): + def get_url( self, ticket_id, request = None ) -> str: + + if not ticket_id: + + return '' + + if request: + + return reverse( + "v2:_api_v2_ticket_related-detail", + request = request, + kwargs={ + 'ticket_id': ticket_id, + 'pk': self.id + } + ) + + return reverse("v2:_api_v2_ticket_related-detail", kwargs={'pk': self.id}) + + + def __str__(self): + + # return '#' + str( self.id ) + + return '#' - # return '' @property def parent_object(self): diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 884e59da7..adb472c08 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -75,6 +75,7 @@ def get_url(self, item): ), 'comments': reverse('v2:_api_v2_ticket_' + str(item.get_ticket_type_display()).lower() + '_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), + 'related_tickets': reverse("v2:_api_v2_ticket_related-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), } diff --git a/app/core/serializers/ticket_related.py b/app/core/serializers/ticket_related.py new file mode 100644 index 000000000..f7aa436fe --- /dev/null +++ b/app/core/serializers/ticket_related.py @@ -0,0 +1,119 @@ +from rest_framework.fields import empty +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from core.serializers.ticket import TicketBaseSerializer + +from core.models.ticket.ticket import RelatedTickets + + + +class RelatedTicketBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.SerializerMethodField('get_url') + + def get_url(self, item) -> str: + + request = None + + ticket_id: int = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + + if 'ticket_id' in self._kwargs['context']['view'].kwargs: + + ticket_id = int(self._kwargs['context']['view'].kwargs['ticket_id']) + + return item.get_url( ticket_id = ticket_id,request = request ) + + + class Meta: + + model = RelatedTickets + + fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'title', + 'url', + ] + + +class RelatedTicketModelSerializer(RelatedTicketBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + request = None + + ticket_id: int = None + + if 'view' in self._context: + + if hasattr(self._context['view'], 'request'): + + request = self._context['view'].request + + if 'ticket_id' in self._kwargs['context']['view'].kwargs: + + ticket_id = int(self._kwargs['context']['view'].kwargs['ticket_id']) + + return { + '_self': item.get_url( ticket_id = ticket_id, request = request ), + } + + + class Meta: + + model = RelatedTickets + + fields = [ + 'id', + 'display_name', + 'to_ticket_id', + 'from_ticket_id', + 'how_related', + 'organization', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'to_ticket_id', + 'from_ticket_id', + 'how_related', + 'organization', + '_urls', + ] + + + +class RelatedTicketViewSerializer(RelatedTicketModelSerializer): + + from_ticket_id = TicketBaseSerializer() + + organization = OrganizationBaseSerializer(many=False, read_only=True) + + to_ticket_id = TicketBaseSerializer() diff --git a/app/core/viewsets/related_ticket.py b/app/core/viewsets/related_ticket.py new file mode 100644 index 000000000..9e597c118 --- /dev/null +++ b/app/core/viewsets/related_ticket.py @@ -0,0 +1,78 @@ +from django.db.models import Q + +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from access.mixin import OrganizationMixin + +from api.viewsets.common import ModelListRetrieveDeleteViewSet + +from core.serializers.ticket_related import ( + RelatedTickets, + RelatedTicketModelSerializer, + RelatedTicketViewSerializer, +) + + + +@extend_schema_view( + destroy = extend_schema( + summary = 'Delete a related ticket', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all related tickets', + description='', + responses = { + 200: OpenApiResponse(description='', response=RelatedTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a related ticket', + description='', + responses = { + 200: OpenApiResponse(description='', response=RelatedTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), +) +class ViewSet(ModelListRetrieveDeleteViewSet): + + + filterset_fields = [ + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = RelatedTickets + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] + + + def get_queryset(self): + + self.queryset = RelatedTickets.objects.filter( + Q(from_ticket_id_id=self.kwargs['ticket_id']) + | + Q(to_ticket_id_id=self.kwargs['ticket_id']) + ) + + return self.queryset.filter().order_by('id') From 564bae99b1b2b29133976bc098b434023d4e9c57 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 19:22:20 +0930 Subject: [PATCH 360/617] feat(core): Add Item Ticket API v2 endpoint added for cluster, config group, device, service, software and operating system. ref: #248 #365 --- app/api/urls_v2.py | 1 + app/config_management/models/groups.py | 2 +- .../serializers/config_group.py | 8 +++++++ app/core/models/ticket/ticket_linked_items.py | 2 +- app/core/viewsets/ticket_linked_item.py | 22 ++++++++++++++++++- app/itam/serializers/device.py | 8 +++++++ app/itam/serializers/operating_system.py | 9 +++++++- app/itam/serializers/software.py | 9 +++++++- app/itim/serializers/cluster.py | 9 +++++++- app/itim/serializers/service.py | 9 +++++++- 10 files changed, 72 insertions(+), 7 deletions(-) diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 3c31b0e03..68256a15c 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -121,6 +121,7 @@ router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') router.register('core/ticket/(?P[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item') router.register('core/ticket/(?P[0-9]+)/related_ticket', related_ticket.ViewSet, basename='_api_v2_ticket_related') +router.register('core/(?P[a-z]+)/(?P[0-9]+)/item_ticket', ticket_linked_item.ViewSet, basename='_api_v2_item_tickets') router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home') diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 21b48a127..8b9ae72c5 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -179,7 +179,7 @@ def validate_config_keys_not_reserved(self): "sections": [ { "layout": "table", - "field": "ticket", + "field": "tickets", } ] }, diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index a48175c0e..953173fdd 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -100,6 +100,14 @@ def get_url(self, item): 'v2:_api_v2_config_group-list', request=self.context['view'].request, ), + 'tickets': reverse( + "v2:_api_v2_item_tickets-list", + request=self._context['view'].request, + kwargs={ + 'item_class': 'cluster', + 'item_id': item.pk + } + ), } rendered_config = serializers.JSONField( source = 'render_config', read_only=True ) diff --git a/app/core/models/ticket/ticket_linked_items.py b/app/core/models/ticket/ticket_linked_items.py index fa1132b23..b04eb6db3 100644 --- a/app/core/models/ticket/ticket_linked_items.py +++ b/app/core/models/ticket/ticket_linked_items.py @@ -69,7 +69,7 @@ class Modules(models.IntegerChoices): ) table_fields: list = [ - 'display_name', + 'ticket', 'status_badge', 'created' ] diff --git a/app/core/viewsets/ticket_linked_item.py b/app/core/viewsets/ticket_linked_item.py index d7b68f082..e6b69615a 100644 --- a/app/core/viewsets/ticket_linked_item.py +++ b/app/core/viewsets/ticket_linked_item.py @@ -90,10 +90,30 @@ def get_queryset(self): for choice in list(map(lambda c: c.name, TicketLinkedItem.Modules)): - if str(getattr(TicketLinkedItem.Modules, 'DEVICE').label).lower() == self.kwargs['item_class']: + if str(getattr(TicketLinkedItem.Modules, 'CLUSTER').label).lower() == self.kwargs['item_class']: + + item_type = getattr(TicketLinkedItem.Modules, 'CLUSTER').value + + elif str(getattr(TicketLinkedItem.Modules, 'CONFIG_GROUP').label).lower() == self.kwargs['item_class']: + + item_type = getattr(TicketLinkedItem.Modules, 'CONFIG_GROUP').value + + elif str(getattr(TicketLinkedItem.Modules, 'DEVICE').label).lower() == self.kwargs['item_class']: item_type = getattr(TicketLinkedItem.Modules, 'DEVICE').value + elif str(getattr(TicketLinkedItem.Modules, 'OPERATING_SYSTEM').label).lower() == self.kwargs['item_class']: + + item_type = getattr(TicketLinkedItem.Modules, 'OPERATING_SYSTEM').value + + elif str(getattr(TicketLinkedItem.Modules, 'SERVICE').label).lower() == self.kwargs['item_class']: + + item_type = getattr(TicketLinkedItem.Modules, 'SERVICE').value + + elif str(getattr(TicketLinkedItem.Modules, 'SOFTWARE').label).lower() == self.kwargs['item_class']: + + item_type = getattr(TicketLinkedItem.Modules, 'SOFTWARE').value + self.queryset = TicketLinkedItem.objects.filter( item=int(self.kwargs['item_id']), item_type = item_type diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 5d4cdc97f..445e9e685 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -78,6 +78,14 @@ def get_url(self, item): 'notes': reverse("v2:_api_v2_device_notes-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), 'service': reverse("v2:_api_v2_service_device-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), 'software': reverse("v2:_api_v2_device_software-list", request=self._context['view'].request, kwargs={'device_id': item.pk}), + 'tickets': reverse( + "v2:_api_v2_item_tickets-list", + request=self._context['view'].request, + kwargs={ + 'item_class': 'device', + 'item_id': item.pk + } + ) } diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index 2fff96c77..3f540ccd5 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -69,7 +69,14 @@ def get_url(self, item): } ), 'notes': reverse("v2:_api_v2_operating_system_notes-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), - 'tickets': 'ToDo', + 'tickets': reverse( + "v2:_api_v2_item_tickets-list", + request=self._context['view'].request, + kwargs={ + 'item_class': 'operating_system', + 'item_id': item.pk + } + ), 'version': reverse("v2:_api_v2_operating_system_version-list", request=self._context['view'].request, kwargs={'operating_system_id': item.pk}), } diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 7026a2b71..9caf0c94b 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -77,7 +77,14 @@ def get_url(self, item): 'software_id': item.pk } ), - 'tickets': 'ToDo' + 'tickets': reverse( + "v2:_api_v2_item_tickets-list", + request=self._context['view'].request, + kwargs={ + 'item_class': 'software', + 'item_id': item.pk + } + ) } diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index 73d5342d8..7ff2f8a35 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -66,7 +66,14 @@ def get_url(self, item): } ), 'notes': reverse("v2:_api_v2_cluster_notes-list", request=self._context['view'].request, kwargs={'cluster_id': item.pk}), - 'tickets': 'ToDo' + 'tickets': reverse( + "v2:_api_v2_item_tickets-list", + request=self._context['view'].request, + kwargs={ + 'item_class': 'cluster', + 'item_id': item.pk + } + ) } diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index 1893bf00d..ded8e8f4c 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -68,7 +68,14 @@ def get_url(self, item): } ), 'notes': reverse("v2:_api_v2_service_notes-list", request=self._context['view'].request, kwargs={'service_id': item.pk}), - 'tickets': 'ToDo' + 'tickets': reverse( + "v2:_api_v2_item_tickets-list", + request=self._context['view'].request, + kwargs={ + 'item_class': 'service', + 'item_id': item.pk + } + ) } From a4e62b37187ccf6b5b476e8cb98bc64631a001a1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 20:06:57 +0930 Subject: [PATCH 361/617] fix(core): Ensure item tickets class can have underscore in name ref: #248 #365 --- app/api/urls_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 68256a15c..e25b6aaaa 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -121,7 +121,7 @@ router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') router.register('core/ticket/(?P[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item') router.register('core/ticket/(?P[0-9]+)/related_ticket', related_ticket.ViewSet, basename='_api_v2_ticket_related') -router.register('core/(?P[a-z]+)/(?P[0-9]+)/item_ticket', ticket_linked_item.ViewSet, basename='_api_v2_item_tickets') +router.register('core/(?P[a-z_]+)/(?P[0-9]+)/item_ticket', ticket_linked_item.ViewSet, basename='_api_v2_item_tickets') router.register('itam', itam_index_v2.Index, basename='_api_v2_itam_home') From b5bc45fa2b496dbcff6772db587b8e32c7021ce0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 20:08:09 +0930 Subject: [PATCH 362/617] fix(core): Ensure that when checking linked ticket class name, spaces are replaced ref: #248 #365 --- app/core/viewsets/ticket_linked_item.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/viewsets/ticket_linked_item.py b/app/core/viewsets/ticket_linked_item.py index e6b69615a..8d6dc1077 100644 --- a/app/core/viewsets/ticket_linked_item.py +++ b/app/core/viewsets/ticket_linked_item.py @@ -94,7 +94,7 @@ def get_queryset(self): item_type = getattr(TicketLinkedItem.Modules, 'CLUSTER').value - elif str(getattr(TicketLinkedItem.Modules, 'CONFIG_GROUP').label).lower() == self.kwargs['item_class']: + elif str(getattr(TicketLinkedItem.Modules, 'CONFIG_GROUP').label).lower().replace(' ', '_') == self.kwargs['item_class']: item_type = getattr(TicketLinkedItem.Modules, 'CONFIG_GROUP').value @@ -102,7 +102,7 @@ def get_queryset(self): item_type = getattr(TicketLinkedItem.Modules, 'DEVICE').value - elif str(getattr(TicketLinkedItem.Modules, 'OPERATING_SYSTEM').label).lower() == self.kwargs['item_class']: + elif str(getattr(TicketLinkedItem.Modules, 'OPERATING_SYSTEM').label).lower().replace(' ', '_') == self.kwargs['item_class']: item_type = getattr(TicketLinkedItem.Modules, 'OPERATING_SYSTEM').value From 487cbd8e542dedc1d060c667a81bcd70c0d5fc4e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 22:14:43 +0930 Subject: [PATCH 363/617] fix(core): Add missing permissions function to ticket viewset ref: #248 #365 --- app/core/viewsets/ticket.py | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 063d06672..0649c605c 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -47,6 +47,47 @@ class TicketViewSet(ModelViewSet): """ + def get_dynamic_permissions(self): + + if self.action == 'create': + + action_keyword = 'add' + + elif self.action == 'destroy': + + action_keyword = 'delete' + + elif self.action == 'list': + + action_keyword = 'view' + + elif self.action == 'partial_update': + + action_keyword = 'change' + + elif self.action == 'retrieve': + + action_keyword = 'view' + + elif self.action == 'update': + + action_keyword = 'change' + + elif self.action is None: + + action_keyword = 'view' + + else: + + raise ValueError('unable to determin the action_keyword') + + self.permission_required = [ + str('core.' + action_keyword + '_ticket_' + self._ticket_type).lower(), + ] + + return super().get_permission_required() + + def get_queryset(self): self.get_ticket_type() From 38e1742f1581cd77bdfcdeb475eab51865df8c7e Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 22:17:29 +0930 Subject: [PATCH 364/617] test(core): Ticket Common API field checks test cases common to ALL ticket types ref: #15 #248 #360 --- app/core/tests/abstract/test_ticket_api_v2.py | 1148 +++++++++++++++++ 1 file changed, 1148 insertions(+) create mode 100644 app/core/tests/abstract/test_ticket_api_v2.py diff --git a/app/core/tests/abstract/test_ticket_api_v2.py b/app/core/tests/abstract/test_ticket_api_v2.py new file mode 100644 index 000000000..80ede1df3 --- /dev/null +++ b/app/core/tests/abstract/test_ticket_api_v2.py @@ -0,0 +1,1148 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket_category import TicketCategory + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone + + +class TicketAPI( + APITenancyObject +): + """ Common Ticket API Field test cases + + Include these test cases in ALL ticket type field tests + + Args: + APITenancyObject (class): Test Cases common to ALL API requests + """ + + model = None + + ticket_type: str = None + """name of ticket in lowercase""" + + + @classmethod + def setUpTestData(self): + """Setup Test + + This method should be `super` called from the inherited class + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.project = Project.objects.create( + organization = self.organization, + name = 'a project', + ) + + self.project_milestone = ProjectMilestone.objects.create( + organization = self.organization, + name = 'a midlestone', + project = self.project, + ) + + self.ticket_category = TicketCategory.objects.create( + organization = self.organization, + name = 'a ticket category', + ) + + + view_permissions = Permission.objects.get( + codename = 'view_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + self.view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + self.view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = self.view_team, + user = self.view_user + ) + + + def test_api_field_exists_display_name(self): + """ Test for existance of API Field + + This is a custom test case of a test with the same name. + This test is required as this field does not exist + + display_name field must not exist + """ + + assert 'display_name' not in self.api_data + + + def test_api_field_type_display_name(self): + """ Test for type for API Field + + This is a custom test case of a test with the same name. + This test is required as this field does not exist + """ + + assert True + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This is a custom test case of a test with the same name. + This test is required as this field does not exist + + model_notes field must not exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This is a custom test case of a test with the same name. + This test is required as this field does not exist + """ + + assert True + + + + def test_api_field_exists_status_badge(self): + """ Test for existance of API Field + + status_badge field must exist + """ + + assert 'status_badge' in self.api_data + + + def test_api_field_type_status_badge(self): + """ Test for type for API Field + + status_badge field must be int + """ + + assert type(self.api_data['status_badge']) is dict + + + def test_api_field_exists_status_badge_icon(self): + """ Test for existance of API Field + + status_badge.icon field must exist + """ + + assert 'icon' in self.api_data['status_badge'] + + + def test_api_field_type_status_badge_icon(self): + """ Test for type for API Field + + status_badge.icon field must be dict + """ + + assert type(self.api_data['status_badge']['icon']) is dict + + + def test_api_field_exists_status_badge_icon_name(self): + """ Test for existance of API Field + + status_badge.icon.name field must exist + """ + + assert 'name' in self.api_data['status_badge']['icon'] + + + def test_api_field_type_status_badge_icon_name(self): + """ Test for type for API Field + + status_badge.icon.name field must be str + """ + + assert type(self.api_data['status_badge']['icon']['name']) is str + + + def test_api_field_exists_status_badge_icon_style(self): + """ Test for existance of API Field + + status_badge.icon.style field must exist + """ + + assert 'style' in self.api_data['status_badge']['icon'] + + + def test_api_field_type_status_badge_icon_style(self): + """ Test for type for API Field + + status_badge.icon.style field must be str + """ + + assert type(self.api_data['status_badge']['icon']['style']) is str + + + + def test_api_field_exists_status_badge_text(self): + """ Test for existance of API Field + + status_badge.text field must exist + """ + + assert 'text' in self.api_data['status_badge'] + + + def test_api_field_type_status_badge_text(self): + """ Test for type for API Field + + status_badge.text field must be str + """ + + assert type(self.api_data['status_badge']['text']) is str + + + + def test_api_field_exists_status_badge_text_style(self): + """ Test for existance of API Field + + status_badge.text_style field must exist + """ + + assert 'text_style' in self.api_data['status_badge'] + + + def test_api_field_type_status_badge_text_style(self): + """ Test for type for API Field + + status_badge.text_style field must be str + """ + + assert type(self.api_data['status_badge']['text_style']) is str + + + + def test_api_field_exists_title(self): + """ Test for existance of API Field + + title field must exist + """ + + assert 'title' in self.api_data + + + def test_api_field_type_title(self): + """ Test for type for API Field + + title field must be str + """ + + assert type(self.api_data['title']) is str + + + + def test_api_field_exists_description(self): + """ Test for existance of API Field + + description field must exist + """ + + assert 'description' in self.api_data + + + def test_api_field_type_description(self): + """ Test for type for API Field + + description field must be str + """ + + assert type(self.api_data['description']) is str + + + + def test_api_field_exists_ticket_type(self): + """ Test for existance of API Field + + ticket_type field must exist + """ + + assert 'ticket_type' in self.api_data + + + def test_api_field_type_description(self): + """ Test for type for API Field + + ticket_type field must be int + """ + + assert type(self.api_data['ticket_type']) is int + + + + def test_api_field_exists_status(self): + """ Test for existance of API Field + + status field must exist + """ + + assert 'status' in self.api_data + + + def test_api_field_type_status(self): + """ Test for type for API Field + + status field must be int + """ + + assert type(self.api_data['status']) is int + + + + def test_api_field_exists_estimate(self): + """ Test for existance of API Field + + estimate field must exist + """ + + assert 'estimate' in self.api_data + + + def test_api_field_type_estimate(self): + """ Test for type for API Field + + estimate field must be int + """ + + assert type(self.api_data['estimate']) is int + + + + def test_api_field_exists_duration(self): + """ Test for existance of API Field + + duration field must exist + """ + + assert 'duration' in self.api_data + + + def test_api_field_type_duration(self): + """ Test for type for API Field + + duration field must be int + """ + + assert type(self.api_data['duration']) is int + + + + def test_api_field_exists_urgency(self): + """ Test for existance of API Field + + urgency field must exist + """ + + assert 'urgency' in self.api_data + + + def test_api_field_type_urgency(self): + """ Test for type for API Field + + urgency field must be int + """ + + assert type(self.api_data['urgency']) is int + + + + def test_api_field_exists_priority(self): + """ Test for existance of API Field + + priority field must exist + """ + + assert 'priority' in self.api_data + + + def test_api_field_type_urgency(self): + """ Test for type for API Field + + priority field must be int + """ + + assert type(self.api_data['priority']) is int + + + + def test_api_field_exists_external_ref(self): + """ Test for existance of API Field + + external_ref field must exist + """ + + assert 'external_ref' in self.api_data + + + def test_api_field_type_external_ref(self): + """ Test for type for API Field + + external_ref field must be int + """ + + assert type(self.api_data['external_ref']) is int + + + + def test_api_field_exists_external_system(self): + """ Test for existance of API Field + + external_system field must exist + """ + + assert 'external_system' in self.api_data + + + def test_api_field_type_external_system(self): + """ Test for type for API Field + + external_system field must be int + """ + + assert type(self.api_data['external_system']) is int + + + + def test_api_field_exists_is_deleted(self): + """ Test for existance of API Field + + is_deleted field must exist + """ + + assert 'is_deleted' in self.api_data + + + def test_api_field_type_is_deleted(self): + """ Test for type for API Field + + is_deleted field must be int + """ + + assert type(self.api_data['is_deleted']) is bool + + + + def test_api_field_exists_date_closed(self): + """ Test for existance of API Field + + date_closed field must exist + """ + + assert 'date_closed' in self.api_data + + + def test_api_field_type_date_closed(self): + """ Test for type for API Field + + date_closed field must be int + """ + + assert type(self.api_data['date_closed']) is str + + + + def test_api_field_exists_project(self): + """ Test for existance of API Field + + project field must exist + """ + + assert 'project' in self.api_data + + + def test_api_field_type_project(self): + """ Test for type for API Field + + project field must be int + """ + + assert type(self.api_data['project']) is int + + + + def test_api_field_exists_milestone(self): + """ Test for existance of API Field + + milestone field must exist + """ + + assert 'milestone' in self.api_data + + + def test_api_field_type_milestone(self): + """ Test for type for API Field + + milestone field must be int + """ + + assert type(self.api_data['milestone']) is int + + + + def test_api_field_exists_assigned_teams(self): + """ Test for existance of API Field + + assigned_teams field must exist + """ + + assert 'assigned_teams' in self.api_data + + + def test_api_field_type_assigned_teams(self): + """ Test for type for API Field + + assigned_teams field must be int + """ + + assert type(self.api_data['assigned_teams']) is list + + + + def test_api_field_exists_assigned_teams_id(self): + """ Test for existance of API Field + + assigned_teams.id field must exist + """ + + assert 'id' in self.api_data['assigned_teams'][0] + + + def test_api_field_type_assigned_teams_id(self): + """ Test for type for API Field + + assigned_teams.id field must be int + """ + + assert type(self.api_data['assigned_teams'][0]['id']) is int + + + def test_api_field_exists_assigned_teams_display_name(self): + """ Test for existance of API Field + + assigned_teams.display_name field must exist + """ + + assert 'display_name' in self.api_data['assigned_teams'][0] + + + def test_api_field_type_assigned_teams_display_name(self): + """ Test for type for API Field + + assigned_teams.display_name field must be str + """ + + assert type(self.api_data['assigned_teams'][0]['display_name']) is str + + + def test_api_field_exists_assigned_teams_url(self): + """ Test for existance of API Field + + assigned_teams.url field must exist + """ + + assert 'url' in self.api_data['assigned_teams'][0] + + + def test_api_field_type_assigned_teams_url(self): + """ Test for type for API Field + + assigned_teams.url field must be str + """ + + assert type(self.api_data['assigned_teams'][0]['url']) is str + + + + def test_api_field_exists_assigned_users(self): + """ Test for existance of API Field + + assigned_users field must exist + """ + + assert 'assigned_users' in self.api_data + + + def test_api_field_type_assigned_users(self): + """ Test for type for API Field + + assigned_users field must be int + """ + + assert type(self.api_data['assigned_users']) is list + + + + def test_api_field_exists_assigned_users_id(self): + """ Test for existance of API Field + + assigned_users.id field must exist + """ + + assert 'id' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_id(self): + """ Test for type for API Field + + assigned_users.id field must be int + """ + + assert type(self.api_data['assigned_users'][0]['id']) is int + + + def test_api_field_exists_assigned_users_display_name(self): + """ Test for existance of API Field + + assigned_users.display_name field must exist + """ + + assert 'display_name' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_display_name(self): + """ Test for type for API Field + + assigned_users.display_name field must be str + """ + + assert type(self.api_data['assigned_users'][0]['display_name']) is str + + + def test_api_field_exists_assigned_users_first_name(self): + """ Test for existance of API Field + + assigned_users.first_name field must exist + """ + + assert 'first_name' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_first_name(self): + """ Test for type for API Field + + assigned_users.first_name field must be str + """ + + assert type(self.api_data['assigned_users'][0]['first_name']) is str + + + def test_api_field_exists_assigned_users_last_name(self): + """ Test for existance of API Field + + assigned_users.last_name field must exist + """ + + assert 'last_name' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_last_name(self): + """ Test for type for API Field + + assigned_users.last_name field must be str + """ + + assert type(self.api_data['assigned_users'][0]['last_name']) is str + + + def test_api_field_exists_assigned_users_username(self): + """ Test for existance of API Field + + assigned_users.username field must exist + """ + + assert 'username' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_username(self): + """ Test for type for API Field + + assigned_users.username field must be str + """ + + assert type(self.api_data['assigned_users'][0]['username']) is str + + + def test_api_field_exists_assigned_users_is_active(self): + """ Test for existance of API Field + + assigned_users.is_active field must exist + """ + + assert 'is_active' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_is_active(self): + """ Test for type for API Field + + assigned_users.is_active field must be bool + """ + + assert type(self.api_data['assigned_users'][0]['is_active']) is bool + + + def test_api_field_exists_assigned_users_url(self): + """ Test for existance of API Field + + assigned_users.url field must exist + """ + + assert 'url' in self.api_data['assigned_users'][0] + + + def test_api_field_type_assigned_users_url(self): + """ Test for type for API Field + + assigned_users.url field must be Hyperlink + """ + + assert type(self.api_data['assigned_users'][0]['url']) is Hyperlink + + + + def test_api_field_exists_subscribed_teams(self): + """ Test for existance of API Field + + subscribed_teams field must exist + """ + + assert 'subscribed_teams' in self.api_data + + + def test_api_field_type_subscribed_teams(self): + """ Test for type for API Field + + subscribed_teams field must be int + """ + + assert type(self.api_data['subscribed_teams']) is list + + + def test_api_field_exists_subscribed_teams_id(self): + """ Test for existance of API Field + + subscribed_teams.id field must exist + """ + + assert 'id' in self.api_data['subscribed_teams'][0] + + + def test_api_field_type_subscribed_teams_id(self): + """ Test for type for API Field + + subscribed_teams.id field must be int + """ + + assert type(self.api_data['subscribed_teams'][0]['id']) is int + + + def test_api_field_exists_subscribed_teams_display_name(self): + """ Test for existance of API Field + + subscribed_teams.display_name field must exist + """ + + assert 'display_name' in self.api_data['subscribed_teams'][0] + + + def test_api_field_type_subscribed_teams_display_name(self): + """ Test for type for API Field + + subscribed_teams.display_name field must be str + """ + + assert type(self.api_data['subscribed_teams'][0]['display_name']) is str + + + def test_api_field_exists_subscribed_teams_url(self): + """ Test for existance of API Field + + subscribed_teams.url field must exist + """ + + assert 'url' in self.api_data['subscribed_teams'][0] + + + def test_api_field_type_subscribed_teams_url(self): + """ Test for type for API Field + + subscribed_teams.url field must be str + """ + + assert type(self.api_data['subscribed_teams'][0]['url']) is str + + + + def test_api_field_exists_subscribed_users(self): + """ Test for existance of API Field + + subscribed_users field must exist + """ + + assert 'subscribed_users' in self.api_data + + + def test_api_field_type_subscribed_users(self): + """ Test for type for API Field + + subscribed_users field must be int + """ + + assert type(self.api_data['subscribed_users']) is list + + + + def test_api_field_exists_subscribed_users_id(self): + """ Test for existance of API Field + + subscribed_users.id field must exist + """ + + assert 'id' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_id(self): + """ Test for type for API Field + + subscribed_users.id field must be int + """ + + assert type(self.api_data['subscribed_users'][0]['id']) is int + + + def test_api_field_exists_subscribed_users_display_name(self): + """ Test for existance of API Field + + subscribed_users.display_name field must exist + """ + + assert 'display_name' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_display_name(self): + """ Test for type for API Field + + subscribed_users.display_name field must be str + """ + + assert type(self.api_data['subscribed_users'][0]['display_name']) is str + + + def test_api_field_exists_subscribed_users_first_name(self): + """ Test for existance of API Field + + subscribed_users.first_name field must exist + """ + + assert 'first_name' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_first_name(self): + """ Test for type for API Field + + subscribed_users.first_name field must be str + """ + + assert type(self.api_data['subscribed_users'][0]['first_name']) is str + + + def test_api_field_exists_subscribed_users_last_name(self): + """ Test for existance of API Field + + subscribed_users.last_name field must exist + """ + + assert 'last_name' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_last_name(self): + """ Test for type for API Field + + subscribed_users.last_name field must be str + """ + + assert type(self.api_data['subscribed_users'][0]['last_name']) is str + + + def test_api_field_exists_subscribed_users_username(self): + """ Test for existance of API Field + + subscribed_users.username field must exist + """ + + assert 'username' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_username(self): + """ Test for type for API Field + + subscribed_users.username field must be str + """ + + assert type(self.api_data['subscribed_users'][0]['username']) is str + + + def test_api_field_exists_subscribed_users_is_active(self): + """ Test for existance of API Field + + subscribed_users.is_active field must exist + """ + + assert 'is_active' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_is_active(self): + """ Test for type for API Field + + subscribed_users.is_active field must be bool + """ + + assert type(self.api_data['subscribed_users'][0]['is_active']) is bool + + + def test_api_field_exists_subscribed_users_url(self): + """ Test for existance of API Field + + subscribed_users.url field must exist + """ + + assert 'url' in self.api_data['subscribed_users'][0] + + + def test_api_field_type_subscribed_users_url(self): + """ Test for type for API Field + + subscribed_users.url field must be Hyperlink + """ + + assert type(self.api_data['subscribed_users'][0]['url']) is Hyperlink + + + + def test_api_field_exists_opened_by(self): + """ Test for existance of API Field + + opened_by field must exist + """ + + assert 'opened_by' in self.api_data + + + def test_api_field_type_opened_by(self): + """ Test for type for API Field + + opened_by field must be int + """ + + assert type(self.api_data['opened_by']) is dict + + + def test_api_field_exists_opened_by_id(self): + """ Test for existance of API Field + + opened_by.id field must exist + """ + + assert 'id' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_id(self): + """ Test for type for API Field + + opened_by.id field must be int + """ + + assert type(self.api_data['opened_by']['id']) is int + + + def test_api_field_exists_opened_by_display_name(self): + """ Test for existance of API Field + + opened_by.display_name field must exist + """ + + assert 'display_name' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_display_name(self): + """ Test for type for API Field + + opened_by.display_name field must be str + """ + + assert type(self.api_data['opened_by']['display_name']) is str + + + def test_api_field_exists_opened_by_first_name(self): + """ Test for existance of API Field + + opened_by.first_name field must exist + """ + + assert 'first_name' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_first_name(self): + """ Test for type for API Field + + opened_by.first_name field must be str + """ + + assert type(self.api_data['opened_by']['first_name']) is str + + + def test_api_field_exists_opened_by_last_name(self): + """ Test for existance of API Field + + opened_by.last_name field must exist + """ + + assert 'last_name' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_last_name(self): + """ Test for type for API Field + + opened_by.last_name field must be str + """ + + assert type(self.api_data['opened_by']['last_name']) is str + + + def test_api_field_exists_opened_by_username(self): + """ Test for existance of API Field + + opened_by.username field must exist + """ + + assert 'username' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_username(self): + """ Test for type for API Field + + opened_by.username field must be str + """ + + assert type(self.api_data['opened_by']['username']) is str + + + def test_api_field_exists_opened_by_is_active(self): + """ Test for existance of API Field + + opened_by.is_active field must exist + """ + + assert 'is_active' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_is_active(self): + """ Test for type for API Field + + opened_by.is_active field must be bool + """ + + assert type(self.api_data['opened_by']['is_active']) is bool + + + def test_api_field_exists_opened_by_url(self): + """ Test for existance of API Field + + opened_by.url field must exist + """ + + assert 'url' in self.api_data['opened_by'] + + + def test_api_field_type_opened_by_url(self): + """ Test for type for API Field + + opened_by.url field must be Hyperlink + """ + + assert type(self.api_data['opened_by']['url']) is Hyperlink + + + + def test_api_field_exists__urls_comments(self): + """ Test for existance of API Field + + _urls.comments field must exist + """ + + assert 'comments' in self.api_data['_urls'] + + + def test_api_field_type__urls_comments(self): + """ Test for type for API Field + + _urls.comments field must be int + """ + + assert type(self.api_data['_urls']['comments']) is str + + + + def test_api_field_exists__urls_linked_items(self): + """ Test for existance of API Field + + _urls.linked_items field must exist + """ + + assert 'linked_items' in self.api_data['_urls'] + + + def test_api_field_type__urls_linked_items(self): + """ Test for type for API Field + + _urls.linked_items field must be int + """ + + assert type(self.api_data['_urls']['linked_items']) is str + + + + def test_api_field_exists__urls_related_tickets(self): + """ Test for existance of API Field + + _urls.related_tickets field must exist + """ + + assert 'related_tickets' in self.api_data['_urls'] + + + def test_api_field_type__urls_related_tickets(self): + """ Test for type for API Field + + _urls.related_tickets field must be int + """ + + assert type(self.api_data['_urls']['related_tickets']) is str From 10703067fa2fce2eb569888752c6d88856d98169 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 27 Oct 2024 22:18:17 +0930 Subject: [PATCH 365/617] test(assistance): Request Ticket API field checks ref: #15 #248 #360 --- .../test_ticket_request_api_v2.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 app/assistance/tests/unit/ticket_request/test_ticket_request_api_v2.py diff --git a/app/assistance/tests/unit/ticket_request/test_ticket_request_api_v2.py b/app/assistance/tests/unit/ticket_request/test_ticket_request_api_v2.py new file mode 100644 index 000000000..f694742c5 --- /dev/null +++ b/app/assistance/tests/unit/ticket_request/test_ticket_request_api_v2.py @@ -0,0 +1,146 @@ +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from core.tests.abstract.test_ticket_api_v2 import TicketAPI + +from core.models.ticket.ticket import Ticket + + + +class RequestTicketAPI( + TicketAPI, + TestCase +): + + model = Ticket + + ticket_type = 'request' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.item = self.model.objects.create( + + # All Tickets + organization=self.organization, + title = 'A ' + self.ticket_type + ' ticket', + description = 'the ticket body', + opened_by = self.view_user, + status = int(Ticket.TicketStatus.All.CLOSED.value), + project = self.project, + milestone = self.project_milestone, + external_ref = 1, + external_system = Ticket.Ticket_ExternalSystem.CUSTOM_1, + date_closed = '2024-01-01T01:02:03Z', + + # ITIL Tickets + category = self.ticket_category, + + # Specific to ticket + ticket_type = int(Ticket.TicketType.REQUEST.value), + ) + + self.item.assigned_teams.set([ self.view_team ]) + + self.item.assigned_users.set([ self.view_user ]) + + self.item.subscribed_teams.set([ self.view_team ]) + + self.item.subscribed_users.set([ self.view_user ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('v2:_api_v2_ticket_' + self.ticket_type + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_impact(self): + """ Test for existance of API Field + + impact field must exist + """ + + assert 'impact' in self.api_data + + + def test_api_field_type_impact(self): + """ Test for type for API Field + + impact field must be int + """ + + assert type(self.api_data['impact']) is int + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink From 811e7230068e7f5327515c031d07980414522a37 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 12:29:41 +0930 Subject: [PATCH 366/617] test(core): Ticket Category API field checks ref: #15 #248 #365 --- .../test_ticket_category_api_v2.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 app/core/tests/unit/ticket_category/test_ticket_category_api_v2.py diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_api_v2.py b/app/core/tests/unit/ticket_category/test_ticket_category_api_v2.py new file mode 100644 index 000000000..5ea2850b8 --- /dev/null +++ b/app/core/tests/unit/ticket_category/test_ticket_category_api_v2.py @@ -0,0 +1,110 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket_category import TicketCategory + + + +class TicketCategoryAPI( + TestCase, + APITenancyObject +): + + model = TicketCategory + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + model_notes = 'text' + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('v2:_api_v2_ticket_category-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + # def test_api_field_exists_name(self): + # """ Test for existance of API Field + + # name field must exist + # """ + + # assert 'name' in self.api_data + + + # def test_api_field_type_name(self): + # """ Test for type for API Field + + # name field must be str + # """ + + # assert type(self.api_data['name']) is str + + + + # def test_api_field_exists_url_history(self): + # """ Test for existance of API Field + + # _urls.history field must exist + # """ + + # assert 'history' in self.api_data['_urls'] + + + # def test_api_field_type_url_history(self): + # """ Test for type for API Field + + # _urls.history field must be str + # """ + + # assert type(self.api_data['_urls']['history']) is str From dd8cbc9da4224fb8376346d8e722e265334e7f0a Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 13:07:44 +0930 Subject: [PATCH 367/617] refactor(core): Ticket Comments to use a single API Endpoint ref: #248 #365 --- app/api/urls_v2.py | 4 ++-- app/core/serializers/ticket.py | 2 +- app/core/serializers/ticket_comment.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index e25b6aaaa..b373132ff 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -101,8 +101,6 @@ router.register('assistance', assistance_index_v2.Index, basename='_api_v2_assistance_home') router.register('assistance/knowledge_base', knowledge_base_v2.ViewSet, basename='_api_v2_knowledge_base') router.register('assistance/ticket/request', request_ticket_v2.ViewSet, basename='_api_v2_ticket_request') -router.register('assistance/ticket/request/(?P[0-9]+)/comments', ticket_comment.ViewSet, basename='_api_v2_ticket_request_comments') -router.register('assistance/ticket/request/(?P[0-9]+)/comments/(?P[0-9]+)/threads', ticket_comment.ViewSet, basename='_api_v2_ticket_request_comment_threads') router.register('base', base_index_v2.Index, basename='_api_v2_base_home') @@ -119,6 +117,8 @@ router.register('core/(?P.+)/(?P[0-9]+)/history', history_v2.ViewSet, basename='_api_v2_model_history') +router.register('core/ticket/(?P[0-9]+)/comments', ticket_comment.ViewSet, basename='_api_v2_ticket_comments') +router.register('core/ticket/(?P[0-9]+)/comments/(?P[0-9]+)/threads', ticket_comment.ViewSet, basename='_api_v2_ticket_comment_threads') router.register('core/ticket/(?P[0-9]+)/linked_item', ticket_linked_item.ViewSet, basename='_api_v2_ticket_linked_item') router.register('core/ticket/(?P[0-9]+)/related_ticket', related_ticket.ViewSet, basename='_api_v2_ticket_related') router.register('core/(?P[a-z_]+)/(?P[0-9]+)/item_ticket', ticket_linked_item.ViewSet, basename='_api_v2_item_tickets') diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index adb472c08..6ae308bcb 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -73,7 +73,7 @@ def get_url(self, item): 'pk': item.pk } ), - 'comments': reverse('v2:_api_v2_ticket_' + str(item.get_ticket_type_display()).lower() + '_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), + 'comments': reverse('v2:_api_v2_ticket_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), 'related_tickets': reverse("v2:_api_v2_ticket_related-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), } diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py index 1e3dd58e9..c4e3d0d7f 100644 --- a/app/core/serializers/ticket_comment.py +++ b/app/core/serializers/ticket_comment.py @@ -23,7 +23,7 @@ def get_display_name(self, item): return str( item ) url = serializers.HyperlinkedIdentityField( - view_name="API:_api_v2_device-detail", format="html" + view_name="API:_api_v2_ticket_comments-detail", format="html" ) class Meta: @@ -75,7 +75,7 @@ def get_url(self, item): urls: dict = { '_self': reverse( - 'API:_api_v2_ticket_' + str(ticket_type_name).lower().replace(' ', '_') + '_comments-detail', + 'API:_api_v2_ticket_comments-detail', request = self._context['view'].request, kwargs={ 'ticket_id': ticket_id, @@ -90,7 +90,7 @@ def get_url(self, item): urls.update({ 'threads': reverse( - 'API:_api_v2_ticket_' + str(ticket_type_name).lower().replace(' ', '_') + '_comment_threads-list', + 'API:_api_v2_ticket_comment_threads-list', request = self._context['view'].request, kwargs={ 'ticket_id': ticket_id, From e0baa57735dc6da8aeacfc5bc96c2d6758cf8282 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 13:50:42 +0930 Subject: [PATCH 368/617] test(core): Ticket Comment API field checks ref: #15 #248 #365 --- .../test_ticket_comment_api_v2.py | 872 ++++++++++++++++++ app/core/viewsets/ticket_comment.py | 6 + 2 files changed, 878 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment/test_ticket_comment_api_v2.py diff --git a/app/core/tests/unit/ticket_comment/test_ticket_comment_api_v2.py b/app/core/tests/unit/ticket_comment/test_ticket_comment_api_v2.py new file mode 100644 index 000000000..1e136eb60 --- /dev/null +++ b/app/core/tests/unit/ticket_comment/test_ticket_comment_api_v2.py @@ -0,0 +1,872 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_comment import TicketComment, TicketCommentCategory + + + +class TicketCommentAPI( + TestCase, + APITenancyObject +): + + model = TicketComment + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + + category = TicketCommentCategory.objects.create( + organization=self.organization, + name = 'cat' + ) + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.ticket = Ticket.objects.create( + organization=self.organization, + title = 'A ticket', + description = 'the ticket body', + opened_by = self.view_user, + ticket_type = int(Ticket.TicketType.REQUEST.value), + ) + + self.item = self.model.objects.create( + organization = self.organization, + body = 'one', + ticket = self.ticket, + category = category, + user = self.view_user, + external_ref = 1, + external_system = Ticket.Ticket_ExternalSystem.CUSTOM_1, + responsible_user = self.view_user, + responsible_team = view_team, + planned_start_date = '2024-01-01T01:02:03Z', + planned_finish_date = '2024-01-02T01:02:03Z', + real_start_date = '2024-01-03T01:02:03Z', + real_finish_date = '2024-01-04T01:02:03Z', + date_closed = '2024-01-05T01:02:03Z', + ) + + child_comment = self.model.objects.create( + organization = self.organization, + body = 'one', + ticket = self.ticket, + user = self.view_user, + parent = self.item, + ) + + + self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + + self.url_view_kwargs_child = {'ticket_id': self.ticket.id, 'parent_id': self.item.id, 'pk': child_comment.id} + + client = Client() + url = reverse('v2:_api_v2_ticket_comments-detail', kwargs=self.url_view_kwargs) + url_child = reverse('v2:_api_v2_ticket_comment_threads-detail', kwargs=self.url_view_kwargs_child) + + + client.force_login(self.view_user) + response = client.get(url) + response_child = client.get(url_child) + + self.api_data = response.data + self.api_data_child = response_child.data + + + + + def test_api_field_exists_display_name(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + display_name field must not exist + """ + + assert 'display_name' not in self.api_data + + + def test_api_field_type_display_name(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + model_notes field must not exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_body(self): + """ Test for existance of API Field + + body field must exist + """ + + assert 'body' in self.api_data + + + def test_api_field_type_body(self): + """ Test for type for API Field + + body field must be str + """ + + assert type(self.api_data['body']) is str + + + + def test_api_field_exists_parent(self): + """ Test for existance of API Field + + parent field must exist + """ + + assert 'parent' in self.api_data_child + + + def test_api_field_type_parent(self): + """ Test for type for API Field + + parent field must be int + """ + + assert type(self.api_data_child['parent']) is int + + + + def test_api_field_exists_ticket(self): + """ Test for existance of API Field + + ticket field must exist + """ + + assert 'ticket' in self.api_data + + + def test_api_field_type_ticket(self): + """ Test for type for API Field + + ticket field must be int + """ + + assert type(self.api_data['ticket']) is int + + + + def test_api_field_exists_external_ref(self): + """ Test for existance of API Field + + external_ref field must exist + """ + + assert 'external_ref' in self.api_data + + + def test_api_field_type_external_ref(self): + """ Test for type for API Field + + external_ref field must be int + """ + + assert type(self.api_data['external_ref']) is int + + + + def test_api_field_exists_external_system(self): + """ Test for existance of API Field + + external_system field must exist + """ + + assert 'external_system' in self.api_data + + + def test_api_field_type_external_system(self): + """ Test for type for API Field + + external_system field must be int + """ + + assert type(self.api_data['external_system']) is int + + + + def test_api_field_exists_comment_type(self): + """ Test for existance of API Field + + comment_type field must exist + """ + + assert 'comment_type' in self.api_data + + + def test_api_field_type_comment_type(self): + """ Test for type for API Field + + comment_type field must be int + """ + + assert type(self.api_data['comment_type']) is int + + + + def test_api_field_exists_private(self): + """ Test for existance of API Field + + private field must exist + """ + + assert 'private' in self.api_data + + + def test_api_field_type_private(self): + """ Test for type for API Field + + private field must be bool + """ + + assert type(self.api_data['private']) is bool + + + + def test_api_field_exists_duration(self): + """ Test for existance of API Field + + duration field must exist + """ + + assert 'duration' in self.api_data + + + def test_api_field_type_duration(self): + """ Test for type for API Field + + duration field must be int + """ + + assert type(self.api_data['duration']) is int + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be int + """ + + assert type(self.api_data['category']) is int + + + + def test_api_field_exists_template(self): + """ Test for existance of API Field + + template field must exist + """ + + assert 'template' in self.api_data + + + @pytest.mark.skip( reason = 'templating not yet implemented' ) + def test_api_field_type_template(self): + """ Test for type for API Field + + template field must be int + """ + + assert type(self.api_data['template']) is int + + + + def test_api_field_exists_is_template(self): + """ Test for existance of API Field + + is_template field must exist + """ + + assert 'is_template' in self.api_data + + + def test_api_field_type_is_template(self): + """ Test for type for API Field + + is_template field must be bool + """ + + assert type(self.api_data['is_template']) is bool + + + + def test_api_field_exists_source(self): + """ Test for existance of API Field + + source field must exist + """ + + assert 'source' in self.api_data + + + def test_api_field_type_source(self): + """ Test for type for API Field + + source field must be int + """ + + assert type(self.api_data['source']) is int + + + + def test_api_field_exists_status(self): + """ Test for existance of API Field + + status field must exist + """ + + assert 'status' in self.api_data + + + def test_api_field_type_status(self): + """ Test for type for API Field + + status field must be int + """ + + assert type(self.api_data['status']) is int + + + + def test_api_field_exists_responsible_user(self): + """ Test for existance of API Field + + responsible_user field must exist + """ + + assert 'responsible_user' in self.api_data + + + def test_api_field_type_responsible_user(self): + """ Test for type for API Field + + responsible_user field must be dict + """ + + assert type(self.api_data['responsible_user']) is dict + + + + def test_api_field_exists_responsible_user_id(self): + """ Test for existance of API Field + + responsible_user.id field must exist + """ + + assert 'id' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_id(self): + """ Test for type for API Field + + responsible_user.id field must be int + """ + + assert type(self.api_data['responsible_user']['id']) is int + + + def test_api_field_exists_responsible_user_display_name(self): + """ Test for existance of API Field + + responsible_user.display_name field must exist + """ + + assert 'display_name' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_display_name(self): + """ Test for type for API Field + + responsible_user.display_name field must be str + """ + + assert type(self.api_data['responsible_user']['display_name']) is str + + + def test_api_field_exists_responsible_user_first_name(self): + """ Test for existance of API Field + + responsible_user.first_name field must exist + """ + + assert 'first_name' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_first_name(self): + """ Test for type for API Field + + responsible_user.first_name field must be str + """ + + assert type(self.api_data['responsible_user']['first_name']) is str + + + def test_api_field_exists_responsible_user_last_name(self): + """ Test for existance of API Field + + responsible_user.last_name field must exist + """ + + assert 'last_name' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_last_name(self): + """ Test for type for API Field + + responsible_user.last_name field must be str + """ + + assert type(self.api_data['responsible_user']['last_name']) is str + + + def test_api_field_exists_responsible_user_username(self): + """ Test for existance of API Field + + responsible_user.username field must exist + """ + + assert 'username' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_username(self): + """ Test for type for API Field + + responsible_user.username field must be str + """ + + assert type(self.api_data['responsible_user']['username']) is str + + + def test_api_field_exists_responsible_user_is_active(self): + """ Test for existance of API Field + + responsible_user.is_active field must exist + """ + + assert 'is_active' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_is_active(self): + """ Test for type for API Field + + responsible_user.is_active field must be bool + """ + + assert type(self.api_data['responsible_user']['is_active']) is bool + + + def test_api_field_exists_responsible_user_url(self): + """ Test for existance of API Field + + responsible_user.url field must exist + """ + + assert 'url' in self.api_data['responsible_user'] + + + def test_api_field_type_responsible_user_url(self): + """ Test for type for API Field + + responsible_user.url field must be Hyperlink + """ + + assert type(self.api_data['responsible_user']['url']) is Hyperlink + + + + def test_api_field_exists_responsible_team(self): + """ Test for existance of API Field + + responsible_team field must exist + """ + + assert 'responsible_team' in self.api_data + + + def test_api_field_type_responsible_team(self): + """ Test for type for API Field + + responsible_team field must be dict + """ + + assert type(self.api_data['responsible_team']) is dict + + + + def test_api_field_exists_responsible_team_id(self): + """ Test for existance of API Field + + responsible_team.id field must exist + """ + + assert 'id' in self.api_data['responsible_team'] + + + def test_api_field_type_responsible_team_id(self): + """ Test for type for API Field + + responsible_team.id field must be int + """ + + assert type(self.api_data['responsible_team']['id']) is int + + + def test_api_field_exists_responsible_team_display_name(self): + """ Test for existance of API Field + + responsible_team.display_name field must exist + """ + + assert 'display_name' in self.api_data['responsible_team'] + + + def test_api_field_type_responsible_team_display_name(self): + """ Test for type for API Field + + responsible_team.display_name field must be str + """ + + assert type(self.api_data['responsible_team']['display_name']) is str + + + def test_api_field_exists_responsible_team_url(self): + """ Test for existance of API Field + + responsible_team.url field must exist + """ + + assert 'url' in self.api_data['responsible_team'] + + + def test_api_field_type_responsible_team_url(self): + """ Test for type for API Field + + responsible_team.url field must be str + """ + + assert type(self.api_data['responsible_team']['url']) is str + + + + def test_api_field_exists_user(self): + """ Test for existance of API Field + + user field must exist + """ + + assert 'user' in self.api_data + + + def test_api_field_type_user(self): + """ Test for type for API Field + + user field must be dict + """ + + assert type(self.api_data['user']) is dict + + + + def test_api_field_exists_user_id(self): + """ Test for existance of API Field + + user.id field must exist + """ + + assert 'id' in self.api_data['user'] + + + def test_api_field_type_user_id(self): + """ Test for type for API Field + + user.id field must be int + """ + + assert type(self.api_data['user']['id']) is int + + + def test_api_field_exists_user_display_name(self): + """ Test for existance of API Field + + user.display_name field must exist + """ + + assert 'display_name' in self.api_data['user'] + + + def test_api_field_type_user_display_name(self): + """ Test for type for API Field + + user.display_name field must be str + """ + + assert type(self.api_data['user']['display_name']) is str + + + def test_api_field_exists_user_first_name(self): + """ Test for existance of API Field + + user.first_name field must exist + """ + + assert 'first_name' in self.api_data['user'] + + + def test_api_field_type_user_first_name(self): + """ Test for type for API Field + + user.first_name field must be str + """ + + assert type(self.api_data['user']['first_name']) is str + + + def test_api_field_exists_user_last_name(self): + """ Test for existance of API Field + + user.last_name field must exist + """ + + assert 'last_name' in self.api_data['user'] + + + def test_api_field_type_user_last_name(self): + """ Test for type for API Field + + user.last_name field must be str + """ + + assert type(self.api_data['user']['last_name']) is str + + + def test_api_field_exists_user_username(self): + """ Test for existance of API Field + + user.username field must exist + """ + + assert 'username' in self.api_data['user'] + + + def test_api_field_type_user_username(self): + """ Test for type for API Field + + user.username field must be str + """ + + assert type(self.api_data['user']['username']) is str + + + def test_api_field_exists_user_is_active(self): + """ Test for existance of API Field + + user.is_active field must exist + """ + + assert 'is_active' in self.api_data['user'] + + + def test_api_field_type_user_is_active(self): + """ Test for type for API Field + + user.is_active field must be bool + """ + + assert type(self.api_data['user']['is_active']) is bool + + + def test_api_field_exists_user_url(self): + """ Test for existance of API Field + + user.url field must exist + """ + + assert 'url' in self.api_data['user'] + + + def test_api_field_type_user_url(self): + """ Test for type for API Field + + user.url field must be Hyperlink + """ + + assert type(self.api_data['user']['url']) is Hyperlink + + + + def test_api_field_exists_planned_start_date(self): + """ Test for existance of API Field + + planned_start_date field must exist + """ + + assert 'planned_start_date' in self.api_data + + + def test_api_field_type_planned_start_date(self): + """ Test for type for API Field + + planned_start_date field must be str + """ + + assert type(self.api_data['planned_start_date']) is str + + + + def test_api_field_exists_planned_finish_date(self): + """ Test for existance of API Field + + planned_finish_date field must exist + """ + + assert 'planned_finish_date' in self.api_data + + + def test_api_field_type_planned_finish_date(self): + """ Test for type for API Field + + planned_finish_date field must be str + """ + + assert type(self.api_data['planned_finish_date']) is str + + + + def test_api_field_exists_real_start_date(self): + """ Test for existance of API Field + + real_start_date field must exist + """ + + assert 'real_start_date' in self.api_data + + + def test_api_field_type_real_start_date(self): + """ Test for type for API Field + + real_start_date field must be str + """ + + assert type(self.api_data['real_start_date']) is str + + + + def test_api_field_exists_real_finish_date(self): + """ Test for existance of API Field + + real_finish_date field must exist + """ + + assert 'real_finish_date' in self.api_data + + + def test_api_field_type_real_finish_date(self): + """ Test for type for API Field + + real_finish_date field must be str + """ + + assert type(self.api_data['real_finish_date']) is str + + + + def test_api_field_exists_date_closed(self): + """ Test for existance of API Field + + date_closed field must exist + """ + + assert 'date_closed' in self.api_data + + + def test_api_field_type_date_closed(self): + """ Test for type for API Field + + date_closed field must be str + """ + + assert type(self.api_data['date_closed']) is str + diff --git a/app/core/viewsets/ticket_comment.py b/app/core/viewsets/ticket_comment.py index 934780684..aa5afa0cc 100644 --- a/app/core/viewsets/ticket_comment.py +++ b/app/core/viewsets/ticket_comment.py @@ -91,7 +91,13 @@ class ViewSet(ModelViewSet): filterset_fields = [ 'category', 'external_system', + 'external_system', + 'is_template', 'organization', + 'parent', + 'source', + 'status', + 'template', ] search_fields = [ From 5589b67e24550f0dc960bb8c847e28f0f6e5cb5f Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:14:36 +0930 Subject: [PATCH 369/617] test(core): Ticket Linked Items API field checks ref: #15 #248 #365 --- .../test_ticket_linked_item_api_v2.py | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_api_v2.py diff --git a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_api_v2.py b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_api_v2.py new file mode 100644 index 000000000..6c3e56508 --- /dev/null +++ b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_api_v2.py @@ -0,0 +1,370 @@ +# import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket_linked_items import Ticket, TicketLinkedItem + +from itam.models.device import Device + + + +class TicketLinkedItemAPI( + TestCase, + APITenancyObject +): + + model = TicketLinkedItem + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + + self.ticket = Ticket.objects.create( + organization=self.organization, + title = 'A ticket', + description = 'the ticket body', + opened_by = self.view_user, + ticket_type = int(Ticket.TicketType.REQUEST.value), + ) + + device = Device.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + item = device.id, + item_type = TicketLinkedItem.Modules.DEVICE, + ticket = self.ticket + ) + + + self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + + + client = Client() + url = reverse('v2:_api_v2_ticket_linked_item-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + model_notes field must not exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + modified field must not exist + """ + + assert 'modified' not in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_item(self): + """ Test for existance of API Field + + item field must exist + """ + + assert 'item' in self.api_data + + + def test_api_field_type_item(self): + """ Test for type for API Field + + item field must be dict + """ + + assert type(self.api_data['item']) is dict + + + + + + + + + + + + + def test_api_field_exists_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert 'id' in self.api_data['item'] + + + def test_api_field_type_item_id(self): + """ Test for type for API Field + + item.id field must be int + """ + + assert type(self.api_data['item']['id']) is int + + + def test_api_field_exists_item_display_name(self): + """ Test for existance of API Field + + item.display_name field must exist + """ + + assert 'display_name' in self.api_data['item'] + + + def test_api_field_type_item_display_name(self): + """ Test for type for API Field + + item.display_name field must be str + """ + + assert type(self.api_data['item']['display_name']) is str + + + def test_api_field_exists_item_name(self): + """ Test for existance of API Field + + item.name field must exist + """ + + assert 'name' in self.api_data['item'] + + + def test_api_field_type_item_name(self): + """ Test for type for API Field + + item.name field must be str + """ + + assert type(self.api_data['item']['name']) is str + + + + def test_api_field_exists_item_url(self): + """ Test for existance of API Field + + item.url field must exist + """ + + assert 'url' in self.api_data['item'] + + + def test_api_field_type_item_url(self): + """ Test for type for API Field + + item.url field must be Hyperlink + """ + + assert type(self.api_data['item']['url']) is Hyperlink + + + + + + + + + + + + def test_api_field_exists_item_type(self): + """ Test for existance of API Field + + item_type field must exist + """ + + assert 'item_type' in self.api_data + + + def test_api_field_type_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert type(self.api_data['item_type']) is int + + + + def test_api_field_exists_ticket(self): + """ Test for existance of API Field + + ticket field must exist + """ + + assert 'ticket' in self.api_data + + + def test_api_field_type_ticket(self): + """ Test for type for API Field + + ticket field must be dict + """ + + assert type(self.api_data['ticket']) is dict + + + + + + + + + def test_api_field_exists_ticket_id(self): + """ Test for existance of API Field + + ticket.id field must exist + """ + + assert 'id' in self.api_data['ticket'] + + + def test_api_field_type_ticket_id(self): + """ Test for type for API Field + + ticket.id field must be int + """ + + assert type(self.api_data['ticket']['id']) is int + + + def test_api_field_exists_ticket_display_name(self): + """ Test for existance of API Field + + ticket.display_name field must exist + """ + + assert 'display_name' in self.api_data['ticket'] + + + def test_api_field_type_ticket_display_name(self): + """ Test for type for API Field + + ticket.display_name field must be str + """ + + assert type(self.api_data['ticket']['display_name']) is str + + + def test_api_field_exists_ticket_title(self): + """ Test for existance of API Field + + ticket.title field must exist + """ + + assert 'title' in self.api_data['ticket'] + + + def test_api_field_type_ticket_title(self): + """ Test for type for API Field + + ticket.title field must be str + """ + + assert type(self.api_data['ticket']['title']) is str + + + + def test_api_field_exists_ticket_url(self): + """ Test for existance of API Field + + ticket.url field must exist + """ + + assert 'url' in self.api_data['ticket'] + + + def test_api_field_type_ticket_url(self): + """ Test for type for API Field + + ticket.url field must be str + """ + + assert type(self.api_data['ticket']['url']) is str + From 805cc888a538842c058cda5b28bafaefcd3c344b Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:57:05 +0930 Subject: [PATCH 370/617] test(core): Linked Ticket Common API field checks test cases common to ALL linked tickets ref: #15 #248 #365 --- .../tests/abstract/test_item_ticket_api_v2.py | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 app/core/tests/abstract/test_item_ticket_api_v2.py diff --git a/app/core/tests/abstract/test_item_ticket_api_v2.py b/app/core/tests/abstract/test_item_ticket_api_v2.py new file mode 100644 index 000000000..d6e656bf0 --- /dev/null +++ b/app/core/tests/abstract/test_item_ticket_api_v2.py @@ -0,0 +1,323 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket_linked_items import Ticket, TicketLinkedItem + + + +class ItemTicketAPI( + APITenancyObject +): + """Common Test Cases for Linked Ticket Items + + This class must be inherited by all test classes by item type. + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + model = TicketLinkedItem + + @classmethod + def setUpTestData(self): + """Setup Test + + This setup method must be called from inheriting class with `super` + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + + self.ticket = Ticket.objects.create( + organization = self.organization, + title = 'A ticket', + description = 'the ticket body', + opened_by = self.view_user, + ticket_type = int(Ticket.TicketType.REQUEST.value), + ) + + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + model_notes field must not exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + modified field must not exist + """ + + assert 'modified' not in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_item(self): + """ Test for existance of API Field + + item field must exist + """ + + assert 'item' in self.api_data + + + def test_api_field_type_item(self): + """ Test for type for API Field + + item field must be dict + """ + + assert type(self.api_data['item']) is dict + + + + def test_api_field_exists_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert 'id' in self.api_data['item'] + + + def test_api_field_type_item_id(self): + """ Test for type for API Field + + item.id field must be int + """ + + assert type(self.api_data['item']['id']) is int + + + def test_api_field_exists_item_display_name(self): + """ Test for existance of API Field + + item.display_name field must exist + """ + + assert 'display_name' in self.api_data['item'] + + + def test_api_field_type_item_display_name(self): + """ Test for type for API Field + + item.display_name field must be str + """ + + assert type(self.api_data['item']['display_name']) is str + + + def test_api_field_exists_item_name(self): + """ Test for existance of API Field + + item.name field must exist + """ + + assert 'name' in self.api_data['item'] + + + def test_api_field_type_item_name(self): + """ Test for type for API Field + + item.name field must be str + """ + + assert type(self.api_data['item']['name']) is str + + + + def test_api_field_exists_item_url(self): + """ Test for existance of API Field + + item.url field must exist + """ + + assert 'url' in self.api_data['item'] + + + def test_api_field_type_item_url(self): + """ Test for type for API Field + + item.url field must be Hyperlink + """ + + assert type(self.api_data['item']['url']) is Hyperlink + + + + def test_api_field_exists_item_type(self): + """ Test for existance of API Field + + item_type field must exist + """ + + assert 'item_type' in self.api_data + + + def test_api_field_type_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert type(self.api_data['item_type']) is int + + + + def test_api_field_exists_ticket(self): + """ Test for existance of API Field + + ticket field must exist + """ + + assert 'ticket' in self.api_data + + + def test_api_field_type_ticket(self): + """ Test for type for API Field + + ticket field must be dict + """ + + assert type(self.api_data['ticket']) is dict + + + + def test_api_field_exists_ticket_id(self): + """ Test for existance of API Field + + ticket.id field must exist + """ + + assert 'id' in self.api_data['ticket'] + + + def test_api_field_type_ticket_id(self): + """ Test for type for API Field + + ticket.id field must be int + """ + + assert type(self.api_data['ticket']['id']) is int + + + def test_api_field_exists_ticket_display_name(self): + """ Test for existance of API Field + + ticket.display_name field must exist + """ + + assert 'display_name' in self.api_data['ticket'] + + + def test_api_field_type_ticket_display_name(self): + """ Test for type for API Field + + ticket.display_name field must be str + """ + + assert type(self.api_data['ticket']['display_name']) is str + + + def test_api_field_exists_ticket_title(self): + """ Test for existance of API Field + + ticket.title field must exist + """ + + assert 'title' in self.api_data['ticket'] + + + def test_api_field_type_ticket_title(self): + """ Test for type for API Field + + ticket.title field must be str + """ + + assert type(self.api_data['ticket']['title']) is str + + + + def test_api_field_exists_ticket_url(self): + """ Test for existance of API Field + + ticket.url field must exist + """ + + assert 'url' in self.api_data['ticket'] + + + def test_api_field_type_ticket_url(self): + """ Test for type for API Field + + ticket.url field must be str + """ + + assert type(self.api_data['ticket']['url']) is str From 76320251a1251fd424eb33d4564669a9b02c10bb Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:57:34 +0930 Subject: [PATCH 371/617] test(core): Config Group Linked Tickets API field checks ref: #15 #248 #365 --- .../test_config_group_item_ticket_api_v2.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py diff --git a/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py b/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py new file mode 100644 index 000000000..a74f4890d --- /dev/null +++ b/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py @@ -0,0 +1,103 @@ + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from core.tests.abstract.test_item_ticket_api_v2 import ItemTicketAPI + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from config_management.models.groups import ConfigGroups + + + +class ServiceItemTicketAPI( + ItemTicketAPI, + TestCase, +): + """Test Cases for Item Tickets + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + item_type = TicketLinkedItem.Modules.CONFIG_GROUP + + item_class = 'config_group' + + item_model = ConfigGroups + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + ticket = self.ticket, + + # Item attributes + + item = self.linked_item.id, + item_type = self.item_type, + ) + + + + self.url_view_kwargs = { + 'item_class': self.item_class, + 'item_id': self.item.id, + 'pk': self.item.id, + } + + + client = Client() + url = reverse('v2:_api_v2_item_tickets-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_value_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert self.api_data['item']['id'] == self.linked_item.id + + + + def test_api_field_value_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert self.api_data['item_type'] == self.item_type + + def test_api_field_type_item_url(self): + """ Test for type for API Field + + item.url field must be str + """ + + assert type(self.api_data['item']['url']) is str From 5381b96ad064e2a8f104064f075da70b1b62818a Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:57:59 +0930 Subject: [PATCH 372/617] test(itam): device Linked Tickets API field checks ref: #15 #248 #365 --- .../device/test_device_item_ticket_api_v2.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/itam/tests/unit/device/test_device_item_ticket_api_v2.py diff --git a/app/itam/tests/unit/device/test_device_item_ticket_api_v2.py b/app/itam/tests/unit/device/test_device_item_ticket_api_v2.py new file mode 100644 index 000000000..0bc747f42 --- /dev/null +++ b/app/itam/tests/unit/device/test_device_item_ticket_api_v2.py @@ -0,0 +1,95 @@ + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from core.tests.abstract.test_item_ticket_api_v2 import ItemTicketAPI + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from itam.models.device import Device + + + +class DeviceItemTicketAPI( + ItemTicketAPI, + TestCase, +): + """Test Cases for Item Tickets + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + item_type = TicketLinkedItem.Modules.DEVICE + + item_class = 'device' + + item_model = Device + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + ticket = self.ticket, + + # Item attributes + + item = self.linked_item.id, + item_type = self.item_type, + ) + + + + self.url_view_kwargs = { + 'item_class': self.item_class, + 'item_id': self.item.id, + 'pk': self.item.id, + } + + + client = Client() + url = reverse('v2:_api_v2_item_tickets-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_value_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert self.api_data['item']['id'] == self.linked_item.id + + + + def test_api_field_value_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert self.api_data['item_type'] == self.item_type From 873f8e16f27af21652b5d71c202c0ba11ee8c2ae Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:58:16 +0930 Subject: [PATCH 373/617] test(itam): Operating System Linked Tickets API field checks ref: #15 #248 #365 --- ...est_operating_system_item_ticket_api_v2.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/itam/tests/unit/operating_system/test_operating_system_item_ticket_api_v2.py diff --git a/app/itam/tests/unit/operating_system/test_operating_system_item_ticket_api_v2.py b/app/itam/tests/unit/operating_system/test_operating_system_item_ticket_api_v2.py new file mode 100644 index 000000000..914e86992 --- /dev/null +++ b/app/itam/tests/unit/operating_system/test_operating_system_item_ticket_api_v2.py @@ -0,0 +1,95 @@ + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from core.tests.abstract.test_item_ticket_api_v2 import ItemTicketAPI + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from itam.models.operating_system import OperatingSystem + + + +class OperatingSystemItemTicketAPI( + ItemTicketAPI, + TestCase, +): + """Test Cases for Item Tickets + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + item_type = TicketLinkedItem.Modules.OPERATING_SYSTEM + + item_class = 'operating_system' + + item_model = OperatingSystem + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + ticket = self.ticket, + + # Item attributes + + item = self.linked_item.id, + item_type = self.item_type, + ) + + + + self.url_view_kwargs = { + 'item_class': self.item_class, + 'item_id': self.item.id, + 'pk': self.item.id, + } + + + client = Client() + url = reverse('v2:_api_v2_item_tickets-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_value_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert self.api_data['item']['id'] == self.linked_item.id + + + + def test_api_field_value_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert self.api_data['item_type'] == self.item_type From 68ee0b3701a5b2f6c553535049f0af8e116ec237 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:58:28 +0930 Subject: [PATCH 374/617] test(itam): Software Linked Tickets API field checks ref: #15 #248 #365 --- .../test_software_item_ticket_api_v2.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/itam/tests/unit/software/test_software_item_ticket_api_v2.py diff --git a/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py b/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py new file mode 100644 index 000000000..fa7cfd7d4 --- /dev/null +++ b/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py @@ -0,0 +1,95 @@ + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from core.tests.abstract.test_item_ticket_api_v2 import ItemTicketAPI + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from itam.models.software import Software + + + +class SoftwareItemTicketAPI( + ItemTicketAPI, + TestCase, +): + """Test Cases for Item Tickets + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + item_type = TicketLinkedItem.Modules.SOFTWARE + + item_class = 'software' + + item_model = Software + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + ticket = self.ticket, + + # Item attributes + + item = self.linked_item.id, + item_type = self.item_type, + ) + + + + self.url_view_kwargs = { + 'item_class': self.item_class, + 'item_id': self.item.id, + 'pk': self.item.id, + } + + + client = Client() + url = reverse('v2:_api_v2_item_tickets-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_value_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert self.api_data['item']['id'] == self.linked_item.id + + + + def test_api_field_value_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert self.api_data['item_type'] == self.item_type From 0ba1a34ee71c5bbbadf008449036b0666bce5ea3 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:58:48 +0930 Subject: [PATCH 375/617] test(itim): Cluster Linked Tickets API field checks ref: #15 #248 #365 --- .../test_cluster_item_ticket_api_v2.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/itim/tests/unit/cluster/test_cluster_item_ticket_api_v2.py diff --git a/app/itim/tests/unit/cluster/test_cluster_item_ticket_api_v2.py b/app/itim/tests/unit/cluster/test_cluster_item_ticket_api_v2.py new file mode 100644 index 000000000..2b1adb6c3 --- /dev/null +++ b/app/itim/tests/unit/cluster/test_cluster_item_ticket_api_v2.py @@ -0,0 +1,95 @@ + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from core.tests.abstract.test_item_ticket_api_v2 import ItemTicketAPI + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from itim.models.clusters import Cluster + + + +class ClusterItemTicketAPI( + ItemTicketAPI, + TestCase, +): + """Test Cases for Item Tickets + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + item_type = TicketLinkedItem.Modules.CLUSTER + + item_class = 'cluster' + + item_model = Cluster + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + ticket = self.ticket, + + # Item attributes + + item = self.linked_item.id, + item_type = self.item_type, + ) + + + + self.url_view_kwargs = { + 'item_class': self.item_class, + 'item_id': self.item.id, + 'pk': self.item.id, + } + + + client = Client() + url = reverse('v2:_api_v2_item_tickets-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_value_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert self.api_data['item']['id'] == self.linked_item.id + + + + def test_api_field_value_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert self.api_data['item_type'] == self.item_type From 5cb329c2827454cfa52a8a4803fd861279299be8 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 14:59:03 +0930 Subject: [PATCH 376/617] test(itim): Service Linked Tickets API field checks ref: #15 #248 #365 --- .../test_service_item_ticket_api_v2.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/itim/tests/unit/service/test_service_item_ticket_api_v2.py diff --git a/app/itim/tests/unit/service/test_service_item_ticket_api_v2.py b/app/itim/tests/unit/service/test_service_item_ticket_api_v2.py new file mode 100644 index 000000000..e372a14e2 --- /dev/null +++ b/app/itim/tests/unit/service/test_service_item_ticket_api_v2.py @@ -0,0 +1,95 @@ + +from django.shortcuts import reverse +from django.test import Client, TestCase + +from core.tests.abstract.test_item_ticket_api_v2 import ItemTicketAPI + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from itim.models.services import Service + + + +class ServiceItemTicketAPI( + ItemTicketAPI, + TestCase, +): + """Test Cases for Item Tickets + + Args: + APITenancyObject (class): Base class for ALL field checks + """ + + item_type = TicketLinkedItem.Modules.SERVICE + + item_class = 'service' + + item_model = Service + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'dev' + ) + + + + self.item = self.model.objects.create( + organization = self.organization, + ticket = self.ticket, + + # Item attributes + + item = self.linked_item.id, + item_type = self.item_type, + ) + + + + self.url_view_kwargs = { + 'item_class': self.item_class, + 'item_id': self.item.id, + 'pk': self.item.id, + } + + + client = Client() + url = reverse('v2:_api_v2_item_tickets-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_value_item_id(self): + """ Test for existance of API Field + + item.id field must exist + """ + + assert self.api_data['item']['id'] == self.linked_item.id + + + + def test_api_field_value_item_type(self): + """ Test for type for API Field + + item_type field must be int + """ + + assert self.api_data['item_type'] == self.item_type From 71ad05e051f282ac9b4fa38b75284a4d7c259dbb Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 15:17:21 +0930 Subject: [PATCH 377/617] test(core): Related Tickets API field checks ref: #15 #248 #365 #367 --- .../test_related_ticket_api_v2.py | 379 ++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 app/core/tests/unit/related_ticket/test_related_ticket_api_v2.py diff --git a/app/core/tests/unit/related_ticket/test_related_ticket_api_v2.py b/app/core/tests/unit/related_ticket/test_related_ticket_api_v2.py new file mode 100644 index 000000000..a3c49e979 --- /dev/null +++ b/app/core/tests/unit/related_ticket/test_related_ticket_api_v2.py @@ -0,0 +1,379 @@ +# import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket import Ticket, RelatedTickets + +from itam.models.device import Device + + + +class RelatedTicketsLinkedItemAPI( + TestCase, + APITenancyObject +): + + model = RelatedTickets + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + + self.ticket = Ticket.objects.create( + organization=self.organization, + title = 'A ticket', + description = 'the ticket body', + opened_by = self.view_user, + ticket_type = int(Ticket.TicketType.REQUEST.value), + ) + + self.ticket_two = Ticket.objects.create( + organization=self.organization, + title = 'A ticket two', + description = 'the ticket body', + opened_by = self.view_user, + ticket_type = int(Ticket.TicketType.REQUEST.value), + ) + + # device = Device.objects.create( + # organization = self.organization, + # name = 'dev' + # ) + + + + self.item = self.model.objects.create( + organization = self.organization, + from_ticket_id = self.ticket, + to_ticket_id = self.ticket_two, + how_related = RelatedTickets.Related.RELATED + ) + + + self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + + + client = Client() + url = reverse('v2:_api_v2_ticket_related-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + + + def test_api_field_exists_model_notes(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + model_notes field must not exist + """ + + assert 'model_notes' not in self.api_data + + + def test_api_field_type_model_notes(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_created(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + created field must not exist + """ + + assert 'created' not in self.api_data + + + def test_api_field_type_created(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_modified(self): + """ Test for existance of API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + + modified field must not exist + """ + + assert 'modified' not in self.api_data + + + def test_api_field_type_modified(self): + """ Test for type for API Field + + This is a custom test of a test case with the same name. + This model does not have this field. + """ + + assert True + + + + def test_api_field_exists_from_ticket_id(self): + """ Test for existance of API Field + + from_ticket_id field must exist + """ + + assert 'from_ticket_id' in self.api_data + + + def test_api_field_type_from_ticket_id(self): + """ Test for type for API Field + + from_ticket_id field must be dict + """ + + assert type(self.api_data['from_ticket_id']) is dict + + + + def test_api_field_exists_from_ticket_id_id(self): + """ Test for existance of API Field + + from_ticket_id.id field must exist + """ + + assert 'id' in self.api_data['from_ticket_id'] + + + def test_api_field_type_from_ticket_id_id(self): + """ Test for type for API Field + + from_ticket_id.id field must be int + """ + + assert type(self.api_data['from_ticket_id']['id']) is int + + + def test_api_field_exists_from_ticket_id_display_name(self): + """ Test for existance of API Field + + from_ticket_id.display_name field must exist + """ + + assert 'display_name' in self.api_data['from_ticket_id'] + + + def test_api_field_type_from_ticket_id_display_name(self): + """ Test for type for API Field + + from_ticket_id.display_name field must be str + """ + + assert type(self.api_data['from_ticket_id']['display_name']) is str + + + def test_api_field_exists_from_ticket_id_title(self): + """ Test for existance of API Field + + from_ticket_id.title field must exist + """ + + assert 'title' in self.api_data['from_ticket_id'] + + + def test_api_field_type_from_ticket_id_title(self): + """ Test for type for API Field + + from_ticket_id.title field must be str + """ + + assert type(self.api_data['from_ticket_id']['title']) is str + + + + def test_api_field_exists_from_ticket_id_url(self): + """ Test for existance of API Field + + from_ticket_id.url field must exist + """ + + assert 'url' in self.api_data['from_ticket_id'] + + + def test_api_field_type_from_ticket_id_url(self): + """ Test for type for API Field + + from_ticket_id.url field must be str + """ + + assert type(self.api_data['from_ticket_id']['url']) is str + + + + def test_api_field_exists_to_ticket_id(self): + """ Test for existance of API Field + + to_ticket_id field must exist + """ + + assert 'to_ticket_id' in self.api_data + + + def test_api_field_type_to_ticket_id(self): + """ Test for type for API Field + + to_ticket_id field must be dict + """ + + assert type(self.api_data['to_ticket_id']) is dict + + + + def test_api_field_exists_to_ticket_id_id(self): + """ Test for existance of API Field + + to_ticket_id.id field must exist + """ + + assert 'id' in self.api_data['to_ticket_id'] + + + def test_api_field_type_to_ticket_id_id(self): + """ Test for type for API Field + + to_ticket_id.id field must be int + """ + + assert type(self.api_data['to_ticket_id']['id']) is int + + + def test_api_field_exists_to_ticket_id_display_name(self): + """ Test for existance of API Field + + to_ticket_id.display_name field must exist + """ + + assert 'display_name' in self.api_data['to_ticket_id'] + + + def test_api_field_type_to_ticket_id_display_name(self): + """ Test for type for API Field + + to_ticket_id.display_name field must be str + """ + + assert type(self.api_data['to_ticket_id']['display_name']) is str + + + def test_api_field_exists_to_ticket_id_title(self): + """ Test for existance of API Field + + to_ticket_id.title field must exist + """ + + assert 'title' in self.api_data['to_ticket_id'] + + + def test_api_field_type_to_ticket_id_title(self): + """ Test for type for API Field + + to_ticket_id.title field must be str + """ + + assert type(self.api_data['to_ticket_id']['title']) is str + + + + def test_api_field_exists_to_ticket_id_url(self): + """ Test for existance of API Field + + to_ticket_id.url field must exist + """ + + assert 'url' in self.api_data['to_ticket_id'] + + + def test_api_field_type_to_ticket_id_url(self): + """ Test for type for API Field + + to_ticket_id.url field must be str + """ + + assert type(self.api_data['to_ticket_id']['url']) is str + + + + def test_api_field_exists_how_related(self): + """ Test for existance of API Field + + how_related field must exist + """ + + assert 'how_related' in self.api_data + + + def test_api_field_type_how_related(self): + """ Test for type for API Field + + how_related field must be int + """ + + assert type(self.api_data['how_related']) is int + From 848c34239795bf1a92d38a33abbe66d1bd9ec193 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 15:31:15 +0930 Subject: [PATCH 378/617] feat(core): Add Ticket Comment Category API v2 endpoint ref: #248 #365 --- .../serializers/ticket_comment_category.py | 89 +++++++++++++++++++ app/core/viewsets/ticket_comment_category.py | 80 +++++++++++++++++ app/settings/viewsets/index.py | 5 ++ 3 files changed, 174 insertions(+) create mode 100644 app/core/serializers/ticket_comment_category.py create mode 100644 app/core/viewsets/ticket_comment_category.py diff --git a/app/core/serializers/ticket_comment_category.py b/app/core/serializers/ticket_comment_category.py new file mode 100644 index 000000000..aab0644a7 --- /dev/null +++ b/app/core/serializers/ticket_comment_category.py @@ -0,0 +1,89 @@ +from rest_framework.reverse import reverse +from rest_framework import serializers + +from access.serializers.organization import OrganizationBaseSerializer + +from app.serializers.user import UserBaseSerializer + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + + +class TicketCommentCategoryBaseSerializer(serializers.ModelSerializer): + + display_name = serializers.SerializerMethodField('get_display_name') + + def get_display_name(self, item): + + return str( item ) + + url = serializers.HyperlinkedIdentityField( + view_name="v2:_api_v2_ticket_comment_category-detail", format="html" + ) + + + class Meta: + + model = TicketCommentCategory + + fields = [ + 'id', + 'display_name', + 'url', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'url', + ] + + +class TicketCommentCategoryModelSerializer(TicketCommentCategoryBaseSerializer): + + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': reverse("v2:_api_v2_ticket_comment_category-detail", + request=self._context['view'].request, + kwargs={ + 'pk': item.pk + } + ) + } + + + class Meta: + + model = TicketCommentCategory + + fields = '__all__' + + fields = [ + 'id', + 'organization', + 'display_name', + 'name', + 'model_notes', + 'is_global', + 'created', + 'modified', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCommentCategoryViewSerializer(TicketCommentCategoryModelSerializer): + + organization = OrganizationBaseSerializer( read_only = True ) diff --git a/app/core/viewsets/ticket_comment_category.py b/app/core/viewsets/ticket_comment_category.py new file mode 100644 index 000000000..a736e491b --- /dev/null +++ b/app/core/viewsets/ticket_comment_category.py @@ -0,0 +1,80 @@ +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from core.serializers.ticket_comment_category import ( + TicketCommentCategory, + TicketCommentCategoryModelSerializer, + TicketCommentCategoryViewSerializer +) + +from api.viewsets.common import ModelViewSet + + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a ticket comment category', + description='', + responses = { + 201: OpenApiResponse(description='Created', response=TicketCommentCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a ticket comment category', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all ticket comment categories', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketCommentCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a single ticket comment category', + description='', + responses = { + 200: OpenApiResponse(description='', response=TicketCommentCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a ticket comment category', + description = '', + responses = { + 200: OpenApiResponse(description='', response=TicketCommentCategoryViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(ModelViewSet): + + filterset_fields = [ + 'organization', + ] + + search_fields = [ + 'name', + ] + + model = TicketCommentCategory + + + def get_serializer_class(self): + + if ( + self.action == 'list' + or self.action == 'retrieve' + ): + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer'] + + + return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer'] diff --git a/app/settings/viewsets/index.py b/app/settings/viewsets/index.py index f36563d96..8d1ebdf77 100644 --- a/app/settings/viewsets/index.py +++ b/app/settings/viewsets/index.py @@ -54,6 +54,10 @@ class Index(CommonViewSet): { "name": "Ticket Category", "model": "ticket_category" + }, + { + "name": "Ticket Comment Category", + "model": "ticket_comment_category" } ] }, @@ -124,6 +128,7 @@ def list(self, request, pk=None): "project_type": reverse('v2:_api_v2_project_type-list', request=request), "software_category": reverse('v2:_api_v2_software_category-list', request=request), "ticket_category": reverse('v2:_api_v2_ticket_category-list', request=request), + "ticket_comment_category": reverse('v2:_api_v2_ticket_comment_category-list', request=request), "user_settings": reverse( 'v2:_api_v2_user_settings-detail', request=request, From 858217d2a287c48e62dd8d229115943d3a54e1b3 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 15:41:42 +0930 Subject: [PATCH 379/617] test(core): Ticket Comment Category API field checks ref: #15 #248 #365 --- .../models/ticket/ticket_comment_category.py | 15 +-- .../test_ticket_comment_category_api_v2.py | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_api_v2.py diff --git a/app/core/models/ticket/ticket_comment_category.py b/app/core/models/ticket/ticket_comment_category.py index cc87a7987..bdd7550c4 100644 --- a/app/core/models/ticket/ticket_comment_category.py +++ b/app/core/models/ticket/ticket_comment_category.py @@ -107,9 +107,7 @@ class Meta: "layout": "double", "left": [ 'organization', - 'parent' - 'name' - 'runbook', + 'name', 'is_global', ], "right": [ @@ -118,17 +116,6 @@ class Meta: 'modified', ] }, - { - "layout": "double", - "left": [ - 'comment', - 'solution' - ], - "right": [ - 'notification', - 'task', - ] - } ] }, { diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_api_v2.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_api_v2.py new file mode 100644 index 000000000..35c07dcff --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_api_v2.py @@ -0,0 +1,110 @@ +import pytest +import unittest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_fields import APITenancyObject + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + + +class TicketCategoryAPI( + TestCase, + APITenancyObject +): + + model = TicketCommentCategory + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + self.organization = Organization.objects.create(name='test_org') + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one', + model_notes = 'text' + ) + + self.url_view_kwargs = {'pk': self.item.id} + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = self.organization, + ) + + view_team.permissions.set([view_permissions]) + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + client = Client() + url = reverse('v2:_api_v2_ticket_comment_category-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_name(self): + """ Test for existance of API Field + + name field must exist + """ + + assert 'name' in self.api_data + + + def test_api_field_type_name(self): + """ Test for type for API Field + + name field must be str + """ + + assert type(self.api_data['name']) is str + + + + # def test_api_field_exists_url_history(self): + # """ Test for existance of API Field + + # _urls.history field must exist + # """ + + # assert 'history' in self.api_data['_urls'] + + + # def test_api_field_type_url_history(self): + # """ Test for type for API Field + + # _urls.history field must be str + # """ + + # assert type(self.api_data['_urls']['history']) is str From 28805ed727d661dd5ad110ff6c01d00c6a011ade Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 16:53:20 +0930 Subject: [PATCH 380/617] test(core): Ticket Common API v2 ViewSet permission checks Test cases common to ALL ticket types ref: #15 #248 #365 --- .../tests/abstract/test_ticket_viewset.py | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 app/core/tests/abstract/test_ticket_viewset.py diff --git a/app/core/tests/abstract/test_ticket_viewset.py b/app/core/tests/abstract/test_ticket_viewset.py new file mode 100644 index 000000000..bbb637091 --- /dev/null +++ b/app/core/tests/abstract/test_ticket_viewset.py @@ -0,0 +1,364 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.ticket.ticket import Ticket + +from settings.models.user_settings import UserSettings + + + +class TicketViewSetPermissionsAPI( + APIPermissions +): + """ Test Cases common to ALL ticket types """ + + model = Ticket + + app_namespace = 'v2' + + change_data = {'title': 'device'} + + delete_data = {} + + ticket_type: str = None + """Name of ticket type in lower case, i.e. `request`""" + + ticket_type_enum:object = None + """Ticket Type Enum for Ticket.TicketType""" + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + self.url_name = '_api_v2_ticket_' + self.ticket_type + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + self.url_kwargs = {} + + + view_permissions = Permission.objects.get( + codename = 'view_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + + triage_permissions = Permission.objects.get( + codename = 'triage_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + triage_team = Team.objects.create( + team_name = 'triage_team', + organization = organization, + ) + + triage_team.permissions.set([triage_permissions]) + + + + import_permissions = Permission.objects.get( + codename = 'import_ticket_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + import_team = Team.objects.create( + team_name = 'import_team', + organization = organization, + ) + + import_team.permissions.set([import_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + title = 'one', + description = 'some text for body', + opened_by = self.view_user, + ticket_type = self.ticket_type_enum, + status = Ticket.TicketStatus.All.NEW + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'title': 'team_post', + 'organization': self.organization.id, + 'description': 'article text', + 'ticket_type': int(self.ticket_type_enum), + 'status': int(Ticket.TicketStatus.All.NEW), + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + user_settings = UserSettings.objects.get(user=self.add_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.triage_user = User.objects.create_user(username="test_user_triage", password="password") + teamuser = TeamUsers.objects.create( + team = triage_team, + user = self.triage_user + ) + + + self.import_user = User.objects.create_user(username="test_user_import", password="password") + teamuser = TeamUsers.objects.create( + team = import_team, + user = self.import_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + + + def test_add_triage_user_denied(self): + """ Check correct permission for add + + Attempt to add as triage user + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.triage_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 403 + + + + def test_add_has_permission_import_user(self): + """ Check correct permission for add + + Attempt to add as import user who should have permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.import_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 201 + + + + def test_change_has_permission_triage_user(self): + """ Check correct permission for change + + Make change with triage user who has change permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.triage_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 200 + + + + def test_change_import_user_denied(self): + """ Ensure permission view cant make change + + Attempt to make change as import user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.import_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 403 + + + + def test_delete_permission_triage_denied(self): + """ Check correct permission for delete + + Attempt to delete as triage user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.triage_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 + + + + def test_delete_permission_import_denied(self): + """ Check correct permission for delete + + Attempt to delete as import user + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.import_user) + response = client.delete(url, data=self.delete_data) + + assert response.status_code == 403 From 846eb79c6e1dfe9ff96970690a8c3c00ce57f461 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 16:54:15 +0930 Subject: [PATCH 381/617] test(assistance): Request Ticket API v2 ViewSet permission checks ref: #15 #248 #365 --- app/assistance/serializers/request.py | 1 - .../ticket_request/test_ticket_request_viewset.py | 13 +++++++++++++ app/core/serializers/ticket.py | 11 ++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 app/assistance/tests/unit/ticket_request/test_ticket_request_viewset.py diff --git a/app/assistance/serializers/request.py b/app/assistance/serializers/request.py index 6e79bdf50..9c89ec27d 100644 --- a/app/assistance/serializers/request.py +++ b/app/assistance/serializers/request.py @@ -107,7 +107,6 @@ class Meta(RequestTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', 'project', 'milestone', 'subscribed_teams', diff --git a/app/assistance/tests/unit/ticket_request/test_ticket_request_viewset.py b/app/assistance/tests/unit/ticket_request/test_ticket_request_viewset.py new file mode 100644 index 000000000..42e987ac1 --- /dev/null +++ b/app/assistance/tests/unit/ticket_request/test_ticket_request_viewset.py @@ -0,0 +1,13 @@ +from django.test import TestCase + +from core.tests.abstract.test_ticket_viewset import Ticket, TicketViewSetPermissionsAPI + + +class TicketRequestPermissionsAPI( + TicketViewSetPermissionsAPI, + TestCase, +): + + ticket_type = 'request' + + ticket_type_enum = Ticket.TicketType.REQUEST diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 6ae308bcb..d7d28710d 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -145,7 +145,7 @@ def is_valid(self, *, raise_exception=False): try: - self.validated_data['ticket_type'] = self._context['view'].ticket_type_id + self.validated_data['ticket_type'] = self._context['view']._ticket_type_id except: @@ -154,6 +154,15 @@ def is_valid(self, *, raise_exception=False): raise UnknownTicketType() + if 'view' in self._context: + + if self._context['view'].action == 'create': + + if hasattr(self._context['view'], 'request'): + + self.validated_data['opened_by_id'] = self._context['view'].request.user.id + + return is_valid From c06d09f507d886a54d49299c2f94f95da74754c6 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 16:58:40 +0930 Subject: [PATCH 382/617] test(core): Ticket Category API v2 ViewSet permission checks ref: #15 #248 #365 --- .../test_ticket_category_viewset.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 app/core/tests/unit/ticket_category/test_ticket_category_viewset.py diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_viewset.py b/app/core/tests/unit/ticket_category/test_ticket_category_viewset.py new file mode 100644 index 000000000..cd7932efa --- /dev/null +++ b/app/core/tests/unit/ticket_category/test_ticket_category_viewset.py @@ -0,0 +1,177 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.ticket.ticket_category import TicketCategory + + + +class TicketCategoryPermissionsAPI(TestCase, APIPermissions): + + model = TicketCategory + + app_namespace = 'v2' + + url_name = '_api_v2_ticket_category' + + change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team_post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 7869ff4478ac8871f8a9fe18ed2fc1525a9e5e8f Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:00:59 +0930 Subject: [PATCH 383/617] test(core): Ticket Comment Category API v2 ViewSet permission checks ref: #15 #248 #365 --- .../test_ticket_comment_category_viewset.py | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_viewset.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_viewset.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_viewset.py new file mode 100644 index 000000000..6e994aad7 --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_viewset.py @@ -0,0 +1,177 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.ticket.ticket_comment_category import TicketCommentCategory + + + +class TicketCommentCategoryPermissionsAPI(TestCase, APIPermissions): + + model = TicketCommentCategory + + app_namespace = 'v2' + + url_name = '_api_v2_ticket_comment_category' + + change_data = {'name': 'device'} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.item = self.model.objects.create( + organization = self.organization, + name = 'one' + ) + + + self.url_view_kwargs = {'pk': self.item.id} + + self.add_data = { + 'name': 'team_post', + 'organization': self.organization.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 95f9a2620fcd9fbfeded428622bef3527a531d73 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:34:12 +0930 Subject: [PATCH 384/617] test(core): Ticket Comment API v2 ViewSet permission checks ref: #15 #248 #365 --- app/core/serializers/ticket_comment.py | 20 +- .../test_ticket_comment_viewset.py | 229 ++++++++++++++++++ 2 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py index c4e3d0d7f..4f1c7e74d 100644 --- a/app/core/serializers/ticket_comment.py +++ b/app/core/serializers/ticket_comment.py @@ -175,10 +175,6 @@ def __init__(self, instance=None, data=empty, **kwargs): if 'ticket_id' in self._kwargs['context']['view'].kwargs: - ticket = Ticket.objects.get(pk=int(self._kwargs['context']['view'].kwargs['ticket_id'])) - self.fields.fields['organization'].initial = ticket.organization.id - - self.fields.fields['ticket'].initial = ticket.id self.fields.fields['comment_type'].initial = TicketComment.CommentType.COMMENT @@ -187,6 +183,22 @@ def __init__(self, instance=None, data=empty, **kwargs): super().__init__(instance=instance, data=data, **kwargs) + def is_valid(self, *, raise_exception=False): + + is_valid: bool = False + + is_valid = super().is_valid(raise_exception=raise_exception) + + + if 'view' in self._context: + + if self._context['view'].action == 'create': + + self.validated_data['ticket_id'] = int(self._kwargs['context']['view'].kwargs['ticket_id']) + + return is_valid + + class TicketCommentITILModelSerializer(TicketCommentModelSerializer): """ITIL Comment Model Base diff --git a/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py b/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py new file mode 100644 index 000000000..194097db1 --- /dev/null +++ b/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py @@ -0,0 +1,229 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import APIPermissions + +from core.models.ticket.ticket_comment import Ticket, TicketComment + +from settings.models.user_settings import UserSettings + + + +class TicketCommentPermissionsAPI( + APIPermissions, + TestCase +): + """ Test Cases common to ALL ticket types """ + + model = TicketComment + + app_namespace = 'v2' + + change_data = {'body': 'it has changed'} + + delete_data = {} + + ticket_type: str = 'request' + + ticket_type_enum = Ticket.TicketType.REQUEST + + url_name = '_api_v2_ticket_comments' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + # import_permissions = Permission.objects.get( + # codename = 'import_' + self.model._meta.model_name, + # content_type = ContentType.objects.get( + # app_label = self.model._meta.app_label, + # model = self.model._meta.model_name, + # ) + # ) + + # import_team = Team.objects.create( + # team_name = 'import_team', + # organization = organization, + # ) + + # import_team.permissions.set([import_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.ticket = Ticket.objects.create( + organization = self.organization, + title = 'one', + description = 'some text for body', + opened_by = self.view_user, + ticket_type = self.ticket_type_enum, + status = Ticket.TicketStatus.All.NEW + ) + + self.item = self.model.objects.create( + organization = self.organization, + body = 'comment', + ticket = self.ticket, + ) + + + self.url_kwargs = {'ticket_id': self.ticket.id} + + self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + + self.add_data = { + 'organization': self.organization.id, + 'body': 'comment body', + 'ticket': self.ticket.id, + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + user_settings = UserSettings.objects.get(user=self.add_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + # self.import_user = User.objects.create_user(username="test_user_import", password="password") + # teamuser = TeamUsers.objects.create( + # team = import_team, + # user = self.import_user + # ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From fffe78a4edb70480bf737636956ab8789f34713f Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:35:14 +0930 Subject: [PATCH 385/617] fix(core): Add missing ticket comment category url ref: #248 #365 --- app/api/urls_v2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index b373132ff..8e37ec682 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -42,6 +42,7 @@ notes as notes_v2, ticket_category, ticket_comment, + ticket_comment_category, ticket_linked_item, related_ticket, @@ -167,6 +168,7 @@ router.register('settings/project_type', project_type_v2.ViewSet, basename='_api_v2_project_type') router.register('settings/software_category', software_category_v2.ViewSet, basename='_api_v2_software_category') router.register('settings/ticket_category', ticket_category.ViewSet, basename='_api_v2_ticket_category') +router.register('settings/ticket_comment_category', ticket_comment_category.ViewSet, basename='_api_v2_ticket_comment_category') router.register('settings/user_settings', user_settings_v2.ViewSet, basename='_api_v2_user_settings') From d4aa3e673fec843bb6e6c19b00272c8d62632c6c Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:54:07 +0930 Subject: [PATCH 386/617] fix(config_management): Correct ticket url in group serializer ref: #248 #365 --- app/config_management/serializers/config_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 953173fdd..529501d48 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -104,7 +104,7 @@ def get_url(self, item): "v2:_api_v2_item_tickets-list", request=self._context['view'].request, kwargs={ - 'item_class': 'cluster', + 'item_class': 'config_group', 'item_id': item.pk } ), From 31af109742b9d113cd59d3e16fc12542b14737b5 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:55:18 +0930 Subject: [PATCH 387/617] test(config_management): Group Ticket URL API field checks ref: #15 #248 #365 --- .../test_config_group_item_ticket_api_v2.py | 2 +- .../test_config_groups_api_v2.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py b/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py index a74f4890d..abbc7cb0a 100644 --- a/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py +++ b/app/config_management/tests/unit/config_groups/test_config_group_item_ticket_api_v2.py @@ -10,7 +10,7 @@ -class ServiceItemTicketAPI( +class ConfigGroupsTicketAPI( ItemTicketAPI, TestCase, ): diff --git a/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py b/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py index 56066b00b..99383cfb9 100644 --- a/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py +++ b/app/config_management/tests/unit/config_groups/test_config_groups_api_v2.py @@ -170,3 +170,22 @@ def test_api_field_type_parent_url(self): """ assert type(self.api_data['parent']['url']) is str + + + + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str From f1332cecf47b49295a876c2566bbca56c4bad733 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:55:46 +0930 Subject: [PATCH 388/617] test(itam): Device Ticket URL API field checks ref: #15 #248 #365 --- .../tests/unit/device/test_device_api_v2.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/itam/tests/unit/device/test_device_api_v2.py b/app/itam/tests/unit/device/test_device_api_v2.py index dc1f1fbf9..562e649c2 100644 --- a/app/itam/tests/unit/device/test_device_api_v2.py +++ b/app/itam/tests/unit/device/test_device_api_v2.py @@ -566,19 +566,19 @@ def test_api_field_type_urls_software(self): - # def test_api_field_exists_urls_tickets(self): - # """ Test for existance of API Field + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field - # _urls.tickets field must exist - # """ + _urls.tickets field must exist + """ - # assert 'tickets' in self.api_data['_urls'] + assert 'tickets' in self.api_data['_urls'] - # def test_api_field_type_urls_tickets(self): - # """ Test for type for API Field + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field - # _urls.tickets field must be str - # """ + _urls.tickets field must be str + """ - # assert type(self.api_data['_urls']['tickets']) is str + assert type(self.api_data['_urls']['tickets']) is str From da414d741f4421cc5fa7e4454b93aa873b0e5f48 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:56:23 +0930 Subject: [PATCH 389/617] test(itam): Operating System Ticket URL API field checks ref: #15 #248 #365 --- .../test_operating_system_api_v2.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py b/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py index f7bcc6eea..628a9d9a9 100644 --- a/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py +++ b/app/itam/tests/unit/operating_system/test_operating_system_api_v2.py @@ -154,3 +154,22 @@ def test_api_field_type_publisher_url(self): assert type(self.api_data['publisher']['url']) is Hyperlink + + + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str + From 0b37f8f2b332ef17781e8ce2796149ea8e4643ce Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:56:40 +0930 Subject: [PATCH 390/617] test(itam): Software Ticket URL API field checks ref: #15 #248 #365 --- .../unit/software/test_software_api_v2.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/itam/tests/unit/software/test_software_api_v2.py b/app/itam/tests/unit/software/test_software_api_v2.py index 589349459..2213421fc 100644 --- a/app/itam/tests/unit/software/test_software_api_v2.py +++ b/app/itam/tests/unit/software/test_software_api_v2.py @@ -311,6 +311,25 @@ def test_api_field_type_urls_notes(self): + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str + + + def test_api_field_exists_urls_tickets(self): """ Test for existance of API Field From e220303d06267a231f528dc39fd49c3ad124976a Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:56:58 +0930 Subject: [PATCH 391/617] test(itim): Cluster Ticket URL API field checks ref: #15 #248 #365 --- .../tests/unit/cluster/test_cluster_api_v2.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/itim/tests/unit/cluster/test_cluster_api_v2.py b/app/itim/tests/unit/cluster/test_cluster_api_v2.py index 237c1715c..66dfb3ca0 100644 --- a/app/itim/tests/unit/cluster/test_cluster_api_v2.py +++ b/app/itim/tests/unit/cluster/test_cluster_api_v2.py @@ -448,3 +448,22 @@ def test_api_field_type_parent_cluster_url(self): """ assert type(self.api_data['parent_cluster']['url']) is Hyperlink + + + + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str From b972ea1f97a6c88c73544bd38421702366ce1e18 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 17:57:17 +0930 Subject: [PATCH 392/617] test(itim): Service Ticket URL API field checks ref: #15 #248 #365 --- .../tests/unit/service/test_service_api_v2.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/itim/tests/unit/service/test_service_api_v2.py b/app/itim/tests/unit/service/test_service_api_v2.py index f8fabbad5..35997ffae 100644 --- a/app/itim/tests/unit/service/test_service_api_v2.py +++ b/app/itim/tests/unit/service/test_service_api_v2.py @@ -569,3 +569,22 @@ def test_api_field_type_cluster_url(self): """ assert type(self.api_data_two['cluster']['url']) is Hyperlink + + + + def test_api_field_exists_urls_tickets(self): + """ Test for existance of API Field + + _urls.tickets field must exist + """ + + assert 'tickets' in self.api_data['_urls'] + + + def test_api_field_type_urls_tickets(self): + """ Test for type for API Field + + _urls.tickets field must be str + """ + + assert type(self.api_data['_urls']['tickets']) is str From 863b2d46c60a8dde2d50393047a4a7205b0a54c0 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 18:48:29 +0930 Subject: [PATCH 393/617] fix(core): Correct serializer item field to be for view serializer ONLY ref: #248 #365 --- app/core/serializers/ticket_linked_item.py | 122 ++++++++++----------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/app/core/serializers/ticket_linked_item.py b/app/core/serializers/ticket_linked_item.py index ddc8540f4..4461653b9 100644 --- a/app/core/serializers/ticket_linked_item.py +++ b/app/core/serializers/ticket_linked_item.py @@ -51,6 +51,67 @@ class Meta: class TicketLinkedItemModelSerializer(TicketLinkedItemBaseSerializer): + + _urls = serializers.SerializerMethodField('get_url') + + def get_url(self, item): + + return { + '_self': item.get_url( request = self._context['view'].request ) + } + + + class Meta: + + model = TicketLinkedItem + + fields = [ + 'id', + 'display_name', + 'item', + 'item_type', + 'ticket', + 'organization', + 'created', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + # 'item', + # 'item_type', + 'ticket', + 'organization', + 'created', + '_urls', + ] + + + + def is_valid(self, *, raise_exception=False): + + is_valid = super().is_valid( raise_exception = raise_exception ) + + + if 'view' in self._context: + + ticket = Ticket.objects.get(pk = int(self._context['view'].kwargs['ticket_id']) ) + + self.validated_data['ticket'] = ticket + + self.validated_data['organization_id'] = ticket.organization.id + + + return is_valid + + + +class TicketLinkedItemViewSerializer(TicketLinkedItemModelSerializer): + + + organization = OrganizationBaseSerializer(many=False, read_only=True) + item = serializers.SerializerMethodField('get_item') def get_item(self, item) -> dict: @@ -129,65 +190,4 @@ def get_item(self, item) -> dict: context=self._context ).data - - _urls = serializers.SerializerMethodField('get_url') - - def get_url(self, item): - - return { - '_self': item.get_url( request = self._context['view'].request ) - } - - - class Meta: - - model = TicketLinkedItem - - fields = [ - 'id', - 'display_name', - 'item', - 'item_type', - 'ticket', - 'organization', - 'created', - '_urls', - ] - - read_only_fields = [ - 'id', - 'display_name', - 'item', - 'item_type', - 'ticket', - 'organization', - 'created', - '_urls', - ] - - - - def is_valid(self, *, raise_exception=False): - - is_valid = super().is_valid( raise_exception = raise_exception ) - - - if 'view' in self._context: - - ticket = Ticket.objects.get(pk = int(self._context['view'].kwargs['ticket_id']) ) - - self.validated_data['ticket'] = ticket - - self.validated_data['organization_id'] = ticket.organization.id - - - return is_valid - - - -class TicketLinkedItemViewSerializer(TicketLinkedItemModelSerializer): - - - organization = OrganizationBaseSerializer(many=False, read_only=True) - ticket = TicketBaseSerializer(read_only = True) From 6b6b70d65342e2150d962ecb8c11cd3bd8426e2c Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 28 Oct 2024 18:49:31 +0930 Subject: [PATCH 394/617] test(itim): Ticket Linked Item API field checks ref: #15 #248 #365 --- .../test_ticket_linked_item_viewset.py | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py diff --git a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py new file mode 100644 index 000000000..ec65034c5 --- /dev/null +++ b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py @@ -0,0 +1,225 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import ( + APIPermissionAdd, + APIPermissionChange, + APIPermissionDelete, + APIPermissionView +) + +from core.models.ticket.ticket_linked_items import Ticket, TicketLinkedItem + +from itam.models.device import Device + +from settings.models.user_settings import UserSettings + + + +class TicketLinkedItemPermissionsAPI( + APIPermissionAdd, + APIPermissionDelete, + APIPermissionView, + TestCase +): + """ Test Cases common to ALL ticket types """ + + model = TicketLinkedItem + + app_namespace = 'v2' + + delete_data = {} + + ticket_type: str = 'request' + + ticket_type_enum = Ticket.TicketType.REQUEST + + url_name = '_api_v2_ticket_linked_item' + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + + self.ticket = Ticket.objects.create( + organization = self.organization, + title = 'one', + description = 'some text for body', + opened_by = self.view_user, + ticket_type = self.ticket_type_enum, + status = Ticket.TicketStatus.All.NEW + ) + + self.device = Device.objects.create( + organization = self.organization, + name = 'one', + ) + + self.device_two = Device.objects.create( + organization = self.organization, + name = 'two', + ) + + self.item = self.model.objects.create( + organization = self.organization, + item = self.device.id, + item_type = TicketLinkedItem.Modules.DEVICE, + ticket = self.ticket, + ) + + + self.url_kwargs = {'ticket_id': self.ticket.id} + + self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + + self.add_data = { + 'organization': self.organization.id, + 'ticket': self.ticket.id, + 'item': self.device_two.id, + 'item_type': int(TicketLinkedItem.Modules.DEVICE), + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + user_settings = UserSettings.objects.get(user=self.add_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) From 4c927efeef1214587b7de3dea619235d050aaefa Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 17:11:54 +0930 Subject: [PATCH 395/617] test(core): Ticket Category API v2 Serializer checks ref: #15 #248 #373 --- .../test_ticket_category_serializer.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 app/core/tests/unit/ticket_category/test_ticket_category_serializer.py diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py b/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py new file mode 100644 index 000000000..f95c8797e --- /dev/null +++ b/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py @@ -0,0 +1,86 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.ticket_category import TicketCategory, TicketCategoryModelSerializer + + + +class TicketCategoryValidationAPI( + TestCase, +): + + model = TicketCategory + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'random title', + ) + + + + def test_serializer_validation_add_valid_item(self): + """Serializer Validation Check + + Ensure that a valid item it does not raise a validation error + """ + + serializer = TicketCategoryModelSerializer( + data={ + "organization": self.organization.id, + "name": 'new category' + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = TicketCategoryModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_add_existing_allowed(self): + """Serializer Validation Check + + Ensure that if adding the same item it raises a validation error + """ + + serializer = TicketCategoryModelSerializer( + data={ + "organization": self.organization.id, + "name": self.item.name + } + ) + + assert serializer.is_valid(raise_exception = True) From fdd50c3208ba9593ba2b04b1a65a17daf7caede9 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 18:05:23 +0930 Subject: [PATCH 396/617] feat(core): Ensure that ticket category cant assign self as parent ref: #15 #248 #373 --- app/core/serializers/ticket_category.py | 37 +++++++++++++++++++ .../test_ticket_category_serializer.py | 23 ++++++++++++ 2 files changed, 60 insertions(+) diff --git a/app/core/serializers/ticket_category.py b/app/core/serializers/ticket_category.py index d4f1a0a16..7335a9f47 100644 --- a/app/core/serializers/ticket_category.py +++ b/app/core/serializers/ticket_category.py @@ -15,6 +15,7 @@ def get_display_name(self, item): return str( item ) + url = serializers.HyperlinkedIdentityField( view_name="v2:_api_v2_ticket_category-detail", format="html" ) @@ -65,6 +66,42 @@ class Meta: ] + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + if 'view' in self._context: + + if( + self._context['view'].action == 'create' + or self._context['view'].action == 'list' + ): + + self.fields['parent'].queryset = self.fields['parent'].queryset.exclude( + id=self.instance.pk + ) + + + def validate(self, attrs) -> bool: + + attrs = super().validate(attrs = attrs) + + if self.instance: + + if 'parent' in attrs: + + if int(attrs['parent'].id) == self.instance.pk: + + raise serializers.ValidationError( + detail = { + 'parent': 'Cant set self as parent category' + }, + code = 'parent_not_self' + ) + + return attrs + + class TicketCategoryViewSerializer(TicketCategoryModelSerializer): diff --git a/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py b/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py index f95c8797e..d7b2602e9 100644 --- a/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py +++ b/app/core/tests/unit/ticket_category/test_ticket_category_serializer.py @@ -52,6 +52,29 @@ def test_serializer_validation_add_valid_item(self): + def test_serializer_validation_self_not_parent(self): + """Serializer Validation Check + + Ensure that a validation error is raised if an attempt to add + self as parent category. + """ + + with pytest.raises(ValidationError) as err: + + serializer = TicketCategoryModelSerializer( + self.item, + data={ + "parent": self.item.id, + }, + partial = True + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['parent'][0] == 'parent_not_self' + + + def test_serializer_validation_no_name(self): """Serializer Validation Check From 0404b52924ea5586ddd2d48863df53ac507745f3 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 18:39:49 +0930 Subject: [PATCH 397/617] test(core): Ticket Comment Category API v2 Serializer checks ref: #15 #248 #373 --- ...test_ticket_comment_category_serializer.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_serializer.py diff --git a/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_serializer.py b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_serializer.py new file mode 100644 index 000000000..2dc9d5eaf --- /dev/null +++ b/app/core/tests/unit/ticket_comment_category/test_ticket_comment_category_serializer.py @@ -0,0 +1,109 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.ticket_comment_category import TicketCommentCategory, TicketCommentCategoryModelSerializer + + + +class TicketCommentCategoryValidationAPI( + TestCase, +): + + model = TicketCommentCategory + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.item = self.model.objects.create( + organization=organization, + name = 'random title', + ) + + + + def test_serializer_validation_add_valid_item(self): + """Serializer Validation Check + + Ensure that a valid item it does not raise a validation error + """ + + serializer = TicketCommentCategoryModelSerializer( + data={ + "organization": self.organization.id, + "name": 'new category' + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_self_not_parent(self): + """Serializer Validation Check + + Ensure that a validation error is raised if an attempt to add + self as parent category. + """ + + with pytest.raises(ValidationError) as err: + + serializer = TicketCommentCategoryModelSerializer( + self.item, + data={ + "parent": self.item.id, + }, + partial = True + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['parent'][0] == 'parent_not_self' + + + + def test_serializer_validation_no_name(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + with pytest.raises(ValidationError) as err: + + serializer = TicketCommentCategoryModelSerializer(data={ + "organization": self.organization.id, + }) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + def test_serializer_validation_add_existing_allowed(self): + """Serializer Validation Check + + Ensure that if adding the same item it raises a validation error + """ + + serializer = TicketCommentCategoryModelSerializer( + data={ + "organization": self.organization.id, + "name": self.item.name + } + ) + + assert serializer.is_valid(raise_exception = True) From 14776a0334705629bf4ec8a1e511940e8fd954e9 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 18:40:00 +0930 Subject: [PATCH 398/617] feat(core): Ensure that ticket comment category cant assign self as parent ref: #15 #248 #373 --- app/core/serializers/ticket_category.py | 6 ++-- .../serializers/ticket_comment_category.py | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/core/serializers/ticket_category.py b/app/core/serializers/ticket_category.py index 7335a9f47..ccc93b069 100644 --- a/app/core/serializers/ticket_category.py +++ b/app/core/serializers/ticket_category.py @@ -73,8 +73,8 @@ def __init__(self, *args, **kwargs): if 'view' in self._context: if( - self._context['view'].action == 'create' - or self._context['view'].action == 'list' + self._context['view'].action == 'partial_update' + or self._context['view'].action == 'update' ): self.fields['parent'].queryset = self.fields['parent'].queryset.exclude( @@ -84,8 +84,6 @@ def __init__(self, *args, **kwargs): def validate(self, attrs) -> bool: - attrs = super().validate(attrs = attrs) - if self.instance: if 'parent' in attrs: diff --git a/app/core/serializers/ticket_comment_category.py b/app/core/serializers/ticket_comment_category.py index aab0644a7..33a950e51 100644 --- a/app/core/serializers/ticket_comment_category.py +++ b/app/core/serializers/ticket_comment_category.py @@ -66,6 +66,7 @@ class Meta: 'id', 'organization', 'display_name', + 'parent', 'name', 'model_notes', 'is_global', @@ -83,6 +84,40 @@ class Meta: ] + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + if 'view' in self._context: + + if( + self._context['view'].action == 'partial_update' + or self._context['view'].action == 'update' + ): + + self.fields['parent'].queryset = self.fields['parent'].queryset.exclude( + id=self.instance.pk + ) + + + def validate(self, attrs): + + if self.instance: + + if 'parent' in attrs: + + if int(attrs['parent'].id) == self.instance.pk: + + raise serializers.ValidationError( + detail = { + 'parent': 'Cant set self as parent category' + }, + code = 'parent_not_self' + ) + + return attrs + + class TicketCommentCategoryViewSerializer(TicketCommentCategoryModelSerializer): From 7b70fd30b37683cf7c55199ad350ebb84d348827 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 19:08:37 +0930 Subject: [PATCH 399/617] feat(core): Ensure that ticket linked item validates if ticket supplied ref: #15 #248 #373 --- app/core/serializers/ticket_linked_item.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/core/serializers/ticket_linked_item.py b/app/core/serializers/ticket_linked_item.py index 4461653b9..434574f7d 100644 --- a/app/core/serializers/ticket_linked_item.py +++ b/app/core/serializers/ticket_linked_item.py @@ -79,8 +79,6 @@ class Meta: read_only_fields = [ 'id', 'display_name', - # 'item', - # 'item_type', 'ticket', 'organization', 'created', @@ -102,6 +100,15 @@ def is_valid(self, *, raise_exception=False): self.validated_data['organization_id'] = ticket.organization.id + else: + + raise serializers.ValidationError( + detail = { + 'ticket': 'Ticket is required' + }, + code = 'required' + ) + return is_valid From 32e3a97b09d9dccae7f814a7373ef3ce394cdb9d Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 19:08:53 +0930 Subject: [PATCH 400/617] test(core): Ticket Linked Item API v2 Serializer checks ref: #15 #248 #373 --- .../test_ticket_linked_item_serializer.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py diff --git a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py new file mode 100644 index 000000000..871347763 --- /dev/null +++ b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py @@ -0,0 +1,137 @@ +import pytest + +from django.test import TestCase +from django.contrib.auth.models import User + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.ticket_linked_item import Ticket, TicketLinkedItem, TicketLinkedItemModelSerializer + +from itam.models.device import Device + + +class TicketLinkedItemValidationAPI( + TestCase, +): + + model = TicketLinkedItem + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.user = User.objects.create( + username = 'user', + password = 'password' + ) + + self.ticket = Ticket.objects.create( + organization=organization, + title = 'ticket title', + description = 'some text', + opened_by = self.user, + status = Ticket.TicketStatus.All.NEW, + ticket_type = Ticket.TicketType.REQUEST, + ) + + self.device = Device.objects.create( + organization=organization, + name = 'item', + ) + + self.device_two = Device.objects.create( + organization=organization, + name = 'item-two', + ) + + self.item = self.model.objects.create( + organization=organization, + ticket = self.ticket, + item = self.device.id, + item_type = TicketLinkedItem.Modules.DEVICE + ) + + + + def test_serializer_validation_add_valid_item(self): + """Serializer Validation Check + + Ensure that a valid item it does not raise a validation error + """ + + class MockView: + + kwargs: dict = { + 'ticket_id': int(self.ticket.id) + } + + + serializer = TicketLinkedItemModelSerializer( + context = { + 'view': MockView + }, + data={ + "organization": self.organization.id, + "ticket": self.ticket.id, + "item_type": int(TicketLinkedItem.Modules.DEVICE), + "item": self.device_two.id, + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_no_ticket(self): + """Serializer Validation Check + + Ensure that a validation error is raised if no ticket specified. + """ + + with pytest.raises(ValidationError) as err: + + serializer = TicketLinkedItemModelSerializer( + data={ + "organization": self.organization.id, + # "ticket": self.ticket.id, + "item_type": int(TicketLinkedItem.Modules.DEVICE), + "item": self.device_two.id, + } + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['ticket'] == 'required' + + + + def test_serializer_validation_no_item(self): + """Serializer Validation Check + + Ensure that a validation error is raised if no ticket specified. + """ + + with pytest.raises(ValidationError) as err: + + serializer = TicketLinkedItemModelSerializer( + data={ + "organization": self.organization.id, + "ticket": self.ticket.id, + "item_type": int(TicketLinkedItem.Modules.DEVICE), + # "item": self.device_two.id, + } + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['item'][0] == 'required' From fe5aac02187e712da9855b84fa0f3e125c9a9041 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 21:35:25 +0930 Subject: [PATCH 401/617] feat(core): Ensure ticket comment Serializer is picked based off of comment_type ref: #248 #373 --- .../test_ticket_comment_viewset.py | 1 + app/core/viewsets/ticket_comment.py | 34 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py b/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py index 194097db1..6ebda029e 100644 --- a/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py +++ b/app/core/tests/unit/ticket_comment/test_ticket_comment_viewset.py @@ -171,6 +171,7 @@ def setUpTestData(self): 'organization': self.organization.id, 'body': 'comment body', 'ticket': self.ticket.id, + 'comment_type': int(TicketComment.CommentType.COMMENT) } diff --git a/app/core/viewsets/ticket_comment.py b/app/core/viewsets/ticket_comment.py index aa5afa0cc..7c9e83ae5 100644 --- a/app/core/viewsets/ticket_comment.py +++ b/app/core/viewsets/ticket_comment.py @@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, PolymorphicProxySerializer -from rest_framework import generics, viewsets +from rest_framework import generics, viewsets, exceptions from rest_framework.response import Response from access.mixin import OrganizationMixin @@ -140,6 +140,18 @@ def get_serializer_class(self): serializer_prefix:str = 'TicketComment' if ( + 'comment_type' not in self.request.data + and self.action == 'create' + ): + + raise exceptions.ValidationError( + detail = { + 'comment_type': 'comment type is required' + }, + code = 'required' + ) + + elif ( self.action == 'create' ): @@ -158,6 +170,26 @@ def get_serializer_class(self): serializer_prefix = serializer_prefix + 'Import' + elif int(self.request.data['comment_type']) == int(TicketComment.CommentType.COMMENT): + + serializer_prefix = serializer_prefix + 'ITILFollowUp' + + elif int(self.request.data['comment_type']) == int(TicketComment.CommentType.SOLUTION): + + serializer_prefix = serializer_prefix + 'ITILFollowUp' + + elif int(self.request.data['comment_type']) == int(TicketComment.CommentType.TASK): + + serializer_prefix = serializer_prefix + 'ITILFollowUp' + + else: + + raise exceptions.ValidationError( + detail = 'Unable to determine the serializer', + code = 'serializer_unknwon' + ) + + if ( self.action == 'list' or self.action == 'retrieve' From a75a56eb966e105b215e3160f692d16bbc80d6c7 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 21:41:32 +0930 Subject: [PATCH 402/617] feat(core): Ensure ticket comment Serializer validates for existance of comment_type and ticket id ref: #248 #373 --- app/core/serializers/ticket_comment.py | 73 +++++++++++++++++++++----- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py index 4f1c7e74d..8e5867580 100644 --- a/app/core/serializers/ticket_comment.py +++ b/app/core/serializers/ticket_comment.py @@ -140,10 +140,8 @@ class Meta: read_only_fields = [ 'id', 'parent', - 'ticket', 'external_ref', 'external_system', - 'comment_type', 'private', 'duration', 'category', @@ -169,18 +167,58 @@ class Meta: def __init__(self, instance=None, data=empty, **kwargs): - if 'context' in self._kwargs: + super().__init__(instance=instance, data=data, **kwargs) - if 'view' in self._kwargs['context']: + if 'context' in kwargs: - if 'ticket_id' in self._kwargs['context']['view'].kwargs: + if 'view' in kwargs['context']: + if kwargs['context']['view'].action == 'create': - self.fields.fields['comment_type'].initial = TicketComment.CommentType.COMMENT - self.fields.fields['user'].initial = kwargs['context']['request']._user.id + if 'request' in kwargs['context']['view'].kwargs: + + self.fields.fields['user'].initial = kwargs['context']['request']._user.id + + + + + def validate(self, attrs): + + if( + ( + 'comment_type' not in attrs + or attrs['comment_type'] is None + ) + and self._context['view'].action == 'create' + ): + + raise serializers.ValidationError( + detail = { + 'comment_type': 'Comment Type is required' + }, + code = 'required' + ) + + elif ( + 'comment_type' in attrs + and ( + self._context['view'].action == 'partial_update' + or self._context['view'].action == 'update' + ) + ): + + raise serializers.ValidationError( + detail = { + 'comment_type': 'Comment Type is not editable' + }, + code = 'read_only' + ) + + + return attrs + - super().__init__(instance=instance, data=data, **kwargs) def is_valid(self, *, raise_exception=False): @@ -194,7 +232,20 @@ def is_valid(self, *, raise_exception=False): if self._context['view'].action == 'create': - self.validated_data['ticket_id'] = int(self._kwargs['context']['view'].kwargs['ticket_id']) + if 'ticket_id' in self._kwargs['context']['view'].kwargs: + + self.validated_data['ticket_id'] = int(self._kwargs['context']['view'].kwargs['ticket_id']) + + else: + + raise serializers.ValidationError( + detail = { + 'ticket': 'Ticket is a required field' + }, + code = 'required' + ) + + return is_valid @@ -246,7 +297,6 @@ class Meta(TicketCommentModelSerializer.Meta): 'ticket', 'external_ref', 'external_system', - 'comment_type', 'body', 'private', 'duration', @@ -286,7 +336,6 @@ class Meta(TicketCommentITILModelSerializer.Meta): 'ticket', 'external_ref', 'external_system', - 'comment_type', # 'body', 'private', 'duration', @@ -326,7 +375,6 @@ class Meta(TicketCommentITILModelSerializer.Meta): 'ticket', 'external_ref', 'external_system', - 'comment_type', # 'body', 'private', 'duration', @@ -366,7 +414,6 @@ class Meta(TicketCommentITILModelSerializer.Meta): 'ticket', 'external_ref', 'external_system', - 'comment_type', 'private', 'duration', 'is_template', From 821ba0edbff53014dc31698bd84acc403a83148a Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 22:30:00 +0930 Subject: [PATCH 403/617] feat(core): Add custom exception class ref: #248 #373 --- app/core/exceptions.py | 17 ++++++++++++++++- app/core/serializers/ticket_comment.py | 7 ++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/core/exceptions.py b/app/core/exceptions.py index 68d074357..4d2b4f73b 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,5 +1,20 @@ +from rest_framework import exceptions, status +from rest_framework.exceptions import ( + ValidationError, + PermissionDenied +) class MissingAttribute(Exception): """ An attribute is missing""" - pass \ No newline at end of file + pass + +class APIError( + exceptions.APIException +): + + status_code = status.HTTP_400_BAD_REQUEST + + default_detail = 'An unknown ERROR occured' + + default_code = 'unknown_error' diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py index 8e5867580..317d524aa 100644 --- a/app/core/serializers/ticket_comment.py +++ b/app/core/serializers/ticket_comment.py @@ -10,6 +10,7 @@ from app.serializers.user import UserBaseSerializer +from core import exceptions as centurion_exceptions from core.models.ticket.ticket_comment import Ticket, TicketComment @@ -193,7 +194,7 @@ def validate(self, attrs): and self._context['view'].action == 'create' ): - raise serializers.ValidationError( + raise centurion_exceptions.ValidationError( detail = { 'comment_type': 'Comment Type is required' }, @@ -208,7 +209,7 @@ def validate(self, attrs): ) ): - raise serializers.ValidationError( + raise centurion_exceptions.ValidationError( detail = { 'comment_type': 'Comment Type is not editable' }, @@ -238,7 +239,7 @@ def is_valid(self, *, raise_exception=False): else: - raise serializers.ValidationError( + raise centurion_exceptions.ValidationError( detail = { 'ticket': 'Ticket is a required field' }, From 8479f8c30bda5619eb89a5324963af3b6bd5b259 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 31 Oct 2024 23:56:08 +0930 Subject: [PATCH 404/617] feat(core): Determine serializer from action and user permissions for Ticket Comments ref: #248 #373 --- app/core/serializers/ticket_comment.py | 212 ++++++++++++++++++++++--- app/core/viewsets/ticket_comment.py | 123 ++++++++++---- 2 files changed, 282 insertions(+), 53 deletions(-) diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py index 317d524aa..11a9d6d98 100644 --- a/app/core/serializers/ticket_comment.py +++ b/app/core/serializers/ticket_comment.py @@ -3,7 +3,7 @@ from rest_framework import serializers from rest_framework.fields import empty -from access.serializers.organization import OrganizationBaseSerializer +from access.serializers.organization import Organization, OrganizationBaseSerializer from access.serializers.teams import TeamBaseSerializer from api.exceptions import UnknownTicketType @@ -47,7 +47,9 @@ class Meta: -class TicketCommentModelSerializer(TicketCommentBaseSerializer): +class TicketCommentModelSerializer( + TicketCommentBaseSerializer, +): """Base class for Ticket Comment Model Args: @@ -164,14 +166,23 @@ class Meta: '_urls', ] + is_triage: bool = False + """ If the serializers is a Triage serializer""" + + request = None + """ HTTP Request that wwas made""" + - def __init__(self, instance=None, data=empty, **kwargs): super().__init__(instance=instance, data=data, **kwargs) if 'context' in kwargs: + if 'request' in kwargs['context']: + + self.request = kwargs['context']['request'] + if 'view' in kwargs['context']: if kwargs['context']['view'].action == 'create': @@ -179,8 +190,7 @@ def __init__(self, instance=None, data=empty, **kwargs): if 'request' in kwargs['context']['view'].kwargs: - self.fields.fields['user'].initial = kwargs['context']['request']._user.id - + self.fields.fields['user'].initial = self.request._user.id @@ -216,6 +226,10 @@ def validate(self, attrs): code = 'read_only' ) + if self.is_triage: + + attrs = self.validate_triage(attrs) + return attrs @@ -322,7 +336,7 @@ class Meta(TicketCommentModelSerializer.Meta): -class TicketCommentITILFollowUpModelSerializer(TicketCommentITILModelSerializer): +class TicketCommentITILFollowUpAddModelSerializer(TicketCommentITILModelSerializer): """ITIL Followup Comment Args: @@ -337,21 +351,64 @@ class Meta(TicketCommentITILModelSerializer.Meta): 'ticket', 'external_ref', 'external_system', - # 'body', 'private', 'duration', + 'category', + 'template', + 'is_template', + 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + + +class TicketCommentITILFollowUpChangeModelSerializer(TicketCommentITILFollowUpAddModelSerializer): + + pass + + + +class TicketCommentITILFollowUpTriageModelSerializer(TicketCommentITILModelSerializer): + """ITIL Followup Comment + + Args: + TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. + """ + + class Meta(TicketCommentITILModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + # 'private', + 'duration', # 'category', # 'template', - 'is_template', + # 'is_template', # 'source', 'status', - # 'responsible_user', - # 'responsible_team', + 'responsible_user', + 'responsible_team', 'user', - # 'planned_start_date', - # 'planned_finish_date', - # # 'real_start_date', - # 'real_finish_date', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', 'organization', 'date_closed', 'created', @@ -359,10 +416,15 @@ class Meta(TicketCommentITILModelSerializer.Meta): '_urls', ] + is_triage: bool = True + def validate_triage(self, attrs): -class TicketCommentITILTaskModelSerializer(TicketCommentITILModelSerializer): - """ITIL Task Comment + return attrs + + +class TicketCommentITILSolutionAddModelSerializer(TicketCommentITILModelSerializer): + """ITIL Solution Comment Args: TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. @@ -376,21 +438,18 @@ class Meta(TicketCommentITILModelSerializer.Meta): 'ticket', 'external_ref', 'external_system', - # 'body', 'private', 'duration', - # 'category', - # 'template', 'is_template', 'source', 'status', - # 'responsible_user', - # 'responsible_team', + 'responsible_user', + 'responsible_team', 'user', - # 'planned_start_date', - # 'planned_finish_date', - # 'real_start_date', - # 'real_finish_date', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', 'organization', 'date_closed', 'created', @@ -400,13 +459,61 @@ class Meta(TicketCommentITILModelSerializer.Meta): -class TicketCommentITILSolutionModelSerializer(TicketCommentITILModelSerializer): +class TicketCommentITILSolutionChangeModelSerializer(TicketCommentITILSolutionAddModelSerializer): + + pass + + + +class TicketCommentITILSolutionTriageModelSerializer(TicketCommentITILModelSerializer): """ITIL Solution Comment Args: TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. """ + class Meta(TicketCommentITILModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + # 'private', + 'duration', + # 'is_template', + # 'source', + 'status', + 'responsible_user', + 'responsible_team', + 'user', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + is_triage: bool = True + + def validate_triage(self, attrs): + + return attrs + + + +class TicketCommentITILTaskAddModelSerializer(TicketCommentITILModelSerializer): + """ITIL Task Comment + + Args: + TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. + """ + class Meta(TicketCommentITILModelSerializer.Meta): read_only_fields = [ @@ -417,6 +524,8 @@ class Meta(TicketCommentITILModelSerializer.Meta): 'external_system', 'private', 'duration', + 'category', + 'template', 'is_template', 'source', 'status', @@ -436,6 +545,57 @@ class Meta(TicketCommentITILModelSerializer.Meta): +class TicketCommentITILTaskChangeModelSerializer(TicketCommentITILTaskAddModelSerializer): + + pass + + + +class TicketCommentITILTaskTriageModelSerializer(TicketCommentITILModelSerializer): + """ITIL Task Comment + + Args: + TicketCommentITILModelSerializer (class): Base class for ALL ITIL comment types. + """ + + class Meta(TicketCommentITILModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'parent', + 'ticket', + 'external_ref', + 'external_system', + # 'body', + # 'private', + 'duration', + # 'category', + # 'template', + # 'is_template', + # 'source', + # 'status', + # 'responsible_user', + # 'responsible_team', + 'user', + # 'planned_start_date', + # 'planned_finish_date', + # 'real_start_date', + # 'real_finish_date', + 'organization', + 'date_closed', + 'created', + 'modified', + '_urls', + ] + + is_triage: bool = True + + def validate_triage(self, attrs): + + return attrs + + + class TicketCommentImportModelSerializer(TicketCommentModelSerializer): """Import User Serializer diff --git a/app/core/viewsets/ticket_comment.py b/app/core/viewsets/ticket_comment.py index 7c9e83ae5..e14677603 100644 --- a/app/core/viewsets/ticket_comment.py +++ b/app/core/viewsets/ticket_comment.py @@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, PolymorphicProxySerializer -from rest_framework import generics, viewsets, exceptions +from rest_framework import generics, viewsets from rest_framework.response import Response from access.mixin import OrganizationMixin @@ -11,13 +11,24 @@ from api.views.mixin import OrganizationPermissionAPI from api.viewsets.common import ModelViewSet +from core import exceptions as centurion_exceptions from core.serializers.ticket_comment import ( Ticket, TicketComment, TicketCommentImportModelSerializer, - TicketCommentITILFollowUpModelSerializer, - TicketCommentITILSolutionModelSerializer, - TicketCommentITILTaskModelSerializer, + + TicketCommentITILFollowUpAddModelSerializer, + TicketCommentITILFollowUpChangeModelSerializer, + TicketCommentITILFollowUpTriageModelSerializer, + + TicketCommentITILSolutionAddModelSerializer, + TicketCommentITILSolutionChangeModelSerializer, + TicketCommentITILSolutionTriageModelSerializer, + + TicketCommentITILTaskAddModelSerializer, + TicketCommentITILTaskChangeModelSerializer, + TicketCommentITILTaskTriageModelSerializer, + TicketCommentModelSerializer, TicketCommentViewSerializer ) @@ -39,10 +50,18 @@ component_name = 'TicketComment', serializers=[ TicketCommentImportModelSerializer, - TicketCommentITILFollowUpModelSerializer, - TicketCommentITILSolutionModelSerializer, - TicketCommentITILTaskModelSerializer, - TicketCommentModelSerializer + + TicketCommentITILFollowUpAddModelSerializer, + TicketCommentITILFollowUpChangeModelSerializer, + TicketCommentITILFollowUpTriageModelSerializer, + + TicketCommentITILSolutionAddModelSerializer, + TicketCommentITILSolutionChangeModelSerializer, + TicketCommentITILSolutionTriageModelSerializer, + + TicketCommentITILTaskAddModelSerializer, + TicketCommentITILTaskChangeModelSerializer, + TicketCommentITILTaskTriageModelSerializer, ], resource_type_field_name=None, many = False @@ -144,52 +163,102 @@ def get_serializer_class(self): and self.action == 'create' ): - raise exceptions.ValidationError( + raise centurion_exceptions.ValidationError( detail = { 'comment_type': 'comment type is required' }, code = 'required' ) - elif ( - self.action == 'create' - ): - ticket = Ticket.objects.get(pk = int(self.kwargs['ticket_id'])) + ticket = Ticket.objects.get(pk = int(self.kwargs['ticket_id'])) - organization = int(ticket.organization.id) + ticket_type = str(ticket.get_ticket_type_display()).lower().replace(' ' , '_') - if organization: + organization = int(ticket.organization.id) - if self.has_organization_permission( - organization = organization, - permissions_required = [ - 'core.import_ticketcomment' - ] + if organization: + + if self.has_organization_permission( + organization = organization, + permissions_required = [ + 'core.import_ticketcomment' + ] + ): + + serializer_prefix = serializer_prefix + 'Import' + + elif ( + self.action == 'create' + or self.action == 'partial_update' + or self.action == 'update' + ): + + if( + self.action == 'partial_update' + or self.action == 'update' ): - serializer_prefix = serializer_prefix + 'Import' + comment_type = list(self.queryset)[0].comment_type - elif int(self.request.data['comment_type']) == int(TicketComment.CommentType.COMMENT): + else: + + comment_type = int(self.request.data['comment_type']) - serializer_prefix = serializer_prefix + 'ITILFollowUp' - elif int(self.request.data['comment_type']) == int(TicketComment.CommentType.SOLUTION): + if comment_type == int(TicketComment.CommentType.COMMENT): serializer_prefix = serializer_prefix + 'ITILFollowUp' - elif int(self.request.data['comment_type']) == int(TicketComment.CommentType.TASK): + elif comment_type == int(TicketComment.CommentType.SOLUTION): - serializer_prefix = serializer_prefix + 'ITILFollowUp' + serializer_prefix = serializer_prefix + 'ITILSolution' + + elif comment_type == int(TicketComment.CommentType.TASK): + + serializer_prefix = serializer_prefix + 'ITILTask' else: - raise exceptions.ValidationError( + raise centurion_exceptions.ValidationError( detail = 'Unable to determine the serializer', code = 'serializer_unknwon' ) + if 'Import' not in serializer_prefix: + + if self.action == 'create': + + if self.has_organization_permission( + organization = ticket.organization.id, + permissions_required = [ 'core.triage_ticket_'+ ticket_type ], + ) and not self.request.user.is_superuser: + + serializer_prefix = serializer_prefix + 'Triage' + + else: + + serializer_prefix = serializer_prefix + 'Add' + + + elif ( + self.action == 'partial_update' + or self.action == 'update' + ): + + if self.has_organization_permission( + organization = ticket.organization.id, + permissions_required = [ 'core.triage_ticket_'+ ticket_type ], + ) and not self.request.user.is_superuser: + + serializer_prefix = serializer_prefix + 'Triage' + + else: + + serializer_prefix = serializer_prefix + 'Change' + + if ( self.action == 'list' or self.action == 'retrieve' From 80b8cdb35633cff0b811646a318d587e97cc9853 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 00:12:53 +0930 Subject: [PATCH 405/617] test(core): Ticket Comment API v2 Serializer checks ref: #15 #248 #373 --- .../test_ticket_comment_serializer.py | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 app/core/tests/unit/ticket_comment/test_ticket_comment_serializer.py diff --git a/app/core/tests/unit/ticket_comment/test_ticket_comment_serializer.py b/app/core/tests/unit/ticket_comment/test_ticket_comment_serializer.py new file mode 100644 index 000000000..95a056a06 --- /dev/null +++ b/app/core/tests/unit/ticket_comment/test_ticket_comment_serializer.py @@ -0,0 +1,354 @@ +import pytest + +from django.test import TestCase +from django.contrib.auth.models import User + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.ticket_comment import ( + Ticket, + TicketComment, + + TicketCommentITILFollowUpAddModelSerializer, + TicketCommentITILSolutionAddModelSerializer, + TicketCommentITILTaskAddModelSerializer, + + TicketCommentITILFollowUpTriageModelSerializer, + TicketCommentITILSolutionTriageModelSerializer, + TicketCommentITILTaskTriageModelSerializer, +) + + +class MockView: + + action: str = None + + kwargs: dict = {} + + + +class MockRequest: + + _user = None + + + +class TicketCommentValidationAPI: + + model = TicketComment + + serializer = None + """Serializer to test""" + + serializer_data: dict = None + """ Data to pass to the serialzer""" + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + self.user = User.objects.create( + username = 'user', + password = 'password', + is_superuser = True, + ) + + self.ticket = Ticket.objects.create( + organization=organization, + title = 'ticket title', + description = 'some text', + opened_by = self.user, + status = Ticket.TicketStatus.All.NEW, + ticket_type = Ticket.TicketType.REQUEST, + ) + + self.item = self.model.objects.create( + organization=organization, + body = 'some text', + ticket = self.ticket + ) + + + def test_serializer_validation_add_valid_item(self): + """Serializer Validation Check + + Ensure that a valid item it does not raise a validation error + """ + + mock_view = MockView() + mock_view.action = 'create' + + mock_request = MockRequest() + mock_request._user = self.user + + + mock_view.kwargs: dict = { + 'ticket_id': int(self.ticket.id) + } + + serializer = self.serializer( + context = { + 'view': mock_view, + 'request': mock_request + }, + data = self.serializer_data + ) + + assert serializer.is_valid(raise_exception = True) + + + def test_serializer_validation_no_ticket(self): + """Serializer Validation Check + + Ensure that no specified ticket raises a validation error + """ + + mock_view = MockView() + mock_view.action = 'create' + + mock_request = MockRequest() + mock_request._user = self.user + + serializer = self.serializer( + context = { + 'view': mock_view, + 'request': mock_request + }, + data = self.serializer_data + ) + + with pytest.raises(ValidationError) as err: + + serializer = self.serializer( + context = { + 'view': mock_view, + 'request': mock_request + }, + data = self.serializer_data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['ticket'] == 'required' + + + def test_serializer_validation_no_body(self): + """Serializer Validation Check + + Ensure that if no body specified a validation error is raised + """ + + mock_view = MockView() + mock_view.action = 'create' + mock_view.kwargs: dict = { + 'ticket_id': int(self.ticket.id) + } + + + mock_request = MockRequest() + mock_request._user = self.user + + + serializer_data:dict = self.serializer_data.copy() + del serializer_data['body'] + + + with pytest.raises(ValidationError) as err: + + serializer = self.serializer( + context = { + 'view': mock_view, + 'request': mock_request + }, + data = serializer_data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['body'][0] == 'required' + + + def test_serializer_validation_no_comment_type(self): + """Serializer Validation Check + + Ensure that if no comment_type specified a validation error is raised + """ + + mock_view = MockView() + mock_view.action = 'create' + mock_view.kwargs: dict = { + 'ticket_id': int(self.ticket.id) + } + + + serializer_data:dict = self.serializer_data.copy() + del serializer_data['comment_type'] + + mock_request = MockRequest() + mock_request._user = self.user + + + with pytest.raises(ValidationError) as err: + + serializer = self.serializer( + context = { + 'view': mock_view, + 'request': mock_request + }, + data = serializer_data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['comment_type'][0] == 'required' + + + +class TicketCommentITILFollowUpAddValidationAPI( + TicketCommentValidationAPI, + TestCase, +): + + serializer = TicketCommentITILFollowUpAddModelSerializer + + comment_type = TicketComment.CommentType.COMMENT + + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + self.serializer_data = { + 'organization': self.organization.id, + 'body': 'comment body', + 'comment_type': int(self.comment_type) + } + + + +class TicketCommentITILSolutionAddValidationAPI( + TicketCommentValidationAPI, + TestCase, +): + + serializer = TicketCommentITILSolutionAddModelSerializer + + comment_type = TicketComment.CommentType.SOLUTION + + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + self.serializer_data = { + 'organization': self.organization.id, + 'body': 'comment body', + 'comment_type': int(self.comment_type) + } + + + +class TicketCommentITILTaskAddValidationAPI( + TicketCommentValidationAPI, + TestCase, +): + + serializer = TicketCommentITILTaskAddModelSerializer + + comment_type = TicketComment.CommentType.TASK + + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + self.serializer_data = { + 'organization': self.organization.id, + 'ticket': self.ticket, + 'body': 'comment body', + 'comment_type': int(self.comment_type) + } + + + +class TicketCommentITILFollowUpTriageValidationAPI( + TicketCommentValidationAPI, + TestCase, +): + + serializer = TicketCommentITILFollowUpTriageModelSerializer + + comment_type = TicketComment.CommentType.COMMENT + + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + self.serializer_data = { + 'organization': self.organization.id, + 'body': 'comment body', + 'comment_type': int(self.comment_type) + } + + + +class TicketCommentITILSolutionTriageValidationAPI( + TicketCommentValidationAPI, + TestCase, +): + + serializer = TicketCommentITILSolutionTriageModelSerializer + + comment_type = TicketComment.CommentType.SOLUTION + + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + self.serializer_data = { + 'organization': self.organization.id, + 'body': 'comment body', + 'comment_type': int(self.comment_type) + } + + + +class TicketCommentITILTaskTriageValidationAPI( + TicketCommentValidationAPI, + TestCase, +): + + serializer = TicketCommentITILTaskTriageModelSerializer + + comment_type = TicketComment.CommentType.TASK + + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + self.serializer_data = { + 'organization': self.organization.id, + 'ticket': self.ticket, + 'body': 'comment body', + 'comment_type': int(self.comment_type) + } From 0965f567197af2c10c41679ffc16af10aa923a1c Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:12:01 +0930 Subject: [PATCH 406/617] test(core): Related Ticket API v2 ViewSet permission checks ref: #15 #248 #368 #374 --- .../test_related_ticket_viewset.py | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 app/core/tests/unit/related_ticket/test_related_ticket_viewset.py diff --git a/app/core/tests/unit/related_ticket/test_related_ticket_viewset.py b/app/core/tests/unit/related_ticket/test_related_ticket_viewset.py new file mode 100644 index 000000000..0cba5970d --- /dev/null +++ b/app/core/tests/unit/related_ticket/test_related_ticket_viewset.py @@ -0,0 +1,265 @@ +import pytest +import unittest +import requests + + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType +from django.shortcuts import reverse +from django.test import Client, TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from api.tests.abstract.api_permissions_viewset import ( + APIPermissionDelete, + APIPermissionView, +) + +from core.models.ticket.ticket import Ticket, RelatedTickets + + + +class RelatedTicketsPermissionsAPI( + APIPermissionDelete, + APIPermissionView, + TestCase, +): + + model = RelatedTickets + + app_namespace = 'v2' + + url_name = '_api_v2_ticket_related' + + change_data = {'from_ticket_id': 1, 'organization': 1,} + + delete_data = {} + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + different_organization = Organization.objects.create(name='test_different_organization') + + view_permissions = Permission.objects.get( + codename = 'view_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'view_team', + organization = organization, + ) + + view_team.permissions.set([view_permissions]) + + + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + add_team.permissions.set([add_permissions]) + + + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + change_team.permissions.set([change_permissions]) + + + + delete_permissions = Permission.objects.get( + codename = 'delete_' + self.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + delete_team = Team.objects.create( + team_name = 'delete_team', + organization = organization, + ) + + delete_team.permissions.set([delete_permissions]) + + + self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password") + + + self.view_user = User.objects.create_user(username="test_user_view", password="password") + teamuser = TeamUsers.objects.create( + team = view_team, + user = self.view_user + ) + + ticket_one = Ticket.objects.create( + organization = self.organization, + title = 'A ticket', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.view_user, + status = Ticket.TicketStatus.All.NEW.value + ) + + ticket_two = Ticket.objects.create( + organization = self.organization, + title = 'B ticket', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.view_user, + status = Ticket.TicketStatus.All.NEW.value + ) + + + self.item = self.model.objects.create( + organization = self.organization, + from_ticket_id = ticket_one, + to_ticket_id = ticket_two, + how_related = RelatedTickets.Related.BLOCKS + ) + + + self.url_view_kwargs = {'ticket_id': ticket_one.id, 'pk': self.item.id} + + self.url_kwargs = {'ticket_id': ticket_one.id} + + self.add_data = { + 'organization': self.organization.id, + 'from_ticket_id': ticket_two.id, + 'to_ticket_id': ticket_one.id, + 'how_related': RelatedTickets.Related.RELATED + } + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + teamuser = TeamUsers.objects.create( + team = add_team, + user = self.add_user + ) + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + teamuser = TeamUsers.objects.create( + team = change_team, + user = self.change_user + ) + + self.delete_user = User.objects.create_user(username="test_user_delete", password="password") + teamuser = TeamUsers.objects.create( + team = delete_team, + user = self.delete_user + ) + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") + + + different_organization_team = Team.objects.create( + team_name = 'different_organization_team', + organization = different_organization, + ) + + different_organization_team.permissions.set([ + view_permissions, + add_permissions, + change_permissions, + delete_permissions, + ]) + + TeamUsers.objects.create( + team = different_organization_team, + user = self.different_organization_user + ) + + + + def test_add_has_permission_post_not_allowed(self): + """ Check correct permission for add + + Attempt to add as user with permission + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 405 + + + + def test_change_has_permission_patch_not_allowed(self): + """ Check correct permission for change + + Make change with user who has change permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.patch(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 + + + + def test_change_has_permission_put_not_allowed(self): + """ Check correct permission for change + + Make change with user who has change permission + """ + + client = Client() + url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.change_user) + response = client.put(url, data=self.change_data, content_type='application/json') + + assert response.status_code == 405 From 2c934d4eaff068d8e42bc5b9674d8912cdcd998d Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:14:37 +0930 Subject: [PATCH 407/617] fix(api): Ensure `METHOD_NOT_ALLOWED` exception is thrown ref: #15 #248 #368 #374 --- app/api/views/mixin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/api/views/mixin.py b/app/api/views/mixin.py index 746fb48a1..4d294e379 100644 --- a/app/api/views/mixin.py +++ b/app/api/views/mixin.py @@ -6,6 +6,8 @@ from access.mixin import OrganizationMixin +from core import exceptions as centurion_exceptions + class OrganizationPermissionAPI(DjangoObjectPermissions, OrganizationMixin): @@ -66,7 +68,7 @@ def permission_check(self, request, view, obj=None) -> bool: if 'organization' in request.data: if not request.data['organization']: - raise ValidationError('you must provide an organization') + raise centurion_exceptions.ValidationError('you must provide an organization') object_organization = int(request.data['organization']) elif method == 'patch': @@ -175,6 +177,10 @@ def permission_check(self, request, view, obj=None) -> bool: raise PermissionDenied('You are not part of this organization') + except centurion_exceptions.MethodNotAllowed as e: + + raise centurion_exceptions.MethodNotAllowed( str(method).upper() ) + except Exception as e: return False From f45019024b084f9f696eacbf947e74f463e1cf46 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:38:00 +0930 Subject: [PATCH 408/617] fix(core): Ensure related ticket slash command works for ticket comments ref: #15 #248 #368 #374 --- app/core/lib/slash_commands/related_ticket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/lib/slash_commands/related_ticket.py b/app/core/lib/slash_commands/related_ticket.py index 97c926bb7..1f7f85d3c 100644 --- a/app/core/lib/slash_commands/related_ticket.py +++ b/app/core/lib/slash_commands/related_ticket.py @@ -75,7 +75,7 @@ def command_related_ticket(self, match) -> str: to_ticket = self.__class__.objects.get(pk = ticket_id) - elif str(self._meta.verbose_name).lower() == 'comment': + elif str(self._meta.verbose_name).lower() == 'ticket comment': from_ticket = self.ticket From 7d62d6b1c78804c3247448707a5d150fed9fbd34 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:56:50 +0930 Subject: [PATCH 409/617] test(core): Related Ticket API v2 Serializer checks ref: #15 #248 #368 #374 --- .../test_related_ticket_serializer.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 app/core/tests/unit/related_ticket/test_related_ticket_serializer.py diff --git a/app/core/tests/unit/related_ticket/test_related_ticket_serializer.py b/app/core/tests/unit/related_ticket/test_related_ticket_serializer.py new file mode 100644 index 000000000..2207a0bee --- /dev/null +++ b/app/core/tests/unit/related_ticket/test_related_ticket_serializer.py @@ -0,0 +1,139 @@ +import pytest + +from django.contrib.auth.models import User +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.serializers.ticket_related import ( + Ticket, + RelatedTickets, + RelatedTicketModelSerializer, +) + + + +class RelatedTicketsValidationAPI( + TestCase, +): + + model = RelatedTickets + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + user = User.objects.create_user(username="test_user_view", password="password") + + + self.ticket_one = Ticket.objects.create( + organization = self.organization, + title = 'A ticket', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = user, + status = Ticket.TicketStatus.All.NEW.value + ) + + self.ticket_two = Ticket.objects.create( + organization = self.organization, + title = 'B ticket', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = user, + status = Ticket.TicketStatus.All.NEW.value + ) + + self.ticket_three = Ticket.objects.create( + organization = self.organization, + title = 'C ticket', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = user, + status = Ticket.TicketStatus.All.NEW.value + ) + + self.item = self.model.objects.create( + organization = self.organization, + from_ticket_id = self.ticket_one, + to_ticket_id = self.ticket_two, + how_related = RelatedTickets.Related.BLOCKS + ) + + + + + def test_serializer_validation_create_valid(self): + """Serializer Validation Check + + Ensure that a valid item is created and no validation error occurs + """ + + serializer = RelatedTicketModelSerializer( + data={ + 'organization': self.organization.id, + 'from_ticket_id': self.ticket_one.id, + 'to_ticket_id': self.ticket_three.id, + 'how_related': RelatedTickets.Related.BLOCKS + } + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_add_existing_related_ticket(self): + """Serializer Validation Check + + Ensure that if adding a duplicate linked ticket + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = RelatedTicketModelSerializer( + data={ + 'organization': self.organization.id, + 'from_ticket_id': self.ticket_one.id, + 'to_ticket_id': self.ticket_two.id, + 'how_related': RelatedTickets.Related.BLOCKS + } + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['to_ticket_id'][0] == 'duplicate_entry' + + + + def test_serializer_validation_add_existing_related_ticket_inverted(self): + """Serializer Validation Check + + Ensure that if adding a duplicate linked ticket + it raises a validation error + """ + + with pytest.raises(ValidationError) as err: + + serializer = RelatedTicketModelSerializer( + data={ + 'organization': self.organization.id, + 'from_ticket_id': self.ticket_two.id, + 'to_ticket_id': self.ticket_one.id, + 'how_related': RelatedTickets.Related.BLOCKS + } + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['to_ticket_id'][0] == 'duplicate_entry' From daa8dbe04b82fff8220d743be41994f715e5ee78 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:58:36 +0930 Subject: [PATCH 410/617] feat(core): Add MethodNot Allowed to Centurion exceptions ref: #15 #248 #368 #374 --- app/core/exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/core/exceptions.py b/app/core/exceptions.py index 4d2b4f73b..0acbfee97 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,7 +1,8 @@ from rest_framework import exceptions, status from rest_framework.exceptions import ( + MethodNotAllowed, + PermissionDenied, ValidationError, - PermissionDenied ) class MissingAttribute(Exception): From bf56b271d7aa77423b5f646c03dcdd87c91554c1 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:59:12 +0930 Subject: [PATCH 411/617] feat(core): Ensure Related Tickets validate against duplicate entries ref: #248 #368 #374 --- app/core/serializers/ticket_related.py | 30 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/app/core/serializers/ticket_related.py b/app/core/serializers/ticket_related.py index f7aa436fe..76c0a192a 100644 --- a/app/core/serializers/ticket_related.py +++ b/app/core/serializers/ticket_related.py @@ -4,8 +4,9 @@ from access.serializers.organization import OrganizationBaseSerializer -from core.serializers.ticket import TicketBaseSerializer +from core.serializers.ticket import Ticket, TicketBaseSerializer +from core import exceptions as centurion_exceptions from core.models.ticket.ticket import RelatedTickets @@ -101,14 +102,33 @@ class Meta: read_only_fields = [ 'id', 'display_name', - 'to_ticket_id', - 'from_ticket_id', - 'how_related', - 'organization', '_urls', ] + def validate(self, attrs): + + check_db = self.Meta.model.objects.filter( + to_ticket_id = attrs['to_ticket_id'], + from_ticket_id = attrs['from_ticket_id'], + ) + + check_db_inverse = self.Meta.model.objects.filter( + to_ticket_id = attrs['from_ticket_id'], + from_ticket_id = attrs['to_ticket_id'], + ) + + if check_db.count() > 0 or check_db_inverse.count() > 0: + + raise centurion_exceptions.ValidationError( + detail = { + 'to_ticket_id': f"Ticket is already related to #{attrs['to_ticket_id'].id}" + }, + code = 'duplicate_entry' + ) + + return attrs + class RelatedTicketViewSerializer(RelatedTicketModelSerializer): From 04ae3388648af528ea88bee2d0c36fdb6abc0910 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 14:59:47 +0930 Subject: [PATCH 412/617] refactor(core): Related ticket slash command to use serializer ref: #248 #368 #374 --- app/core/lib/slash_commands/related_ticket.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/core/lib/slash_commands/related_ticket.py b/app/core/lib/slash_commands/related_ticket.py index 1f7f85d3c..4e0287c54 100644 --- a/app/core/lib/slash_commands/related_ticket.py +++ b/app/core/lib/slash_commands/related_ticket.py @@ -48,7 +48,7 @@ def command_related_ticket(self, match) -> str: if ticket_id is not None: - from core.models.ticket.ticket import RelatedTickets + from core.serializers.ticket_related import RelatedTicketModelSerializer if command == 'relate': @@ -82,13 +82,23 @@ def command_related_ticket(self, match) -> str: to_ticket = self.ticket.__class__.objects.get(pk = ticket_id) - RelatedTickets.objects.create( - from_ticket_id = from_ticket, - how_related = how_related, - to_ticket_id = to_ticket, - organization = self.organization + item = RelatedTicketModelSerializer( + data = { + 'from_ticket_id': from_ticket.id, + 'how_related': int(how_related), + 'to_ticket_id': to_ticket.id, + 'organization': from_ticket.organization.id + } ) + if item.is_valid( raise_exception = False ): + + item.save() + + else: + + return str(match.string[match.start():match.end()]) + else: #ToDo: Add logging that the slash command could not be processed. From e61b883c14bc4429b328a974a1fe9de58cc92324 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 15:00:32 +0930 Subject: [PATCH 413/617] fix(core): Only use Import Serializer on Ticket Comment Create if user has perms ref: #248 #368 #374 --- app/core/viewsets/ticket_comment.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/core/viewsets/ticket_comment.py b/app/core/viewsets/ticket_comment.py index e14677603..8fbc93205 100644 --- a/app/core/viewsets/ticket_comment.py +++ b/app/core/viewsets/ticket_comment.py @@ -186,7 +186,12 @@ def get_serializer_class(self): ] ): - serializer_prefix = serializer_prefix + 'Import' + if ( + self.action == 'create' + or self.action == 'partial_update' + or self.action == 'update' + ): + serializer_prefix = serializer_prefix + 'Import' elif ( self.action == 'create' From 9fe4883f919639e1291363c17e9bea807a8bca7a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 16:14:20 +0930 Subject: [PATCH 414/617] refactor(core): Ticket Linked Item slash command to use serializer ref: #248 #368 #374 --- app/core/lib/slash_commands/linked_model.py | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/core/lib/slash_commands/linked_model.py b/app/core/lib/slash_commands/linked_model.py index dbd40c490..7efafb793 100644 --- a/app/core/lib/slash_commands/linked_model.py +++ b/app/core/lib/slash_commands/linked_model.py @@ -1,6 +1,7 @@ import re + class CommandLinkedModel: # This summary is used for the user documentation """Link an item to the current ticket. Supports all ticket @@ -132,14 +133,24 @@ def command_linked_model(self, match) -> str: pk = model_id ) - TicketLinkedItem.objects.create( - organization = self.organization, - ticket = ticket, - item_type = item_type, - item = item.id + from core.serializers.ticket_linked_item import TicketLinkedItemModelSerializer + + serializer = TicketLinkedItemModelSerializer( + data = { + 'organization': ticket.organization, + 'ticket': ticket.id, + 'item_type': item_type, + 'item': item.id + } ) - return None + if serializer.is_valid(): + + serializer.save() + + return None + + return str(match.string[match.start():match.end()]) except Exception as e: From effa2904f8aeb0d0e1283ecb347db5f3caa0bca7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 16:14:49 +0930 Subject: [PATCH 415/617] fix(core): Ensure Ticket Linked Item slash command works for ticket comments ref: #248 #368 #374 --- app/core/lib/slash_commands/linked_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/lib/slash_commands/linked_model.py b/app/core/lib/slash_commands/linked_model.py index 7efafb793..85968cc9f 100644 --- a/app/core/lib/slash_commands/linked_model.py +++ b/app/core/lib/slash_commands/linked_model.py @@ -122,7 +122,7 @@ def command_linked_model(self, match) -> str: ticket = self - elif str(self._meta.verbose_name).lower() == 'comment': + elif str(self._meta.verbose_name).lower() == 'ticket comment': ticket = self.ticket From f27e0379c2644b035133f9fef868b437fc733ad7 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 16:22:13 +0930 Subject: [PATCH 416/617] refactor(core): Ensure Ticket Linked Serializer works for Item Tickets ref: #248 #368 #374 --- app/core/serializers/ticket_linked_item.py | 28 +++++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/app/core/serializers/ticket_linked_item.py b/app/core/serializers/ticket_linked_item.py index 434574f7d..33aa8696d 100644 --- a/app/core/serializers/ticket_linked_item.py +++ b/app/core/serializers/ticket_linked_item.py @@ -79,26 +79,36 @@ class Meta: read_only_fields = [ 'id', 'display_name', - 'ticket', 'organization', 'created', '_urls', ] - - - def is_valid(self, *, raise_exception=False): - is_valid = super().is_valid( raise_exception = raise_exception ) + def validate(self, data): + ticket = None if 'view' in self._context: - ticket = Ticket.objects.get(pk = int(self._context['view'].kwargs['ticket_id']) ) + if 'ticket_id' in self._context['view'].kwargs: + + ticket = Ticket.objects.get(pk = int(self._context['view'].kwargs['ticket_id']) ) + + + if ( + 'ticket' in data + and ticket is None + ): + + ticket = data['ticket'] + + + if ticket: - self.validated_data['ticket'] = ticket + data['ticket'] = ticket - self.validated_data['organization_id'] = ticket.organization.id + data['organization_id'] = ticket.organization.id else: @@ -110,7 +120,7 @@ def is_valid(self, *, raise_exception=False): ) - return is_valid + return data From 08e13a728ad11741eccc3a8efbea5f81333fbc56 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 16:23:06 +0930 Subject: [PATCH 417/617] test(core): Item Linked Ticket API v2 ViewSet permission checks ref: #15 #248 #368 #374 --- .../test_ticket_linked_item_viewset.py | 373 ++++++++++++++++-- 1 file changed, 348 insertions(+), 25 deletions(-) diff --git a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py index ec65034c5..18a77b47d 100644 --- a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py +++ b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_viewset.py @@ -14,17 +14,14 @@ from core.models.ticket.ticket_linked_items import Ticket, TicketLinkedItem -from itam.models.device import Device - from settings.models.user_settings import UserSettings -class TicketLinkedItemPermissionsAPI( +class BaseTicketLinkedItemPermissionsAPI( APIPermissionAdd, APIPermissionDelete, APIPermissionView, - TestCase ): """ Test Cases common to ALL ticket types """ @@ -40,9 +37,8 @@ class TicketLinkedItemPermissionsAPI( url_name = '_api_v2_ticket_linked_item' - @classmethod - def setUpTestData(self): + def CreateOrg(self): """Setup Test 1. Create an organization for user and item @@ -58,6 +54,26 @@ def setUpTestData(self): self.organization = organization + # different_organization = Organization.objects.create(name='test_different_organization') + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + . create an organization that is different to item + 2. Create a team + 3. create teams with each permission: view, add, change, delete + 4. create a user per team + """ + + + + # organization = Organization.objects.create(name='test_org') + + # self.organization = organization + different_organization = Organization.objects.create(name='test_different_organization') @@ -71,7 +87,7 @@ def setUpTestData(self): view_team = Team.objects.create( team_name = 'view_team', - organization = organization, + organization = self.organization, ) view_team.permissions.set([view_permissions]) @@ -88,7 +104,7 @@ def setUpTestData(self): add_team = Team.objects.create( team_name = 'add_team', - organization = organization, + organization = self.organization, ) add_team.permissions.set([add_permissions]) @@ -105,7 +121,7 @@ def setUpTestData(self): change_team = Team.objects.create( team_name = 'change_team', - organization = organization, + organization = self.organization, ) change_team.permissions.set([change_permissions]) @@ -122,7 +138,7 @@ def setUpTestData(self): delete_team = Team.objects.create( team_name = 'delete_team', - organization = organization, + organization = self.organization, ) delete_team.permissions.set([delete_permissions]) @@ -147,32 +163,22 @@ def setUpTestData(self): status = Ticket.TicketStatus.All.NEW ) - self.device = Device.objects.create( - organization = self.organization, - name = 'one', - ) - - self.device_two = Device.objects.create( - organization = self.organization, - name = 'two', - ) - self.item = self.model.objects.create( organization = self.organization, - item = self.device.id, - item_type = TicketLinkedItem.Modules.DEVICE, + item = self.linked_item.id, + item_type = self.item_type, ticket = self.ticket, ) - self.url_kwargs = {'ticket_id': self.ticket.id} + # self.url_kwargs = {'ticket_id': self.ticket.id} - self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + # self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} self.add_data = { 'organization': self.organization.id, 'ticket': self.ticket.id, - 'item': self.device_two.id, + 'item': self.linked_item_two.id, 'item_type': int(TicketLinkedItem.Modules.DEVICE), } @@ -223,3 +229,320 @@ def setUpTestData(self): team = different_organization_team, user = self.different_organization_user ) + + + +class TicketLinkedItemPermissionsAPI( + BaseTicketLinkedItemPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + model = TicketLinkedItem + + app_namespace = 'v2' + + delete_data = {} + + ticket_type: str = 'request' + + ticket_type_enum = Ticket.TicketType.REQUEST + + url_name = '_api_v2_ticket_linked_item' + + item_class: str = 'device' + + item_type = TicketLinkedItem.Modules.DEVICE + + + @classmethod + def setUpTestData(self): + + from itam.models.device import Device + + self.CreateOrg() + + self.linked_item = Device.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = Device.objects.create( + organization = self.organization, + name = 'two', + ) + + super().setUpTestData() + + self.url_kwargs = {'ticket_id': self.ticket.id} + + self.url_view_kwargs = {'ticket_id': self.ticket.id, 'pk': self.item.id} + + + +class BaseItemTicketPermissionsAPI( + BaseTicketLinkedItemPermissionsAPI, +): + """ Test Cases common to ALL ticket types """ + + model = TicketLinkedItem + + app_namespace = 'v2' + + delete_data = {} + + ticket_type: str = 'request' + + ticket_type_enum = Ticket.TicketType.REQUEST + + url_name = '_api_v2_item_tickets' + + item_class: str = None + + item_type = None + + @classmethod + def setUpTestData(self): + + from itam.models.device import Device + + super().setUpTestData() + + self.url_kwargs = {'item_class': self.item_class, 'item_id': self.linked_item.id} + + self.url_view_kwargs = {'item_class': self.item_class, 'item_id': self.linked_item.id, 'pk': self.item.id} + + + + def test_add_has_permission(self): + """ Check correct permission for add + + Add not allowed from this endpoint + """ + + client = Client() + if self.url_kwargs: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = self.url_kwargs) + + else: + + url = reverse(self.app_namespace + ':' + self.url_name + '-list') + + + client.force_login(self.add_user) + response = client.post(url, data=self.add_data) + + assert response.status_code == 201 + + + + +class ItemClusterTicketPermissionsAPI( + BaseItemTicketPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + + item_class: str = 'cluster' + + item_type = TicketLinkedItem.Modules.CLUSTER + + + @classmethod + def setUpTestData(self): + + from itim.models.clusters import Cluster + + self.CreateOrg() + + self.linked_item = Cluster.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = Cluster.objects.create( + organization = self.organization, + name = 'two', + ) + + + super().setUpTestData() + + + +class ItemConfigGroupsTicketPermissionsAPI( + BaseItemTicketPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + + item_class: str = 'config_group' + + item_type = TicketLinkedItem.Modules.CONFIG_GROUP + + + @classmethod + def setUpTestData(self): + + from config_management.models.groups import ConfigGroups + + self.CreateOrg() + + self.linked_item = ConfigGroups.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = ConfigGroups.objects.create( + organization = self.organization, + name = 'two', + ) + + + super().setUpTestData() + + + +class ItemDeviceTicketPermissionsAPI( + BaseItemTicketPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + + item_class: str = 'device' + + item_type = TicketLinkedItem.Modules.DEVICE + + + @classmethod + def setUpTestData(self): + + from itam.models.device import Device + + self.CreateOrg() + + self.linked_item = Device.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = Device.objects.create( + organization = self.organization, + name = 'two', + ) + + super().setUpTestData() + + +class ItemOperatingSystemTicketPermissionsAPI( + BaseItemTicketPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + + item_class: str = 'operating_system' + + item_type = TicketLinkedItem.Modules.OPERATING_SYSTEM + + + @classmethod + def setUpTestData(self): + + from itam.models.operating_system import OperatingSystem + + self.CreateOrg() + + self.linked_item = OperatingSystem.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = OperatingSystem.objects.create( + organization = self.organization, + name = 'two', + ) + + + super().setUpTestData() + + + +class ItemServiceTicketPermissionsAPI( + BaseItemTicketPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + + item_class: str = 'service' + + item_type = TicketLinkedItem.Modules.SERVICE + + + @classmethod + def setUpTestData(self): + + from itim.models.services import Service + + self.CreateOrg() + + self.linked_item = Service.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = Service.objects.create( + organization = self.organization, + name = 'two', + ) + + + super().setUpTestData() + + + +class ItemSoftwareTicketPermissionsAPI( + BaseItemTicketPermissionsAPI, + TestCase +): + """ Test Cases common to ALL ticket types """ + + + item_class: str = 'software' + + item_type = TicketLinkedItem.Modules.SOFTWARE + + + @classmethod + def setUpTestData(self): + + from itam.models.software import Software + + self.CreateOrg() + + self.linked_item = Software.objects.create( + organization = self.organization, + name = 'one', + ) + + + self.linked_item_two = Software.objects.create( + organization = self.organization, + name = 'two', + ) + + + super().setUpTestData() + + From 7d3a4c7c6300cbdc8d1a72cb02812e7111663014 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 16:29:39 +0930 Subject: [PATCH 418/617] test(core): Item Ticket API v2 Serializer checks ref: #15 #248 #368 #374 --- .../test_ticket_linked_item_serializer.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py index 871347763..5eccab59b 100644 --- a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py +++ b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item_serializer.py @@ -92,6 +92,26 @@ class MockView: + def test_serializer_validation_add_valid_item_related_ticket(self): + """Serializer Validation Check + + Ensure that a valid item it does not raise a validation error + when adding a ticket as related to an item + """ + + serializer = TicketLinkedItemModelSerializer( + data={ + "organization": self.organization.id, + "ticket": self.ticket.id, + "item_type": int(TicketLinkedItem.Modules.DEVICE), + "item": self.device_two.id, + } + ) + + assert serializer.is_valid(raise_exception = True) + + + def test_serializer_validation_no_ticket(self): """Serializer Validation Check @@ -111,7 +131,7 @@ def test_serializer_validation_no_ticket(self): serializer.is_valid(raise_exception = True) - assert err.value.get_codes()['ticket'] == 'required' + assert err.value.get_codes()['ticket'][0] == 'required' From 4acfe5f3133d8119f4c903aba74c75fb4b92e8de Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:28:46 +0930 Subject: [PATCH 419/617] test(core): fix broken tests from 8b701785b3489db567f5ae08c58e28ae76529881 changes ref: #15 #248 #368 #374 --- .../test_organizaiton_permission_api.py | 6 +-- .../unit/test_history/test_history_viewset.py | 20 +++---- .../app_settings/test_app_settings_viewset.py | 53 +------------------ .../test_user_settings_viewset.py | 4 +- 4 files changed, 15 insertions(+), 68 deletions(-) diff --git a/app/access/tests/unit/organization/test_organizaiton_permission_api.py b/app/access/tests/unit/organization/test_organizaiton_permission_api.py index 6f3bb582f..eccb3c559 100644 --- a/app/access/tests/unit/organization/test_organizaiton_permission_api.py +++ b/app/access/tests/unit/organization/test_organizaiton_permission_api.py @@ -204,7 +204,7 @@ def test_add_is_prohibited_diff_org_user(self): client.force_login(self.different_organization_user) response = client.post(url, data={'name': 'should not create'}, content_type='application/json') - assert response.status_code == 403 + assert response.status_code == 405 def test_add_is_prohibited_super_user(self): @@ -220,7 +220,7 @@ def test_add_is_prohibited_super_user(self): client.force_login(self.super_user) response = client.post(url, data={'name': 'should not create'}, content_type='application/json') - assert response.status_code == 403 + assert response.status_code == 405 def test_add_is_prohibited_user_same_org(self): @@ -236,4 +236,4 @@ def test_add_is_prohibited_user_same_org(self): client.force_login(self.add_user) response = client.post(url, data={'name': 'should not create'}, content_type='application/json') - assert response.status_code == 403 + assert response.status_code == 405 diff --git a/app/core/tests/unit/test_history/test_history_viewset.py b/app/core/tests/unit/test_history/test_history_viewset.py index db8d7adf9..fcef00baa 100644 --- a/app/core/tests/unit/test_history/test_history_viewset.py +++ b/app/core/tests/unit/test_history/test_history_viewset.py @@ -11,7 +11,7 @@ from access.models import Organization, Team, TeamUsers, Permission -from api.tests.abstract.api_permissions_viewset import APIPermissions +from api.tests.abstract.api_permissions_viewset import APIPermissionView from core.models.history import History @@ -19,7 +19,7 @@ -class HistoryPermissionsAPI(TestCase, APIPermissions): +class HistoryPermissionsAPI(APIPermissionView, TestCase): model = History @@ -27,7 +27,7 @@ class HistoryPermissionsAPI(TestCase, APIPermissions): url_name = '_api_v2_model_history' - # change_data = {'name': 'device'} + change_data = {'name': 'device'} delete_data = {} @@ -253,7 +253,7 @@ def test_view_has_permission(self): assert response.status_code == 403 - def test_add_has_permission(self): + def test_add_has_permission_method_not_allowed(self): """ Check correct permission for add Custom permission of test case with same name. @@ -275,10 +275,11 @@ def test_add_has_permission(self): client.force_login(self.add_user) response = client.post(url, data=self.add_data) - assert response.status_code == 403 + assert response.status_code == 405 + - def test_change_has_permission(self): + def test_change_has_permission_method_not_allowed(self): """ Check correct permission for change Custom permission of test case with same name. @@ -294,10 +295,10 @@ def test_change_has_permission(self): client.force_login(self.change_user) response = client.patch(url, data=self.change_data, content_type='application/json') - assert response.status_code == 403 + assert response.status_code == 405 - def test_delete_has_permission(self): + def test_delete_has_permission_method_not_allowed(self): """ Check correct permission for delete Custom permission of test case with same name. @@ -313,5 +314,4 @@ def test_delete_has_permission(self): client.force_login(self.delete_user) response = client.delete(url, data=self.delete_data) - assert response.status_code == 403 - + assert response.status_code == 405 diff --git a/app/settings/tests/unit/app_settings/test_app_settings_viewset.py b/app/settings/tests/unit/app_settings/test_app_settings_viewset.py index dd6e0830a..c4fb4fcf1 100644 --- a/app/settings/tests/unit/app_settings/test_app_settings_viewset.py +++ b/app/settings/tests/unit/app_settings/test_app_settings_viewset.py @@ -10,7 +10,6 @@ from api.tests.abstract.api_permissions_viewset import ( APIPermissionChange, - APIPermissionDelete, APIPermissionView ) @@ -22,7 +21,6 @@ class AppSettingsPermissionsAPI( TestCase, # APIPermissions APIPermissionChange, - APIPermissionDelete, APIPermissionView ): @@ -215,53 +213,4 @@ def test_delete_has_permission(self): client.force_login(self.delete_user) response = client.delete(url, data=self.delete_data) - assert response.status_code == 403 - - - # def test_view_has_permission(self): - # """ Check correct permission for view - - # Attempt to view as user with view permission - # """ - - # client = Client() - # url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) - - - # client.force_login(self.view_user) - # response = client.get(url) - - # assert response.status_code == 403 - - - # def test_change_has_permission(self): - # """ Check correct permission for change - - # Make change with user who has change permission - # """ - - # client = Client() - # url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) - - - # client.force_login(self.change_user) - # response = client.patch(url, data=self.change_data, content_type='application/json') - - # assert response.status_code == 403 - - - # def test_delete_has_permission(self): - # """ Check correct permission for delete - - # Delete item as user with delete permission - # """ - - # client = Client() - # url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=self.url_view_kwargs) - - - # client.force_login(self.delete_user) - # response = client.delete(url, data=self.delete_data) - - # assert response.status_code == 403 - + assert response.status_code == 405 diff --git a/app/settings/tests/unit/user_settings/test_user_settings_viewset.py b/app/settings/tests/unit/user_settings/test_user_settings_viewset.py index 85044dd8a..36fc52e5b 100644 --- a/app/settings/tests/unit/user_settings/test_user_settings_viewset.py +++ b/app/settings/tests/unit/user_settings/test_user_settings_viewset.py @@ -10,7 +10,6 @@ from api.tests.abstract.api_permissions_viewset import ( APIPermissionChange, - APIPermissionDelete, APIPermissionView ) @@ -20,7 +19,6 @@ class UserSettingsPermissionsAPI( TestCase, APIPermissionChange, - APIPermissionDelete, APIPermissionView ): @@ -214,7 +212,7 @@ def test_delete_has_permission(self): client.force_login(self.delete_user) response = client.delete(url, data=self.delete_data) - assert response.status_code == 403 + assert response.status_code == 405 From e39ec70236720245e5105b31453ffb9258aaae9b Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:37:30 +0930 Subject: [PATCH 420/617] fix(core): Ensure triage and import permissions are catered for Tickets ref: #248 #368 #374 --- app/core/viewsets/ticket.py | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 0649c605c..18aadcf91 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -1,6 +1,8 @@ from api.exceptions import UnknownTicketType from api.viewsets.common import ModelViewSet +from access.models import Organization + from assistance.serializers.request import ( RequestAddTicketModelSerializer, RequestChangeTicketModelSerializer, @@ -49,10 +51,46 @@ class TicketViewSet(ModelViewSet): def get_dynamic_permissions(self): + organization = None + + + if( + self.action == 'create' + or self.action == 'partial_update' + or self.action == 'update' + ): + + if 'organization' in self.request.data: + + organization = Organization.objects.get( + pk = int(self.request.data['organization']) + ) + + elif( + self.action == 'partial_update' + or self.action == 'update' + ): + + obj = list(self.queryset)[0] + + organization = obj.organization + if self.action == 'create': action_keyword = 'add' + if organization: + + if self.has_organization_permission( + organization = organization.id, + permissions_required = [ + str('core.import_ticket_' + self._ticket_type).lower() + ] + ): + + action_keyword = 'import' + + elif self.action == 'destroy': action_keyword = 'delete' @@ -65,6 +103,18 @@ def get_dynamic_permissions(self): action_keyword = 'change' + if organization: + + if self.has_organization_permission( + organization = organization.id, + permissions_required = [ + str('core.triage_ticket_' + self._ticket_type).lower() + ] + ): + + action_keyword = 'triage' + + elif self.action == 'retrieve': action_keyword = 'view' @@ -73,6 +123,16 @@ def get_dynamic_permissions(self): action_keyword = 'change' + if self.has_organization_permission( + organization = organization.id, + permissions_required = [ + str('core.triage_ticket_' + self._ticket_type).lower() + ] + ): + + action_keyword = 'triage' + + elif self.action is None: action_keyword = 'view' From dac01ace32f5b4472b24a333e5d23947f1391a3e Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:38:26 +0930 Subject: [PATCH 421/617] refactor(core): Move ticket validation from is_valid -> validate method ref: #248 #368 #374 --- app/core/serializers/ticket.py | 26 +++++++------------ .../tests/abstract/test_ticket_viewset.py | 22 +++++++++++++++- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index d7d28710d..3dffb83da 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -137,33 +137,27 @@ class Meta: ] - def is_valid(self, *, raise_exception=False): + def validate(self, data): - is_valid: bool = False - - is_valid = super().is_valid(raise_exception=raise_exception) - - try: - - self.validated_data['ticket_type'] = self._context['view']._ticket_type_id + if 'view' in self._context: - except: + if self._context['view'].action == 'create': - is_valid = False + if hasattr(self._context['view'], 'request'): - raise UnknownTicketType() + data['opened_by_id'] = self._context['view'].request.user.id - if 'view' in self._context: + if hasattr(self._context['view'], '_ticket_type_id'): - if self._context['view'].action == 'create': + data['ticket_type'] = self._context['view']._ticket_type_id - if hasattr(self._context['view'], 'request'): + else: - self.validated_data['opened_by_id'] = self._context['view'].request.user.id + raise UnknownTicketType() - return is_valid + return data diff --git a/app/core/tests/abstract/test_ticket_viewset.py b/app/core/tests/abstract/test_ticket_viewset.py index bbb637091..c4667d38b 100644 --- a/app/core/tests/abstract/test_ticket_viewset.py +++ b/app/core/tests/abstract/test_ticket_viewset.py @@ -220,6 +220,12 @@ def setUpTestData(self): user = self.triage_user ) + user_settings = UserSettings.objects.get(user=self.triage_user) + + user_settings.default_organization = self.organization + + user_settings.save() + self.import_user = User.objects.create_user(username="test_user_import", password="password") teamuser = TeamUsers.objects.create( @@ -228,6 +234,14 @@ def setUpTestData(self): ) + user_settings = UserSettings.objects.get(user=self.import_user) + + user_settings.default_organization = self.organization + + user_settings.save() + + + self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password") @@ -290,8 +304,14 @@ def test_add_has_permission_import_user(self): url = reverse(self.app_namespace + ':' + self.url_name + '-list') + data = self.add_data.copy() + + data.update({ + 'opened_by': self.import_user.id + }) + client.force_login(self.import_user) - response = client.post(url, data=self.add_data) + response = client.post(url, data=data) assert response.status_code == 201 From df55cf04509e31163ef9bc7a2c2b3ecbe8841f78 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:46:23 +0930 Subject: [PATCH 422/617] feat(api): Depreciate v1 API Endpoint Ticket Categories ref: #248 #345 #374 --- app/api/views/core/ticket_categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/core/ticket_categories.py b/app/api/views/core/ticket_categories.py index d0c5d3727..2f5fb27c0 100644 --- a/app/api/views/core/ticket_categories.py +++ b/app/api/views/core/ticket_categories.py @@ -10,7 +10,7 @@ from api.views.mixin import OrganizationPermissionAPI - +@extend_schema(deprecated=True) class View(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ From 91e38a80f7b02d9f5acb48d32e67d656fb058840 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:46:33 +0930 Subject: [PATCH 423/617] feat(api): Depreciate v1 API Endpoint Ticket Comment Categories ref: #248 #345 #374 --- app/api/views/core/ticket_comment_categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/core/ticket_comment_categories.py b/app/api/views/core/ticket_comment_categories.py index 94cf32211..409329c21 100644 --- a/app/api/views/core/ticket_comment_categories.py +++ b/app/api/views/core/ticket_comment_categories.py @@ -10,7 +10,7 @@ from api.views.mixin import OrganizationPermissionAPI - +@extend_schema(deprecated=True) class View(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ From 8e21cb5a859eab0aa406479d423246870c0048fa Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:46:42 +0930 Subject: [PATCH 424/617] feat(api): Depreciate v1 API Endpoint Ticket Comments ref: #248 #345 #374 --- app/api/views/core/ticket_comments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/core/ticket_comments.py b/app/api/views/core/ticket_comments.py index 24ade4863..caa6ce155 100644 --- a/app/api/views/core/ticket_comments.py +++ b/app/api/views/core/ticket_comments.py @@ -12,7 +12,7 @@ from core.models.ticket.ticket_comment import TicketComment - +@extend_schema(deprecated=True) class View(OrganizationMixin, viewsets.ModelViewSet): permission_classes = [ From 8cd442ea254e474d0d5cc9345079f16c84f9cf51 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:48:38 +0930 Subject: [PATCH 425/617] feat(api): Depreciate v1 API Endpoint Assistance ref: #248 #345 #374 --- app/api/views/assistance/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/views/assistance/index.py b/app/api/views/assistance/index.py index 983af1bd2..20cb4a0dc 100644 --- a/app/api/views/assistance/index.py +++ b/app/api/views/assistance/index.py @@ -7,7 +7,7 @@ from rest_framework.reverse import reverse - +@extend_schema(deprecated=True) class Index(views.APIView): permission_classes = [ From a04cfeef86c09ee3c1ac89214f2daf1a4a91bfb3 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:48:48 +0930 Subject: [PATCH 426/617] feat(api): Depreciate v1 API Endpoint Request Ticket ref: #248 #345 #374 --- app/api/views/assistance/request_ticket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/views/assistance/request_ticket.py b/app/api/views/assistance/request_ticket.py index ca83fe599..c71aeddb1 100644 --- a/app/api/views/assistance/request_ticket.py +++ b/app/api/views/assistance/request_ticket.py @@ -4,6 +4,7 @@ from api.views.core.tickets import View +@extend_schema(deprecated=True) class View(View): _ticket_type:str = 'request' From 429f3a9a944de7e3ce8a9f99c2a7a9b246f4bc0a Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:49:21 +0930 Subject: [PATCH 427/617] docs(views): update to denote dynamic permissions ref: #248 #345 #374 --- .../centurion_erp/development/views.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 9e7e4ad5a..0ee6066fe 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -35,6 +35,32 @@ Views are used with Centurion ERP to Fetch the data for rendering. - Model VieSets must be tested against tests `from api.tests.abstract.viewsets import ViewSetModel` +## Permissions + +If you wish to deviate from the standard CRUD permissions, define a function called `get_dynamic_permissions` within the `view`/`ViewSet`. The function must return a list of permissions. This is useful if you have added additional permissions to a model. + +Example of the function `get_dynamic_permissions` + +``` py + + +def get_dynamic_permissions(self): + + if self.action == 'create': + + self.permission_required = [ + 'core.random_permission_name', + ] + + else: + + raise ValueError('unable to determine the action_keyword') + + return super().get_permission_required() + +``` + + ## Pre v1.3 Docs !!! warning From d20a1460da621ad6774737f0575f64327fcdf7ad Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:52:25 +0930 Subject: [PATCH 428/617] feat(api): Depreciate v1 API Endpoint Assistance ref: #248 #345 #374 --- app/api/views/assistance/index.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/views/assistance/index.py b/app/api/views/assistance/index.py index 20cb4a0dc..9dbcb63f4 100644 --- a/app/api/views/assistance/index.py +++ b/app/api/views/assistance/index.py @@ -1,5 +1,7 @@ from django.utils.safestring import mark_safe +from drf_spectacular.utils import extend_schema + from rest_framework import generics, permissions, routers, views # from rest_framework.decorators import api_view from rest_framework.permissions import IsAuthenticated From 223e78ae07cff2a33dfee2fbd30c4624a7e24734 Mon Sep 17 00:00:00 2001 From: Jon Date: Fri, 1 Nov 2024 18:55:05 +0930 Subject: [PATCH 429/617] docs(views): update to denote dynamic permissions ref: #248 #345 #374 --- docs/projects/centurion_erp/development/views.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/projects/centurion_erp/development/views.md b/docs/projects/centurion_erp/development/views.md index 0ee6066fe..4070a55c6 100644 --- a/docs/projects/centurion_erp/development/views.md +++ b/docs/projects/centurion_erp/development/views.md @@ -37,13 +37,12 @@ Views are used with Centurion ERP to Fetch the data for rendering. ## Permissions -If you wish to deviate from the standard CRUD permissions, define a function called `get_dynamic_permissions` within the `view`/`ViewSet`. The function must return a list of permissions. This is useful if you have added additional permissions to a model. +If you wish to deviate from the standard CRUD permissions, define a function called `get_dynamic_permissions` within the `view`/`ViewSet`. The function must return a list of permissions. This is useful if you have added additional permissions to a model. Example of the function `get_dynamic_permissions` ``` py - def get_dynamic_permissions(self): if self.action == 'create': From 189b81106d73cbfa60f7f2963061f2d963d090ef Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 14:13:52 +0930 Subject: [PATCH 430/617] feat(itim): Add Change Ticket API v2 endpoint ref: #248 #377 --- app/api/react_ui_metadata.py | 5 + app/api/urls_v2.py | 1 + app/api/views/itim/change_ticket.py | 2 +- app/core/viewsets/ticket.py | 10 ++ app/itim/serializers/change.py | 225 ++++++++++++++++++++++++++++ app/itim/viewsets/change.py | 83 ++++++++++ app/itim/viewsets/index.py | 2 +- 7 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 app/itim/serializers/change.py create mode 100644 app/itim/viewsets/change.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 6a4a06768..5ee3b1082 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -156,6 +156,11 @@ def determine_metadata(self, request, view): "display_name": "ITIM", "name": "itim", "pages": [ + { + "display_name": "Changes", + "name": "chanage_ticket", + "link": "/itim/ticket/change" + }, { "display_name": "Clusters", "name": "cluster", diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 8e37ec682..5feff6c09 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -63,6 +63,7 @@ from itim.viewsets import ( index as itim_v2, + change, cluster as cluster_v2, cluster_type as cluster_type_v2, port as port_v2, diff --git a/app/api/views/itim/change_ticket.py b/app/api/views/itim/change_ticket.py index 4375956b8..006b93be5 100644 --- a/app/api/views/itim/change_ticket.py +++ b/app/api/views/itim/change_ticket.py @@ -5,7 +5,7 @@ from api.views.core.tickets import View - +@extend_schema(deprecated=True) class View(View): _ticket_type:str = 'change' diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 18aadcf91..77dc87524 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -16,9 +16,19 @@ Ticket, ) +from itim.serializers.change import ( + ChangeAddTicketModelSerializer, + ChangeChangeTicketModelSerializer, + ChangeImportTicketModelSerializer, + ChangeTriageTicketModelSerializer, + ChangeTicketModelSerializer, + ChangeTicketViewSerializer, +) + from settings.models.user_settings import UserSettings + class TicketViewSet(ModelViewSet): filterset_fields = [ diff --git a/app/itim/serializers/change.py b/app/itim/serializers/change.py new file mode 100644 index 000000000..aa3377176 --- /dev/null +++ b/app/itim/serializers/change.py @@ -0,0 +1,225 @@ +from rest_framework import serializers + +from app.serializers.user import UserBaseSerializer + +from core.serializers.ticket import ( + Ticket, + TicketBaseSerializer, + TicketModelSerializer, + TicketViewSerializer +) + + + +class ChangeTicketBaseSerializer( + TicketBaseSerializer +): + + class Meta( TicketBaseSerializer.Meta ): + + pass + + + +class ChangeTicketModelSerializer( + ChangeTicketBaseSerializer, + TicketModelSerializer, +): + + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Change]) + + class Meta( TicketModelSerializer.Meta ): + + fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'title', + 'description', + 'estimate', + 'duration', + 'urgency', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'external_ref', + 'external_system', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class ChangeAddTicketModelSerializer( + ChangeTicketModelSerializer, +): + """Serializer for `Add` user + + Args: + ChangeTicketModelSerializer (class): Model Serializer + """ + + + class Meta(ChangeTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class ChangeChangeTicketModelSerializer( + ChangeTicketModelSerializer, +): + """Serializer for `Change` user + + Args: + ChangeTicketModelSerializer (class): Change Model Serializer + """ + + class Meta(ChangeTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class ChangeTriageTicketModelSerializer( + ChangeTicketModelSerializer, +): + """Serializer for `Triage` user + + Args: + ChangeTicketModelSerializer (class): Change Model Serializer + """ + + + class Meta(ChangeTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'created', + 'modified', + 'status_badge', + 'estimate', + 'duration', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + '_urls', + ] + + + +class ChangeImportTicketModelSerializer( + ChangeTicketModelSerializer, +): + """Serializer for `Import` user + + Args: + ChangeTicketModelSerializer (class): Change Model Serializer + """ + + class Meta(ChangeTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'display_name', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class ChangeTicketViewSerializer( + ChangeTicketModelSerializer, + TicketViewSerializer, +): + + pass diff --git a/app/itim/viewsets/change.py b/app/itim/viewsets/change.py new file mode 100644 index 000000000..c6970da5a --- /dev/null +++ b/app/itim/viewsets/change.py @@ -0,0 +1,83 @@ +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiResponse, + PolymorphicProxySerializer, +) + +from itim.serializers.change import ( + ChangeAddTicketModelSerializer, + ChangeChangeTicketModelSerializer, + ChangeImportTicketModelSerializer, + ChangeTriageTicketModelSerializer, + ChangeTicketViewSerializer, +) + +from core.viewsets.ticket import TicketViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a Change Ticket', + description='', + request = PolymorphicProxySerializer( + component_name = 'ChangeTicket', + serializers=[ + ChangeImportTicketModelSerializer, + ChangeAddTicketModelSerializer, + ChangeChangeTicketModelSerializer, + ChangeTriageTicketModelSerializer, + ], + resource_type_field_name=None, + many = False + ), + responses = { + 201: OpenApiResponse(description='Created', response=ChangeTicketViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a Change Ticket', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all Change Tickets', + description='', + responses = { + 200: OpenApiResponse(description='', response=ChangeTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a Change Ticket', + description='', + responses = { + 200: OpenApiResponse(description='', response=ChangeTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a Change Ticket', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ChangeTicketViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(TicketViewSet): + """Change Ticket + + This class exists only for the purpose of swagger for documentation. + + Args: + TicketViewSet (class): Base Ticket ViewSet. + """ + + _ticket_type: str = 'Change' diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py index a00e2d1c6..b6e7be71a 100644 --- a/app/itim/viewsets/index.py +++ b/app/itim/viewsets/index.py @@ -25,7 +25,7 @@ def list(self, request, pk=None): return Response( { - "change": "ToDo", + "change": reverse('v2:_api_v2_ticket_change-list', request=request), "cluster": reverse('v2:_api_v2_cluster-list', request=request), "incident": "ToDo", "problem": "ToDo", From 73d7338e7af360d325c4d23ce84943df886c70b9 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 15:37:13 +0930 Subject: [PATCH 431/617] feat(itim): Add Incident Ticket API v2 endpoint ref: #248 #377 --- app/api/react_ui_metadata.py | 5 + app/api/urls_v2.py | 4 +- app/api/views/itim/incident_ticket.py | 1 + app/core/viewsets/ticket.py | 9 ++ app/itim/serializers/incident.py | 225 ++++++++++++++++++++++++++ app/itim/viewsets/incident.py | 83 ++++++++++ app/itim/viewsets/index.py | 2 +- 7 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 app/itim/serializers/incident.py create mode 100644 app/itim/viewsets/incident.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 5ee3b1082..4198134e6 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -166,6 +166,11 @@ def determine_metadata(self, request, view): "name": "cluster", "link": "/itim/cluster" }, + { + "display_name": "Incidents", + "name": "incident_ticket", + "link": "/itim/ticket/incident" + }, { "display_name": "Services", "name": "service", diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 5feff6c09..3e3baa05f 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -66,6 +66,7 @@ change, cluster as cluster_v2, cluster_type as cluster_type_v2, + incident, port as port_v2, service as service_v2, service_device as service_device_v2 @@ -142,6 +143,7 @@ router.register('itim', itim_v2.Index, basename='_api_v2_itim_home') router.register('itim/cluster', cluster_v2.ViewSet, basename='_api_v2_cluster') router.register('itim/cluster/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_notes') +router.register('itim/ticket/incident', incident.ViewSet, basename='_api_v2_ticket_incident') router.register('itim/service', service_v2.ViewSet, basename='_api_v2_service') router.register('itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') @@ -149,7 +151,7 @@ router.register('project_management', project_management_v2.Index, basename='_api_v2_project_management_home') router.register('project_management/project', project_v2.ViewSet, basename='_api_v2_project') router.register('project_management/project/(?P[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone') -router.register('itim/project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') +router.register('project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') router.register('settings', settings_index_v2.Index, basename='_api_v2_settings_home') diff --git a/app/api/views/itim/incident_ticket.py b/app/api/views/itim/incident_ticket.py index 3472a8024..05df54e5f 100644 --- a/app/api/views/itim/incident_ticket.py +++ b/app/api/views/itim/incident_ticket.py @@ -5,6 +5,7 @@ +@extend_schema(deprecated=True) class View(View): _ticket_type:str = 'incident' diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 77dc87524..e990cecee 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -25,6 +25,15 @@ ChangeTicketViewSerializer, ) +from itim.serializers.incident import ( + IncidentAddTicketModelSerializer, + IncidentChangeTicketModelSerializer, + IncidentImportTicketModelSerializer, + IncidentTriageTicketModelSerializer, + IncidentTicketModelSerializer, + IncidentTicketViewSerializer, +) + from settings.models.user_settings import UserSettings diff --git a/app/itim/serializers/incident.py b/app/itim/serializers/incident.py new file mode 100644 index 000000000..213eb2762 --- /dev/null +++ b/app/itim/serializers/incident.py @@ -0,0 +1,225 @@ +from rest_framework import serializers + +from app.serializers.user import UserBaseSerializer + +from core.serializers.ticket import ( + Ticket, + TicketBaseSerializer, + TicketModelSerializer, + TicketViewSerializer +) + + + +class IncidentTicketBaseSerializer( + TicketBaseSerializer +): + + class Meta( TicketBaseSerializer.Meta ): + + pass + + + +class IncidentTicketModelSerializer( + IncidentTicketBaseSerializer, + TicketModelSerializer, +): + + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Incident]) + + class Meta( TicketModelSerializer.Meta ): + + fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'title', + 'description', + 'estimate', + 'duration', + 'urgency', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'external_ref', + 'external_system', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class IncidentAddTicketModelSerializer( + IncidentTicketModelSerializer, +): + """Serializer for `Add` user + + Args: + IncidentTicketModelSerializer (class): Model Serializer + """ + + + class Meta(IncidentTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class IncidentChangeTicketModelSerializer( + IncidentTicketModelSerializer, +): + """Serializer for `Incident` user + + Args: + IncidentTicketModelSerializer (class): Incident Model Serializer + """ + + class Meta(IncidentTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class IncidentTriageTicketModelSerializer( + IncidentTicketModelSerializer, +): + """Serializer for `Triage` user + + Args: + IncidentTicketModelSerializer (class): Incident Model Serializer + """ + + + class Meta(IncidentTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'created', + 'modified', + 'status_badge', + 'estimate', + 'duration', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + '_urls', + ] + + + +class IncidentImportTicketModelSerializer( + IncidentTicketModelSerializer, +): + """Serializer for `Import` user + + Args: + IncidentTicketModelSerializer (class): Incident Model Serializer + """ + + class Meta(IncidentTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'display_name', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class IncidentTicketViewSerializer( + IncidentTicketModelSerializer, + TicketViewSerializer, +): + + pass diff --git a/app/itim/viewsets/incident.py b/app/itim/viewsets/incident.py new file mode 100644 index 000000000..32de50bc8 --- /dev/null +++ b/app/itim/viewsets/incident.py @@ -0,0 +1,83 @@ +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiResponse, + PolymorphicProxySerializer, +) + +from itim.serializers.incident import ( + IncidentAddTicketModelSerializer, + IncidentChangeTicketModelSerializer, + IncidentImportTicketModelSerializer, + IncidentTriageTicketModelSerializer, + IncidentTicketViewSerializer, +) + +from core.viewsets.ticket import TicketViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a Incident Ticket', + description='', + request = PolymorphicProxySerializer( + component_name = 'IncidentTicket', + serializers=[ + IncidentImportTicketModelSerializer, + IncidentAddTicketModelSerializer, + IncidentChangeTicketModelSerializer, + IncidentTriageTicketModelSerializer, + ], + resource_type_field_name=None, + many = False + ), + responses = { + 201: OpenApiResponse(description='Created', response=IncidentTicketViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a Incident Ticket', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all Incident Tickets', + description='', + responses = { + 200: OpenApiResponse(description='', response=IncidentTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a Incident Ticket', + description='', + responses = { + 200: OpenApiResponse(description='', response=IncidentTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a Incident Ticket', + description = '', + responses = { + 200: OpenApiResponse(description='', response=IncidentTicketViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(TicketViewSet): + """Incident Ticket + + This class exists only for the purpose of swagger for documentation. + + Args: + TicketViewSet (class): Base Ticket ViewSet. + """ + + _ticket_type: str = 'Incident' diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py index b6e7be71a..ea7912718 100644 --- a/app/itim/viewsets/index.py +++ b/app/itim/viewsets/index.py @@ -27,7 +27,7 @@ def list(self, request, pk=None): { "change": reverse('v2:_api_v2_ticket_change-list', request=request), "cluster": reverse('v2:_api_v2_cluster-list', request=request), - "incident": "ToDo", + "incident": reverse('v2:_api_v2_ticket_incident-list', request=request), "problem": "ToDo", "service": reverse('v2:_api_v2_service-list', request=request), } From 7fc5138fcd037e471b5379cbd4f76bcd65722ec5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 15:39:00 +0930 Subject: [PATCH 432/617] feat(itim): Add Problem Ticket API v2 endpoint ref: #248 #377 --- app/api/react_ui_metadata.py | 5 + app/api/urls_v2.py | 3 + app/api/views/itim/index.py | 3 + app/api/views/itim/problem_ticket.py | 1 + app/core/viewsets/ticket.py | 10 ++ app/itim/serializers/problem.py | 225 +++++++++++++++++++++++++++ app/itim/viewsets/index.py | 2 +- app/itim/viewsets/problem.py | 83 ++++++++++ 8 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 app/itim/serializers/problem.py create mode 100644 app/itim/viewsets/problem.py diff --git a/app/api/react_ui_metadata.py b/app/api/react_ui_metadata.py index 4198134e6..f2777e4e9 100644 --- a/app/api/react_ui_metadata.py +++ b/app/api/react_ui_metadata.py @@ -171,6 +171,11 @@ def determine_metadata(self, request, view): "name": "incident_ticket", "link": "/itim/ticket/incident" }, + { + "display_name": "Problems", + "name": "problem_ticket", + "link": "/itim/ticket/problem" + }, { "display_name": "Services", "name": "service", diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 3e3baa05f..0163afd91 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -68,6 +68,7 @@ cluster_type as cluster_type_v2, incident, port as port_v2, + problem, service as service_v2, service_device as service_device_v2 ) @@ -141,9 +142,11 @@ router.register('itim', itim_v2.Index, basename='_api_v2_itim_home') +router.register('itim/ticket/change', change.ViewSet, basename='_api_v2_ticket_change') router.register('itim/cluster', cluster_v2.ViewSet, basename='_api_v2_cluster') router.register('itim/cluster/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_cluster_notes') router.register('itim/ticket/incident', incident.ViewSet, basename='_api_v2_ticket_incident') +router.register('itim/ticket/problem', problem.ViewSet, basename='_api_v2_ticket_problem') router.register('itim/service', service_v2.ViewSet, basename='_api_v2_service') router.register('itim/service/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_service_notes') diff --git a/app/api/views/itim/index.py b/app/api/views/itim/index.py index feb70477a..ce433dd64 100644 --- a/app/api/views/itim/index.py +++ b/app/api/views/itim/index.py @@ -1,5 +1,7 @@ from django.utils.safestring import mark_safe +from drf_spectacular.utils import extend_schema + from rest_framework import views from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -7,6 +9,7 @@ +@extend_schema(deprecated=True) class Index(views.APIView): permission_classes = [ diff --git a/app/api/views/itim/problem_ticket.py b/app/api/views/itim/problem_ticket.py index 2e428eea5..360f4ef7d 100644 --- a/app/api/views/itim/problem_ticket.py +++ b/app/api/views/itim/problem_ticket.py @@ -5,6 +5,7 @@ +@extend_schema(deprecated=True) class View(View): _ticket_type:str = 'problem' diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index e990cecee..a05369f6c 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -34,6 +34,16 @@ IncidentTicketViewSerializer, ) +from itim.serializers.problem import ( + ProblemAddTicketModelSerializer, + ProblemChangeTicketModelSerializer, + ProblemImportTicketModelSerializer, + ProblemTriageTicketModelSerializer, + ProblemTicketModelSerializer, + ProblemTicketViewSerializer, +) + + from settings.models.user_settings import UserSettings diff --git a/app/itim/serializers/problem.py b/app/itim/serializers/problem.py new file mode 100644 index 000000000..2200af278 --- /dev/null +++ b/app/itim/serializers/problem.py @@ -0,0 +1,225 @@ +from rest_framework import serializers + +from app.serializers.user import UserBaseSerializer + +from core.serializers.ticket import ( + Ticket, + TicketBaseSerializer, + TicketModelSerializer, + TicketViewSerializer +) + + + +class ProblemTicketBaseSerializer( + TicketBaseSerializer +): + + class Meta( TicketBaseSerializer.Meta ): + + pass + + + +class ProblemTicketModelSerializer( + ProblemTicketBaseSerializer, + TicketModelSerializer, +): + + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Problem]) + + class Meta( TicketModelSerializer.Meta ): + + fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'title', + 'description', + 'estimate', + 'duration', + 'urgency', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'external_ref', + 'external_system', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class ProblemAddTicketModelSerializer( + ProblemTicketModelSerializer, +): + """Serializer for `Add` user + + Args: + ProblemTicketModelSerializer (class): Model Serializer + """ + + + class Meta(ProblemTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class ProblemChangeTicketModelSerializer( + ProblemTicketModelSerializer, +): + """Serializer for `Problem` user + + Args: + ProblemTicketModelSerializer (class): Problem Model Serializer + """ + + class Meta(ProblemTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class ProblemTriageTicketModelSerializer( + ProblemTicketModelSerializer, +): + """Serializer for `Triage` user + + Args: + ProblemTicketModelSerializer (class): Problem Model Serializer + """ + + + class Meta(ProblemTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'created', + 'modified', + 'status_badge', + 'estimate', + 'duration', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + '_urls', + ] + + + +class ProblemImportTicketModelSerializer( + ProblemTicketModelSerializer, +): + """Serializer for `Import` user + + Args: + ProblemTicketModelSerializer (class): Problem Model Serializer + """ + + class Meta(ProblemTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'display_name', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class ProblemTicketViewSerializer( + ProblemTicketModelSerializer, + TicketViewSerializer, +): + + pass diff --git a/app/itim/viewsets/index.py b/app/itim/viewsets/index.py index ea7912718..9a0b840ab 100644 --- a/app/itim/viewsets/index.py +++ b/app/itim/viewsets/index.py @@ -28,7 +28,7 @@ def list(self, request, pk=None): "change": reverse('v2:_api_v2_ticket_change-list', request=request), "cluster": reverse('v2:_api_v2_cluster-list', request=request), "incident": reverse('v2:_api_v2_ticket_incident-list', request=request), - "problem": "ToDo", + "problem": reverse('v2:_api_v2_ticket_problem-list', request=request), "service": reverse('v2:_api_v2_service-list', request=request), } ) diff --git a/app/itim/viewsets/problem.py b/app/itim/viewsets/problem.py new file mode 100644 index 000000000..42170496f --- /dev/null +++ b/app/itim/viewsets/problem.py @@ -0,0 +1,83 @@ +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiResponse, + PolymorphicProxySerializer, +) + +from itim.serializers.problem import ( + ProblemAddTicketModelSerializer, + ProblemChangeTicketModelSerializer, + ProblemImportTicketModelSerializer, + ProblemTriageTicketModelSerializer, + ProblemTicketViewSerializer, +) + +from core.viewsets.ticket import TicketViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a Problem Ticket', + description='', + request = PolymorphicProxySerializer( + component_name = 'ProblemTicket', + serializers=[ + ProblemImportTicketModelSerializer, + ProblemAddTicketModelSerializer, + ProblemChangeTicketModelSerializer, + ProblemTriageTicketModelSerializer, + ], + resource_type_field_name=None, + many = False + ), + responses = { + 201: OpenApiResponse(description='Created', response=ProblemTicketViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a Problem Ticket', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all Problem Tickets', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProblemTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a Problem Ticket', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProblemTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a Problem Ticket', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ProblemTicketViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(TicketViewSet): + """Problem Ticket + + This class exists only for the purpose of swagger for documentation. + + Args: + TicketViewSet (class): Base Ticket ViewSet. + """ + + _ticket_type: str = 'Problem' From 74d55fb81e7544937a419cf901214eb17bd3fc58 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 16:12:09 +0930 Subject: [PATCH 433/617] feat(itim): Add Project Task API v2 endpoint ref: #248 #377 --- app/api/urls_v2.py | 2 + .../views/project_management/project_task.py | 1 + app/core/serializers/ticket.py | 46 +++- app/core/viewsets/ticket.py | 50 ++-- app/project_management/serializers/project.py | 8 +- .../serializers/project_task.py | 225 ++++++++++++++++++ .../viewsets/project_task.py | 83 +++++++ 7 files changed, 389 insertions(+), 26 deletions(-) create mode 100644 app/project_management/serializers/project_task.py create mode 100644 app/project_management/viewsets/project_task.py diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index 0163afd91..a5e8d78ce 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -78,6 +78,7 @@ project as project_v2, project_milestone as project_milestone_v2, project_state as project_state_v2, + project_task, project_type as project_type_v2, ) @@ -155,6 +156,7 @@ router.register('project_management/project', project_v2.ViewSet, basename='_api_v2_project') router.register('project_management/project/(?P[0-9]+)/milestone', project_milestone_v2.ViewSet, basename='_api_v2_project_milestone') router.register('project_management/project/(?P[0-9]+)/notes', notes_v2.ViewSet, basename='_api_v2_project_notes') +router.register('project_management/project/(?P[0-9]+)/project_task', project_task.ViewSet, basename='_api_v2_ticket_project_task') router.register('settings', settings_index_v2.Index, basename='_api_v2_settings_home') diff --git a/app/api/views/project_management/project_task.py b/app/api/views/project_management/project_task.py index 0f6bfd7a2..1def06af8 100644 --- a/app/api/views/project_management/project_task.py +++ b/app/api/views/project_management/project_task.py @@ -6,6 +6,7 @@ +@extend_schema(deprecated=True) class View(View): _ticket_type:str = 'project_task' diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 3dffb83da..ea1a5710a 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -29,12 +29,26 @@ def my_url(self, item): context = self.context.copy() - return reverse( - "v2:_api_v2_ticket_" + str(item.get_ticket_type_display()).lower() + "-detail", - request=context['view'].request, - kwargs={ + ticket_type = str(item.get_ticket_type_display()).lower().replace(' ', '_') + + if ticket_type == 'project_task': + + kwargs: dict = { + 'project_id': item.project.id, + 'pk': item.pk + } + + else: + + kwargs: dict = { 'pk': item.pk } + + + return reverse( + "v2:_api_v2_ticket_" + ticket_type + "-detail", + request=context['view'].request, + kwargs = kwargs ) @@ -65,13 +79,29 @@ def get_url(self, item): context = self.context.copy() + context = self.context.copy() + + ticket_type = str(item.get_ticket_type_display()).lower().replace(' ', '_') + + if ticket_type == 'project_task': + + kwargs: dict = { + 'project_id': item.project.id, + 'pk': item.pk + } + + else: + + kwargs: dict = { + 'pk': item.pk + } + + return { '_self': reverse( - "v2:_api_v2_ticket_" + str(item.get_ticket_type_display()).lower() + "-detail", + "v2:_api_v2_ticket_" + ticket_type + "-detail", request=context['view'].request, - kwargs={ - 'pk': item.pk - } + kwargs = kwargs ), 'comments': reverse('v2:_api_v2_ticket_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index a05369f6c..4c4dc9386 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -43,6 +43,14 @@ ProblemTicketViewSerializer, ) +from project_management.serializers.project_task import ( + ProjectTaskAddTicketModelSerializer, + ProjectTaskChangeTicketModelSerializer, + ProjectTaskImportTicketModelSerializer, + ProjectTaskTriageTicketModelSerializer, + ProjectTaskTicketModelSerializer, + ProjectTaskTicketViewSerializer, +) from settings.models.user_settings import UserSettings @@ -181,9 +189,18 @@ def get_queryset(self): self.get_ticket_type() - queryset = super().get_queryset().filter( - ticket_type = self._ticket_type_id - ) + if str(self._ticket_type).lower().replace(' ', '_') == 'project_task': + + queryset = super().get_queryset().filter( + project_id = int(self.kwargs['project_id']) + ) + + else: + + queryset = super().get_queryset().filter( + ticket_type = self._ticket_type_id + ) + self.queryset = queryset @@ -198,9 +215,9 @@ def get_ticket_type(self) -> None: ticket_types = [e for e in Ticket.TicketType] - for i in range( 1, len(ticket_types) ): + for i in range( 0, len(ticket_types) ): - if self._ticket_type.lower() == str(ticket_types[i - 1].label).lower(): + if self._ticket_type.lower() == str(ticket_types[i].label).lower(): ticket_type_id = i @@ -220,7 +237,7 @@ def get_serializer_class(self): self.get_ticket_type() - serializer_prefix = self._ticket_type + serializer_prefix = str(self._ticket_type).replace(' ', '') if ( @@ -262,48 +279,47 @@ def get_serializer_class(self): if self.has_organization_permission( organization = organization, permissions_required = [ - 'core.import_ticket_request' + 'core.import_ticket_' + str(self._ticket_type).lower().replace(' ', '_') ] ): - serializer_prefix = self._ticket_type + 'Import' + serializer_prefix = serializer_prefix + 'Import' elif self.has_organization_permission( organization = organization, permissions_required = [ - 'core.triage_ticket_request' + 'core.triage_ticket_' + str(self._ticket_type).lower().replace(' ', '_') ] ): - serializer_prefix = self._ticket_type + 'Triage' + serializer_prefix = serializer_prefix + 'Triage' elif self.has_organization_permission( organization = organization, permissions_required = [ - 'core.change_ticket_request' + 'core.change_ticket_' + str(self._ticket_type).lower().replace(' ', '_') ] ): - serializer_prefix = self._ticket_type + 'Change' + serializer_prefix = serializer_prefix + 'Change' elif self.has_organization_permission( organization = organization, permissions_required = [ - 'core.add_ticket_request' + 'core.add_ticket_' + str(self._ticket_type).lower().replace(' ', '_') ] ): - serializer_prefix = self._ticket_type + 'Add' + serializer_prefix = serializer_prefix + 'Add' elif self.has_organization_permission( organization = organization, permissions_required = [ - 'core.view_ticket_request' + 'core.view_ticket_' + str(self._ticket_type).lower().replace(' ', '_') ] ): - serializer_prefix = self._ticket_type + 'View' - + serializer_prefix = serializer_prefix + 'View' if ( diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 5a4aae5ac..46362880c 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -66,7 +66,13 @@ def get_url(self, item): ), 'milestone': reverse("v2:_api_v2_project_milestone-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), 'notes': reverse("v2:_api_v2_project_notes-list", request=self._context['view'].request, kwargs={'project_id': item.pk}), - 'tickets': 'ToDo' + 'tickets': reverse( + "v2:_api_v2_ticket_project_task-list", + request=self._context['view'].request, + kwargs={ + 'project_id': item.pk + } + ), } diff --git a/app/project_management/serializers/project_task.py b/app/project_management/serializers/project_task.py new file mode 100644 index 000000000..8930b769e --- /dev/null +++ b/app/project_management/serializers/project_task.py @@ -0,0 +1,225 @@ +from rest_framework import serializers + +from app.serializers.user import UserBaseSerializer + +from core.serializers.ticket import ( + Ticket, + TicketBaseSerializer, + TicketModelSerializer, + TicketViewSerializer +) + + + +class ProjectTaskTicketBaseSerializer( + TicketBaseSerializer +): + + class Meta( TicketBaseSerializer.Meta ): + + pass + + + +class ProjectTaskTicketModelSerializer( + ProjectTaskTicketBaseSerializer, + TicketModelSerializer, +): + + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.ProjectTask]) + + class Meta( TicketModelSerializer.Meta ): + + fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'title', + 'description', + 'estimate', + 'duration', + 'urgency', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + read_only_fields = [ + 'id', + 'display_name', + 'external_ref', + 'external_system', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class ProjectTaskAddTicketModelSerializer( + ProjectTaskTicketModelSerializer, +): + """Serializer for `Add` user + + Args: + ProjectTaskTicketModelSerializer (class): Model Serializer + """ + + + class Meta(ProjectTaskTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class ProjectTaskChangeTicketModelSerializer( + ProjectTaskTicketModelSerializer, +): + """Serializer for `ProjectTask` user + + Args: + ProjectTaskTicketModelSerializer (class): ProjectTask Model Serializer + """ + + class Meta(ProjectTaskTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'assigned_teams', + 'assigned_users', + 'category', + 'created', + 'modified', + 'status', + 'status_badge', + 'estimate', + 'duration', + 'impact', + 'priority', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + 'project', + 'milestone', + 'subscribed_teams', + 'subscribed_users', + '_urls', + ] + + + +class ProjectTaskTriageTicketModelSerializer( + ProjectTaskTicketModelSerializer, +): + """Serializer for `Triage` user + + Args: + ProjectTaskTicketModelSerializer (class): ProjectTask Model Serializer + """ + + + class Meta(ProjectTaskTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'created', + 'modified', + 'status_badge', + 'estimate', + 'duration', + 'external_ref', + 'external_system', + 'ticket_type', + 'is_deleted', + 'date_closed', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', + 'opened_by', + 'organization', + '_urls', + ] + + + +class ProjectTaskImportTicketModelSerializer( + ProjectTaskTicketModelSerializer, +): + """Serializer for `Import` user + + Args: + ProjectTaskTicketModelSerializer (class): ProjectTask Model Serializer + """ + + class Meta(ProjectTaskTicketModelSerializer.Meta): + + read_only_fields = [ + 'id', + 'display_name', + 'status_badge', + 'ticket_type', + '_urls', + ] + + + +class ProjectTaskTicketViewSerializer( + ProjectTaskTicketModelSerializer, + TicketViewSerializer, +): + + pass diff --git a/app/project_management/viewsets/project_task.py b/app/project_management/viewsets/project_task.py new file mode 100644 index 000000000..9e94f9396 --- /dev/null +++ b/app/project_management/viewsets/project_task.py @@ -0,0 +1,83 @@ +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiResponse, + PolymorphicProxySerializer, +) + +from project_management.serializers.project_task import ( + ProjectTaskAddTicketModelSerializer, + ProjectTaskChangeTicketModelSerializer, + ProjectTaskImportTicketModelSerializer, + ProjectTaskTriageTicketModelSerializer, + ProjectTaskTicketViewSerializer, +) + +from core.viewsets.ticket import TicketViewSet + + + +@extend_schema_view( + create=extend_schema( + summary = 'Create a Project Task', + description='', + request = PolymorphicProxySerializer( + component_name = 'ProjectTask', + serializers=[ + ProjectTaskImportTicketModelSerializer, + ProjectTaskAddTicketModelSerializer, + ProjectTaskChangeTicketModelSerializer, + ProjectTaskTriageTicketModelSerializer, + ], + resource_type_field_name=None, + many = False + ), + responses = { + 201: OpenApiResponse(description='Created', response=ProjectTaskTicketViewSerializer), + 403: OpenApiResponse(description='User is missing add permissions'), + } + ), + destroy = extend_schema( + summary = 'Delete a Project Task', + description = '', + responses = { + 204: OpenApiResponse(description=''), + 403: OpenApiResponse(description='User is missing delete permissions'), + } + ), + list = extend_schema( + summary = 'Fetch all Project Task', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectTaskTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + retrieve = extend_schema( + summary = 'Fetch a Project Task', + description='', + responses = { + 200: OpenApiResponse(description='', response=ProjectTaskTicketViewSerializer), + 403: OpenApiResponse(description='User is missing view permissions'), + } + ), + update = extend_schema(exclude = True), + partial_update = extend_schema( + summary = 'Update a Project Task', + description = '', + responses = { + 200: OpenApiResponse(description='', response=ProjectTaskTicketViewSerializer), + 403: OpenApiResponse(description='User is missing change permissions'), + } + ), +) +class ViewSet(TicketViewSet): + """Change Ticket + + This class exists only for the purpose of swagger for documentation. + + Args: + TicketViewSet (class): Base Ticket ViewSet. + """ + + _ticket_type: str = 'Project Task' From 74c6ee24cf601d15ec811832824cff494a86114f Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 17:14:45 +0930 Subject: [PATCH 434/617] fix(core): When obtaining ticket type use it's enum value ref: #248 #377 --- app/core/viewsets/ticket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 4c4dc9386..6a50816d5 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -219,7 +219,7 @@ def get_ticket_type(self) -> None: if self._ticket_type.lower() == str(ticket_types[i].label).lower(): - ticket_type_id = i + ticket_type_id = int(ticket_types[i]) break From 9908253a7e92d3c968b40b30ba88cef1713b93ff Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 17:35:45 +0930 Subject: [PATCH 435/617] fix(core): Add Ticket Category URL to all Ticket Types ref: #248 #377 --- app/api/serializers/core/ticket.py | 3 +- app/assistance/serializers/request.py | 11 ++++++- app/core/serializers/ticket.py | 33 +++++++++++++------ app/core/viewsets/ticket_category.py | 5 +++ app/itim/serializers/change.py | 13 +++++++- app/itim/serializers/incident.py | 11 ++++++- app/itim/serializers/problem.py | 12 ++++++- .../serializers/project_task.py | 12 ++++++- 8 files changed, 84 insertions(+), 16 deletions(-) diff --git a/app/api/serializers/core/ticket.py b/app/api/serializers/core/ticket.py index 877f6f040..8c8530ba7 100644 --- a/app/api/serializers/core/ticket.py +++ b/app/api/serializers/core/ticket.py @@ -5,6 +5,7 @@ from api.serializers.core.ticket_comment import TicketCommentSerializer +from core import exceptions as centurion_exception from core.forms.validate_ticket import TicketValidation from core.models.ticket.ticket import Ticket @@ -188,7 +189,7 @@ def is_valid(self, *, raise_exception=True) -> bool: except Exception as unhandled_exception: - serializers.ParseError( + centurion_exception.ParseError( detail=f"Server encountered an error during validation, Traceback: {unhandled_exception.with_traceback}" ) diff --git a/app/assistance/serializers/request.py b/app/assistance/serializers/request.py index 9c89ec27d..af0c399d6 100644 --- a/app/assistance/serializers/request.py +++ b/app/assistance/serializers/request.py @@ -2,6 +2,7 @@ from app.serializers.user import UserBaseSerializer +from core.models.ticket.ticket_category import TicketCategory from core.serializers.ticket import ( Ticket, TicketBaseSerializer, @@ -26,6 +27,14 @@ class RequestTicketModelSerializer( TicketModelSerializer, ): + + category = serializers.PrimaryKeyRelatedField( + queryset = TicketCategory.objects.filter( + request = True + ), + required = False + ) + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Request]) class Meta( TicketModelSerializer.Meta ): @@ -217,8 +226,8 @@ class Meta(RequestTicketModelSerializer.Meta): class RequestTicketViewSerializer( - RequestTicketModelSerializer, TicketViewSerializer, + RequestTicketModelSerializer, ): pass diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index ea1a5710a..e08c30af7 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -79,8 +79,6 @@ def get_url(self, item): context = self.context.copy() - context = self.context.copy() - ticket_type = str(item.get_ticket_type_display()).lower().replace(' ', '_') if ticket_type == 'project_task': @@ -96,8 +94,7 @@ def get_url(self, item): 'pk': item.pk } - - return { + url_dict: dict = { '_self': reverse( "v2:_api_v2_ticket_" + ticket_type + "-detail", request=context['view'].request, @@ -105,9 +102,26 @@ def get_url(self, item): ), 'comments': reverse('v2:_api_v2_ticket_comments-list', request=context['view'].request, kwargs={'ticket_id': item.pk}), 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), - 'related_tickets': reverse("v2:_api_v2_ticket_related-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), } + if item.category: + + url_dict.update({ + 'ticketcategory': reverse( + 'v2:_api_v2_ticket_category-list', + request=context['view'].request, + kwargs={}, + ) + '?' + ticket_type + '=true', + }) + + + url_dict.update({ + 'related_tickets': reverse("v2:_api_v2_ticket_related-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), + }) + + + return url_dict + duration = serializers.IntegerField(source='duration_ticket', read_only=True) @@ -190,19 +204,18 @@ def validate(self, data): return data - class TicketViewSerializer(TicketModelSerializer): - assigned_users = UserBaseSerializer(many=True, label='Assigned Users') - assigned_teams = TeamBaseSerializer(many=True) + assigned_users = UserBaseSerializer(many=True, label='Assigned Users') + category = TicketCategoryBaseSerializer() opened_by = UserBaseSerializer() - subscribed_users = UserBaseSerializer(many=True) + organization = OrganizationBaseSerializer(many=False, read_only=True) subscribed_teams = TeamBaseSerializer(many=True) - organization = OrganizationBaseSerializer(many=False, read_only=True) + subscribed_users = UserBaseSerializer(many=True) diff --git a/app/core/viewsets/ticket_category.py b/app/core/viewsets/ticket_category.py index 348919d3e..1529e69b4 100644 --- a/app/core/viewsets/ticket_category.py +++ b/app/core/viewsets/ticket_category.py @@ -56,7 +56,12 @@ class ViewSet(ModelViewSet): filterset_fields = [ + 'change', + 'incident', 'organization', + 'problem', + 'project_task', + 'request', ] search_fields = [ diff --git a/app/itim/serializers/change.py b/app/itim/serializers/change.py index aa3377176..174a3a1ec 100644 --- a/app/itim/serializers/change.py +++ b/app/itim/serializers/change.py @@ -1,7 +1,9 @@ from rest_framework import serializers +from rest_framework.fields import empty from app.serializers.user import UserBaseSerializer +from core.models.ticket.ticket_category import TicketCategory from core.serializers.ticket import ( Ticket, TicketBaseSerializer, @@ -26,8 +28,17 @@ class ChangeTicketModelSerializer( TicketModelSerializer, ): + + category = serializers.PrimaryKeyRelatedField( + queryset = TicketCategory.objects.filter( + change = True + ), + required = False + ) + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Change]) + class Meta( TicketModelSerializer.Meta ): fields = [ @@ -218,8 +229,8 @@ class Meta(ChangeTicketModelSerializer.Meta): class ChangeTicketViewSerializer( - ChangeTicketModelSerializer, TicketViewSerializer, + ChangeTicketModelSerializer, ): pass diff --git a/app/itim/serializers/incident.py b/app/itim/serializers/incident.py index 213eb2762..dd2e4bc39 100644 --- a/app/itim/serializers/incident.py +++ b/app/itim/serializers/incident.py @@ -2,6 +2,7 @@ from app.serializers.user import UserBaseSerializer +from core.models.ticket.ticket_category import TicketCategory from core.serializers.ticket import ( Ticket, TicketBaseSerializer, @@ -26,8 +27,16 @@ class IncidentTicketModelSerializer( TicketModelSerializer, ): + category = serializers.PrimaryKeyRelatedField( + queryset = TicketCategory.objects.filter( + incident = True + ), + required = False + ) + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Incident]) + class Meta( TicketModelSerializer.Meta ): fields = [ @@ -218,8 +227,8 @@ class Meta(IncidentTicketModelSerializer.Meta): class IncidentTicketViewSerializer( - IncidentTicketModelSerializer, TicketViewSerializer, + IncidentTicketModelSerializer, ): pass diff --git a/app/itim/serializers/problem.py b/app/itim/serializers/problem.py index 2200af278..56e608d79 100644 --- a/app/itim/serializers/problem.py +++ b/app/itim/serializers/problem.py @@ -2,6 +2,7 @@ from app.serializers.user import UserBaseSerializer +from core.models.ticket.ticket_category import TicketCategory from core.serializers.ticket import ( Ticket, TicketBaseSerializer, @@ -26,8 +27,17 @@ class ProblemTicketModelSerializer( TicketModelSerializer, ): + + category = serializers.PrimaryKeyRelatedField( + queryset = TicketCategory.objects.filter( + problem = True + ), + required = False + ) + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Problem]) + class Meta( TicketModelSerializer.Meta ): fields = [ @@ -218,8 +228,8 @@ class Meta(ProblemTicketModelSerializer.Meta): class ProblemTicketViewSerializer( - ProblemTicketModelSerializer, TicketViewSerializer, + ProblemTicketModelSerializer, ): pass diff --git a/app/project_management/serializers/project_task.py b/app/project_management/serializers/project_task.py index 8930b769e..b4c45c0bd 100644 --- a/app/project_management/serializers/project_task.py +++ b/app/project_management/serializers/project_task.py @@ -2,6 +2,7 @@ from app.serializers.user import UserBaseSerializer +from core.models.ticket.ticket_category import TicketCategory from core.serializers.ticket import ( Ticket, TicketBaseSerializer, @@ -26,8 +27,17 @@ class ProjectTaskTicketModelSerializer( TicketModelSerializer, ): + + category = serializers.PrimaryKeyRelatedField( + queryset = TicketCategory.objects.filter( + project_task = True + ), + required = False + ) + status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.ProjectTask]) + class Meta( TicketModelSerializer.Meta ): fields = [ @@ -218,8 +228,8 @@ class Meta(ProjectTaskTicketModelSerializer.Meta): class ProjectTaskTicketViewSerializer( - ProjectTaskTicketModelSerializer, TicketViewSerializer, + ProjectTaskTicketModelSerializer, ): pass From 6428e96c09a612831ba3e3aca16056b6d769dd82 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 19:00:32 +0930 Subject: [PATCH 436/617] fix(core): Add project URL to all Ticket Types ref: #248 #377 --- app/core/serializers/ticket.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index e08c30af7..c4b38bf49 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -104,6 +104,12 @@ def get_url(self, item): 'linked_items': reverse("v2:_api_v2_ticket_linked_item-list", request=context['view'].request, kwargs={'ticket_id': item.pk}), } + if item.project: + + url_dict.update({ + 'project': reverse("v2:_api_v2_project-list", request=context['view'].request, kwargs={}), + }) + if item.category: url_dict.update({ From 352b34294cca415d852ab1c9b4479dc8f354cba7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 19:11:48 +0930 Subject: [PATCH 437/617] feat(core): Ticket serializer to validate organization ref: #248 #368 #377 --- app/core/serializers/ticket.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index c4b38bf49..e26390dba 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -7,6 +7,8 @@ from app.serializers.user import UserBaseSerializer from api.exceptions import UnknownTicketType + +from core import exceptions as centurion_exception from core.models.ticket.ticket import Ticket from core.fields.badge import Badge, BadgeField @@ -187,6 +189,38 @@ class Meta: ] + + def validate_field_organization(self) -> bool: + """Check `organization field` + + Raises: + ValidationError: user tried to change the organization + + Returns: + True (bool): OK + False (bool): User tried to edit the organization + """ + + is_valid: bool = True + + if self.instance is not None: + + if self.instance.pk is not None: + + if 'organization' in self.get_user_changed_fields: + + if self.field_edited('organization'): + + is_valid = False + + centurion_exception.ValidationError( + detail = 'cant edit field: organization', + code = 'cant_edit_field_organization', + ) + + + return is_valid + def validate(self, data): if 'view' in self._context: @@ -206,6 +240,7 @@ def validate(self, data): raise UnknownTicketType() + self.validate_field_organization() return data From 11ce8cc864bc360f03885391c844abae95579c45 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 19:12:00 +0930 Subject: [PATCH 438/617] feat(core): Ticket serializer to validate milestone ref: #248 #368 #377 --- app/core/serializers/ticket.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index e26390dba..a7137114d 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -221,6 +221,42 @@ def validate_field_organization(self) -> bool: return is_valid + + def validate_field_milestone( self ) -> bool: + + is_valid: bool = False + + if self.instance is not None: + + if self.instance.milestone is None: + + return True + + else: + + if self.instance.project is None: + + raise centurion_exception.ValidationError( + details = 'Milestones require a project', + code = 'milestone_requires_project', + ) + + return False + + if self.instance.project.id == self.instance.milestone.project.id: + + return True + + else: + + raise centurion_exception.ValidationError( + detail = 'Milestone must be from the same project', + code = 'milestone_same_project', + ) + + return is_valid + + def validate(self, data): if 'view' in self._context: @@ -242,6 +278,8 @@ def validate(self, data): self.validate_field_organization() + self.validate_field_milestone() + return data From f522e6f9c15cdb228356d8ac7814feef2210026e Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 19:12:52 +0930 Subject: [PATCH 439/617] feat(core): Ticket serializer to ensure user who opens ticket is subscribed to it ref: #248 #368 #377 --- app/core/serializers/ticket.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index a7137114d..3c02b73e1 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -276,6 +276,18 @@ def validate(self, data): raise UnknownTicketType() + + if self.instance is None: + + subscribed_users: list = [] + + if 'subscribed_users' in data: + + subscribed_users: list = data['subscribed_users'] + + data['subscribed_users'] = subscribed_users + [ data['opened_by_id'] ] + + self.validate_field_organization() self.validate_field_milestone() From a2cac47414140a07b316be9e63ac038208a1862c Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 19:13:34 +0930 Subject: [PATCH 440/617] feat(core): Add Parse error to exceptions ref: #248 #368 #377 --- app/core/exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/exceptions.py b/app/core/exceptions.py index 0acbfee97..4c949647d 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,6 +1,7 @@ from rest_framework import exceptions, status from rest_framework.exceptions import ( MethodNotAllowed, + ParseError, PermissionDenied, ValidationError, ) From 13ab073f99e2943ba6c693493887fb460b34cd25 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:34:15 +0930 Subject: [PATCH 441/617] fix(core): Ticket serializer org validator to access correct data ref: #248 #368 #377 --- app/core/serializers/ticket.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 3c02b73e1..a8847ac5d 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -207,9 +207,7 @@ def validate_field_organization(self) -> bool: if self.instance.pk is not None: - if 'organization' in self.get_user_changed_fields: - - if self.field_edited('organization'): + if 'organization' in self.initial_data: is_valid = False From a00cc52ff0a6d8156c5435c43f75d07432f67d26 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:34:49 +0930 Subject: [PATCH 442/617] fix(core): Ensure that when fetching ticket permission, spaces are replaced with '_' ref: #248 #368 #377 --- app/core/viewsets/ticket.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/core/viewsets/ticket.py b/app/core/viewsets/ticket.py index 6a50816d5..6e187613a 100644 --- a/app/core/viewsets/ticket.py +++ b/app/core/viewsets/ticket.py @@ -121,7 +121,7 @@ def get_dynamic_permissions(self): if self.has_organization_permission( organization = organization.id, permissions_required = [ - str('core.import_ticket_' + self._ticket_type).lower() + str('core.import_ticket_' + self._ticket_type).lower().replace(' ', '_') ] ): @@ -145,7 +145,7 @@ def get_dynamic_permissions(self): if self.has_organization_permission( organization = organization.id, permissions_required = [ - str('core.triage_ticket_' + self._ticket_type).lower() + str('core.triage_ticket_' + self._ticket_type).lower().replace(' ', '_') ] ): @@ -163,7 +163,7 @@ def get_dynamic_permissions(self): if self.has_organization_permission( organization = organization.id, permissions_required = [ - str('core.triage_ticket_' + self._ticket_type).lower() + str('core.triage_ticket_' + self._ticket_type).lower().replace(' ', '_') ] ): @@ -179,7 +179,7 @@ def get_dynamic_permissions(self): raise ValueError('unable to determin the action_keyword') self.permission_required = [ - str('core.' + action_keyword + '_ticket_' + self._ticket_type).lower(), + str('core.' + action_keyword + '_ticket_' + self._ticket_type).lower().replace(' ', '_'), ] return super().get_permission_required() From 66d7d513ae99b23c363595e825165b22ece58a94 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:35:40 +0930 Subject: [PATCH 443/617] fix(core): Ensure Organization can be set when creating a ticket ref: #248 #368 #377 --- app/itim/serializers/change.py | 1 - app/itim/serializers/incident.py | 1 - app/itim/serializers/problem.py | 1 - app/project_management/serializers/project_task.py | 4 +--- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/itim/serializers/change.py b/app/itim/serializers/change.py index 174a3a1ec..162cecca3 100644 --- a/app/itim/serializers/change.py +++ b/app/itim/serializers/change.py @@ -118,7 +118,6 @@ class Meta(ChangeTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', 'project', 'milestone', 'subscribed_teams', diff --git a/app/itim/serializers/incident.py b/app/itim/serializers/incident.py index dd2e4bc39..a8f269a44 100644 --- a/app/itim/serializers/incident.py +++ b/app/itim/serializers/incident.py @@ -116,7 +116,6 @@ class Meta(IncidentTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', 'project', 'milestone', 'subscribed_teams', diff --git a/app/itim/serializers/problem.py b/app/itim/serializers/problem.py index 56e608d79..971091a9a 100644 --- a/app/itim/serializers/problem.py +++ b/app/itim/serializers/problem.py @@ -117,7 +117,6 @@ class Meta(ProblemTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', 'project', 'milestone', 'subscribed_teams', diff --git a/app/project_management/serializers/project_task.py b/app/project_management/serializers/project_task.py index b4c45c0bd..5ecbb60e4 100644 --- a/app/project_management/serializers/project_task.py +++ b/app/project_management/serializers/project_task.py @@ -23,8 +23,8 @@ class Meta( TicketBaseSerializer.Meta ): class ProjectTaskTicketModelSerializer( - ProjectTaskTicketBaseSerializer, TicketModelSerializer, + ProjectTaskTicketBaseSerializer, ): @@ -117,8 +117,6 @@ class Meta(ProjectTaskTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', - 'project', 'milestone', 'subscribed_teams', 'subscribed_users', From fe56fab6fd7b3192e044141c5eaa68303e51f0f2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:36:35 +0930 Subject: [PATCH 444/617] test(itim): Change Ticket API v2 ViewSet permission checks ref: #15 #248 #368 #377 --- .../ticket_change/test_ticket_change_viewset.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/itim/tests/unit/ticket_change/test_ticket_change_viewset.py diff --git a/app/itim/tests/unit/ticket_change/test_ticket_change_viewset.py b/app/itim/tests/unit/ticket_change/test_ticket_change_viewset.py new file mode 100644 index 000000000..cd354a62b --- /dev/null +++ b/app/itim/tests/unit/ticket_change/test_ticket_change_viewset.py @@ -0,0 +1,13 @@ +from django.test import TestCase + +from core.tests.abstract.test_ticket_viewset import Ticket, TicketViewSetPermissionsAPI + + +class TicketChangePermissionsAPI( + TicketViewSetPermissionsAPI, + TestCase, +): + + ticket_type = 'change' + + ticket_type_enum = Ticket.TicketType.CHANGE From e8629b2e1cb4d97c6485706614e68ef9785cffa2 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:37:02 +0930 Subject: [PATCH 445/617] test(itim): Incident Ticket API v2 ViewSet permission checks ref: #15 #248 #368 #377 --- .../ticket_incident/test_ticket_incident_viewset.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/itim/tests/unit/ticket_incident/test_ticket_incident_viewset.py diff --git a/app/itim/tests/unit/ticket_incident/test_ticket_incident_viewset.py b/app/itim/tests/unit/ticket_incident/test_ticket_incident_viewset.py new file mode 100644 index 000000000..f25067779 --- /dev/null +++ b/app/itim/tests/unit/ticket_incident/test_ticket_incident_viewset.py @@ -0,0 +1,13 @@ +from django.test import TestCase + +from core.tests.abstract.test_ticket_viewset import Ticket, TicketViewSetPermissionsAPI + + +class TicketIncidentPermissionsAPI( + TicketViewSetPermissionsAPI, + TestCase, +): + + ticket_type = 'incident' + + ticket_type_enum = Ticket.TicketType.INCIDENT From 525e826857948f90343d6968ecc2ccf5e457f10c Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:38:14 +0930 Subject: [PATCH 446/617] test(itim): Problem Ticket API v2 ViewSet permission checks ref: #15 #248 #368 #377 --- .../ticket_problem/test_ticket_problem_viewset.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/itim/tests/unit/ticket_problem/test_ticket_problem_viewset.py diff --git a/app/itim/tests/unit/ticket_problem/test_ticket_problem_viewset.py b/app/itim/tests/unit/ticket_problem/test_ticket_problem_viewset.py new file mode 100644 index 000000000..f27b7acce --- /dev/null +++ b/app/itim/tests/unit/ticket_problem/test_ticket_problem_viewset.py @@ -0,0 +1,13 @@ +from django.test import TestCase + +from core.tests.abstract.test_ticket_viewset import Ticket, TicketViewSetPermissionsAPI + + +class TicketProblemPermissionsAPI( + TicketViewSetPermissionsAPI, + TestCase, +): + + ticket_type = 'problem' + + ticket_type_enum = Ticket.TicketType.PROBLEM From ec3ab3e0551f5bc7330dc70e553ebdc223c5c9e7 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:38:51 +0930 Subject: [PATCH 447/617] test(project_management): PRoject_task API v2 ViewSet permission checks ref: #15 #248 #368 #377 --- .../unit/project_task/test_project_task_viewset.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 app/project_management/tests/unit/project_task/test_project_task_viewset.py diff --git a/app/project_management/tests/unit/project_task/test_project_task_viewset.py b/app/project_management/tests/unit/project_task/test_project_task_viewset.py new file mode 100644 index 000000000..dc7a7c380 --- /dev/null +++ b/app/project_management/tests/unit/project_task/test_project_task_viewset.py @@ -0,0 +1,14 @@ +from django.test import TestCase + +from core.tests.abstract.test_ticket_viewset import Ticket, TicketViewSetPermissionsAPI + + + +class TicketProjectTaskPermissionsAPI( + TicketViewSetPermissionsAPI, + TestCase, +): + + ticket_type = 'project_task' + + ticket_type_enum = Ticket.TicketType.PROJECT_TASK From 8c782b19ce23c879e31c92f7420c7dba693d93d0 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 20:50:29 +0930 Subject: [PATCH 448/617] test(project_management): Ensure ticket assigned project for all API v2 ViewSet permission checks ref: #15 #248 #368 #377 --- .../tests/abstract/test_ticket_viewset.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/core/tests/abstract/test_ticket_viewset.py b/app/core/tests/abstract/test_ticket_viewset.py index c4667d38b..e8f7f90b9 100644 --- a/app/core/tests/abstract/test_ticket_viewset.py +++ b/app/core/tests/abstract/test_ticket_viewset.py @@ -9,6 +9,8 @@ from core.models.ticket.ticket import Ticket +from project_management.models.projects import Project + from settings.models.user_settings import UserSettings @@ -52,8 +54,6 @@ def setUpTestData(self): different_organization = Organization.objects.create(name='test_different_organization') - self.url_kwargs = {} - view_permissions = Permission.objects.get( codename = 'view_ticket_' + self.ticket_type, @@ -166,17 +166,34 @@ def setUpTestData(self): ) + self.project = Project.objects.create( + organization = self.organization, + name = 'proj name' + ) + + self.item = self.model.objects.create( organization = self.organization, title = 'one', description = 'some text for body', opened_by = self.view_user, ticket_type = self.ticket_type_enum, - status = Ticket.TicketStatus.All.NEW + status = Ticket.TicketStatus.All.NEW, + project = self.project, ) + if self.ticket_type == 'project_task': + + self.url_kwargs = { 'project_id': self.project.id } + + self.url_view_kwargs = { 'project_id': self.project.id, 'pk': self.item.id} + + else: + + self.url_kwargs = {} + + self.url_view_kwargs = {'pk': self.item.id} - self.url_view_kwargs = {'pk': self.item.id} self.add_data = { 'title': 'team_post', @@ -184,6 +201,7 @@ def setUpTestData(self): 'description': 'article text', 'ticket_type': int(self.ticket_type_enum), 'status': int(Ticket.TicketStatus.All.NEW), + 'project': self.project.id, } From 39a2f4c3030ec3df2596492db648fec6111c6d5c Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 21:05:17 +0930 Subject: [PATCH 449/617] feat(core): Show project using base serializer for all ticket types ref: #248 #368 #377 --- app/core/serializers/ticket.py | 4 ++++ app/project_management/serializers/project_milestone.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index a8847ac5d..24127fce3 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -14,6 +14,8 @@ from core.fields.badge import Badge, BadgeField from core.serializers.ticket_category import TicketCategoryBaseSerializer +from project_management.serializers.project import ProjectBaseSerializer + class TicketBaseSerializer(serializers.ModelSerializer): @@ -305,6 +307,8 @@ class TicketViewSerializer(TicketModelSerializer): organization = OrganizationBaseSerializer(many=False, read_only=True) + project = ProjectBaseSerializer(many=False, read_only=True) + subscribed_teams = TeamBaseSerializer(many=True) subscribed_users = UserBaseSerializer(many=True) diff --git a/app/project_management/serializers/project_milestone.py b/app/project_management/serializers/project_milestone.py index e1eba818b..369aabd36 100644 --- a/app/project_management/serializers/project_milestone.py +++ b/app/project_management/serializers/project_milestone.py @@ -21,9 +21,11 @@ def get_display_name(self, item): def get_url(self, item): + context = self.context.copy() + return reverse( "v2:_api_v2_project_milestone-detail", - request=self._context['view'].request, + request=context['view'].request, kwargs={ 'project_id': item.project.id, 'pk': item.pk From bf1e211c22b62a17ef8989f307d470e4e3f8571e Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 21:05:36 +0930 Subject: [PATCH 450/617] feat(core): Show milestone using base serializer for all ticket types ref: #248 #368 #377 --- app/core/serializers/ticket.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 24127fce3..fb059120e 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -15,6 +15,7 @@ from core.serializers.ticket_category import TicketCategoryBaseSerializer from project_management.serializers.project import ProjectBaseSerializer +from project_management.serializers.project_milestone import ProjectMilestoneBaseSerializer @@ -309,6 +310,8 @@ class TicketViewSerializer(TicketModelSerializer): project = ProjectBaseSerializer(many=False, read_only=True) + milestone = ProjectMilestoneBaseSerializer(many=False, read_only=True) + subscribed_teams = TeamBaseSerializer(many=True) subscribed_users = UserBaseSerializer(many=True) From b53f4aa770f057763156423b43231c5d70f5f4b1 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 22:53:16 +0930 Subject: [PATCH 451/617] test(assistance): Update request field checks to cater for project and milestone as dicts ref: #15 #248 #368 #377 --- app/core/tests/abstract/test_ticket_api_v2.py | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/app/core/tests/abstract/test_ticket_api_v2.py b/app/core/tests/abstract/test_ticket_api_v2.py index 80ede1df3..5a8709a3f 100644 --- a/app/core/tests/abstract/test_ticket_api_v2.py +++ b/app/core/tests/abstract/test_ticket_api_v2.py @@ -485,7 +485,61 @@ def test_api_field_type_project(self): project field must be int """ - assert type(self.api_data['project']) is int + assert type(self.api_data['project']) is dict + + + def test_api_field_exists_project_id(self): + """ Test for existance of API Field + + project.id field must exist + """ + + assert 'id' in self.api_data['project'] + + + def test_api_field_type_project_id(self): + """ Test for type for API Field + + project.id field must be int + """ + + assert type(self.api_data['project']['id']) is int + + + def test_api_field_exists_project_display_name(self): + """ Test for existance of API Field + + project.display_name field must exist + """ + + assert 'display_name' in self.api_data['project'] + + + def test_api_field_type_project_display_name(self): + """ Test for type for API Field + + project.display_name field must be str + """ + + assert type(self.api_data['project']['display_name']) is str + + + def test_api_field_exists_project_url(self): + """ Test for existance of API Field + + project.url field must exist + """ + + assert 'url' in self.api_data['project'] + + + def test_api_field_type_project_url(self): + """ Test for type for API Field + + project.url field must be Hyperlink + """ + + assert type(self.api_data['project']['url']) is Hyperlink @@ -504,7 +558,61 @@ def test_api_field_type_milestone(self): milestone field must be int """ - assert type(self.api_data['milestone']) is int + assert type(self.api_data['milestone']) is dict + + + def test_api_field_exists_milestone_id(self): + """ Test for existance of API Field + + milestone.id field must exist + """ + + assert 'id' in self.api_data['milestone'] + + + def test_api_field_type_milestone_id(self): + """ Test for type for API Field + + milestone.id field must be int + """ + + assert type(self.api_data['milestone']['id']) is int + + + def test_api_field_exists_milestone_display_name(self): + """ Test for existance of API Field + + milestone.display_name field must exist + """ + + assert 'display_name' in self.api_data['milestone'] + + + def test_api_field_type_milestone_display_name(self): + """ Test for type for API Field + + milestone.display_name field must be str + """ + + assert type(self.api_data['milestone']['display_name']) is str + + + def test_api_field_exists_milestone_url(self): + """ Test for existance of API Field + + milestone.url field must exist + """ + + assert 'url' in self.api_data['milestone'] + + + def test_api_field_type_milestone_url(self): + """ Test for type for API Field + + milestone.url field must be str + """ + + assert type(self.api_data['milestone']['url']) is str From 67fa708edf2d5818263786d39eb17cac9b7c9b13 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 23:10:35 +0930 Subject: [PATCH 452/617] test(itim): Change Ticket API field checks ref: #15 #248 #368 #377 --- app/core/tests/abstract/test_ticket_api_v2.py | 2 +- .../test_ticket_change_api_v2.py | 146 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 app/itim/tests/unit/ticket_change/test_ticket_change_api_v2.py diff --git a/app/core/tests/abstract/test_ticket_api_v2.py b/app/core/tests/abstract/test_ticket_api_v2.py index 5a8709a3f..d1ec95d9b 100644 --- a/app/core/tests/abstract/test_ticket_api_v2.py +++ b/app/core/tests/abstract/test_ticket_api_v2.py @@ -78,7 +78,7 @@ def setUpTestData(self): self.view_team.permissions.set([view_permissions]) - self.view_user = User.objects.create_user(username="test_user_view", password="password") + self.view_user = User.objects.create_user(username="test_user_view", password="password", is_superuser = True) teamuser = TeamUsers.objects.create( team = self.view_team, user = self.view_user diff --git a/app/itim/tests/unit/ticket_change/test_ticket_change_api_v2.py b/app/itim/tests/unit/ticket_change/test_ticket_change_api_v2.py new file mode 100644 index 000000000..1c8e84be0 --- /dev/null +++ b/app/itim/tests/unit/ticket_change/test_ticket_change_api_v2.py @@ -0,0 +1,146 @@ +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from core.tests.abstract.test_ticket_api_v2 import TicketAPI + +from core.models.ticket.ticket import Ticket + + + +class ChangeTicketAPI( + TicketAPI, + TestCase +): + + model = Ticket + + ticket_type = 'change' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.item = self.model.objects.create( + + # All Tickets + organization=self.organization, + title = 'A ' + self.ticket_type + ' ticket', + description = 'the ticket body', + opened_by = self.view_user, + status = int(Ticket.TicketStatus.All.CLOSED.value), + project = self.project, + milestone = self.project_milestone, + external_ref = 1, + external_system = Ticket.Ticket_ExternalSystem.CUSTOM_1, + date_closed = '2024-01-01T01:02:03Z', + + # ITIL Tickets + category = self.ticket_category, + + # Specific to ticket + ticket_type = int(Ticket.TicketType.CHANGE.value), + ) + + self.item.assigned_teams.set([ self.view_team ]) + + self.item.assigned_users.set([ self.view_user ]) + + self.item.subscribed_teams.set([ self.view_team ]) + + self.item.subscribed_users.set([ self.view_user ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('v2:_api_v2_ticket_' + self.ticket_type + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_impact(self): + """ Test for existance of API Field + + impact field must exist + """ + + assert 'impact' in self.api_data + + + def test_api_field_type_impact(self): + """ Test for type for API Field + + impact field must be int + """ + + assert type(self.api_data['impact']) is int + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink From 3ef7d175c1283e6de5c4d13f3dd1326f9b4c93cb Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 23:10:52 +0930 Subject: [PATCH 453/617] test(itim): Incident Ticket API field checks ref: #15 #248 #368 #377 --- .../test_ticket_incident_api_v2.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 app/itim/tests/unit/ticket_incident/test_ticket_incident_api_v2.py diff --git a/app/itim/tests/unit/ticket_incident/test_ticket_incident_api_v2.py b/app/itim/tests/unit/ticket_incident/test_ticket_incident_api_v2.py new file mode 100644 index 000000000..14b501e94 --- /dev/null +++ b/app/itim/tests/unit/ticket_incident/test_ticket_incident_api_v2.py @@ -0,0 +1,146 @@ +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from core.tests.abstract.test_ticket_api_v2 import TicketAPI + +from core.models.ticket.ticket import Ticket + + + +class IncidentTicketAPI( + TicketAPI, + TestCase +): + + model = Ticket + + ticket_type = 'incident' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.item = self.model.objects.create( + + # All Tickets + organization=self.organization, + title = 'A ' + self.ticket_type + ' ticket', + description = 'the ticket body', + opened_by = self.view_user, + status = int(Ticket.TicketStatus.All.CLOSED.value), + project = self.project, + milestone = self.project_milestone, + external_ref = 1, + external_system = Ticket.Ticket_ExternalSystem.CUSTOM_1, + date_closed = '2024-01-01T01:02:03Z', + + # ITIL Tickets + category = self.ticket_category, + + # Specific to ticket + ticket_type = int(Ticket.TicketType.INCIDENT.value), + ) + + self.item.assigned_teams.set([ self.view_team ]) + + self.item.assigned_users.set([ self.view_user ]) + + self.item.subscribed_teams.set([ self.view_team ]) + + self.item.subscribed_users.set([ self.view_user ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('v2:_api_v2_ticket_' + self.ticket_type + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_impact(self): + """ Test for existance of API Field + + impact field must exist + """ + + assert 'impact' in self.api_data + + + def test_api_field_type_impact(self): + """ Test for type for API Field + + impact field must be int + """ + + assert type(self.api_data['impact']) is int + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink From 85a9cf17cd28f59f799df80c487a51cf46e56d33 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 23:11:02 +0930 Subject: [PATCH 454/617] test(itim): Problem Ticket API field checks ref: #15 #248 #368 #377 --- .../test_ticket_problem_api_v2.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 app/itim/tests/unit/ticket_problem/test_ticket_problem_api_v2.py diff --git a/app/itim/tests/unit/ticket_problem/test_ticket_problem_api_v2.py b/app/itim/tests/unit/ticket_problem/test_ticket_problem_api_v2.py new file mode 100644 index 000000000..0109e6543 --- /dev/null +++ b/app/itim/tests/unit/ticket_problem/test_ticket_problem_api_v2.py @@ -0,0 +1,146 @@ +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from core.tests.abstract.test_ticket_api_v2 import TicketAPI + +from core.models.ticket.ticket import Ticket + + + +class ProblemTicketAPI( + TicketAPI, + TestCase +): + + model = Ticket + + ticket_type = 'problem' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.item = self.model.objects.create( + + # All Tickets + organization=self.organization, + title = 'A ' + self.ticket_type + ' ticket', + description = 'the ticket body', + opened_by = self.view_user, + status = int(Ticket.TicketStatus.All.CLOSED.value), + project = self.project, + milestone = self.project_milestone, + external_ref = 1, + external_system = Ticket.Ticket_ExternalSystem.CUSTOM_1, + date_closed = '2024-01-01T01:02:03Z', + + # ITIL Tickets + category = self.ticket_category, + + # Specific to ticket + ticket_type = int(Ticket.TicketType.PROBLEM.value), + ) + + self.item.assigned_teams.set([ self.view_team ]) + + self.item.assigned_users.set([ self.view_user ]) + + self.item.subscribed_teams.set([ self.view_team ]) + + self.item.subscribed_users.set([ self.view_user ]) + + + self.url_view_kwargs = {'pk': self.item.id} + + client = Client() + url = reverse('v2:_api_v2_ticket_' + self.ticket_type + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_impact(self): + """ Test for existance of API Field + + impact field must exist + """ + + assert 'impact' in self.api_data + + + def test_api_field_type_impact(self): + """ Test for type for API Field + + impact field must be int + """ + + assert type(self.api_data['impact']) is int + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink From 1b286d0873a2f4842c6aef7b2c0b991f31b851f5 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 2 Nov 2024 23:11:23 +0930 Subject: [PATCH 455/617] test(project_management): Project Task API field checks ref: #15 #248 #368 #377 --- .../test_ticket_project_task_api_v2.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 app/project_management/tests/unit/project_task/test_ticket_project_task_api_v2.py diff --git a/app/project_management/tests/unit/project_task/test_ticket_project_task_api_v2.py b/app/project_management/tests/unit/project_task/test_ticket_project_task_api_v2.py new file mode 100644 index 000000000..c57b7dac0 --- /dev/null +++ b/app/project_management/tests/unit/project_task/test_ticket_project_task_api_v2.py @@ -0,0 +1,147 @@ +from django.shortcuts import reverse +from django.test import Client, TestCase + +from rest_framework.relations import Hyperlink + +from core.tests.abstract.test_ticket_api_v2 import TicketAPI + +from core.models.ticket.ticket import Ticket + + + +class ProjectTaskTicketAPI( + TicketAPI, + TestCase +): + + model = Ticket + + ticket_type = 'project_task' + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create an item + + """ + + super().setUpTestData() + + + self.item = self.model.objects.create( + + # All Tickets + organization=self.organization, + title = 'A ' + self.ticket_type + ' ticket', + description = 'the ticket body', + opened_by = self.view_user, + status = int(Ticket.TicketStatus.All.CLOSED.value), + project = self.project, + milestone = self.project_milestone, + external_ref = 1, + external_system = Ticket.Ticket_ExternalSystem.CUSTOM_1, + date_closed = '2024-01-01T01:02:03Z', + + # ITIL Tickets + category = self.ticket_category, + + # Specific to ticket + ticket_type = int(Ticket.TicketType.PROJECT_TASK.value), + ) + + self.item.assigned_teams.set([ self.view_team ]) + + self.item.assigned_users.set([ self.view_user ]) + + self.item.subscribed_teams.set([ self.view_team ]) + + self.item.subscribed_users.set([ self.view_user ]) + + + self.url_kwargs = {'project_id': self.project.id} + self.url_view_kwargs = {'project_id': self.project.id, 'pk': self.item.id} + + client = Client() + url = reverse('v2:_api_v2_ticket_' + self.ticket_type + '-detail', kwargs=self.url_view_kwargs) + + + client.force_login(self.view_user) + response = client.get(url) + + self.api_data = response.data + + + + def test_api_field_exists_impact(self): + """ Test for existance of API Field + + impact field must exist + """ + + assert 'impact' in self.api_data + + + def test_api_field_type_impact(self): + """ Test for type for API Field + + impact field must be int + """ + + assert type(self.api_data['impact']) is int + + + + def test_api_field_exists_category(self): + """ Test for existance of API Field + + category field must exist + """ + + assert 'category' in self.api_data + + + def test_api_field_type_category(self): + """ Test for type for API Field + + category field must be dict + """ + + assert type(self.api_data['category']) is dict + + + def test_api_field_exists_category_display_name(self): + """ Test for existance of API Field + + category.display_name field must exist + """ + + assert 'display_name' in self.api_data['category'] + + + def test_api_field_type_category_display_name(self): + """ Test for type for API Field + + category.display_name field must be str + """ + + assert type(self.api_data['category']['display_name']) is str + + + def test_api_field_exists_category_url(self): + """ Test for existance of API Field + + category.url field must exist + """ + + assert 'url' in self.api_data['category'] + + + def test_api_field_type_category_url(self): + """ Test for type for API Field + + category.url field must be Hyperlink + """ + + assert type(self.api_data['category']['url']) is Hyperlink From b1a42e01bfbffa0c1e30c6d8f1125d48fc33b74b Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:42:53 +0930 Subject: [PATCH 456/617] fix(core): Correct inheritence order for ticket serializers ref: #248 #378 --- app/assistance/serializers/request.py | 2 +- app/itim/serializers/change.py | 2 +- app/itim/serializers/incident.py | 2 +- app/itim/serializers/problem.py | 2 +- app/project_management/serializers/project_task.py | 1 - 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assistance/serializers/request.py b/app/assistance/serializers/request.py index af0c399d6..b45fa96ab 100644 --- a/app/assistance/serializers/request.py +++ b/app/assistance/serializers/request.py @@ -23,8 +23,8 @@ class Meta( TicketBaseSerializer.Meta ): class RequestTicketModelSerializer( - RequestTicketBaseSerializer, TicketModelSerializer, + RequestTicketBaseSerializer, ): diff --git a/app/itim/serializers/change.py b/app/itim/serializers/change.py index 162cecca3..b20a224bd 100644 --- a/app/itim/serializers/change.py +++ b/app/itim/serializers/change.py @@ -24,8 +24,8 @@ class Meta( TicketBaseSerializer.Meta ): class ChangeTicketModelSerializer( - ChangeTicketBaseSerializer, TicketModelSerializer, + ChangeTicketBaseSerializer, ): diff --git a/app/itim/serializers/incident.py b/app/itim/serializers/incident.py index a8f269a44..50b5dde69 100644 --- a/app/itim/serializers/incident.py +++ b/app/itim/serializers/incident.py @@ -23,8 +23,8 @@ class Meta( TicketBaseSerializer.Meta ): class IncidentTicketModelSerializer( - IncidentTicketBaseSerializer, TicketModelSerializer, + IncidentTicketBaseSerializer, ): category = serializers.PrimaryKeyRelatedField( diff --git a/app/itim/serializers/problem.py b/app/itim/serializers/problem.py index 971091a9a..dae7e46b4 100644 --- a/app/itim/serializers/problem.py +++ b/app/itim/serializers/problem.py @@ -23,8 +23,8 @@ class Meta( TicketBaseSerializer.Meta ): class ProblemTicketModelSerializer( - ProblemTicketBaseSerializer, TicketModelSerializer, + ProblemTicketBaseSerializer, ): diff --git a/app/project_management/serializers/project_task.py b/app/project_management/serializers/project_task.py index 5ecbb60e4..b2e4e4113 100644 --- a/app/project_management/serializers/project_task.py +++ b/app/project_management/serializers/project_task.py @@ -24,7 +24,6 @@ class Meta( TicketBaseSerializer.Meta ): class ProjectTaskTicketModelSerializer( TicketModelSerializer, - ProjectTaskTicketBaseSerializer, ): From 5821c5b33b215545088a976e03fbeea8d9b88dcb Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:45:21 +0930 Subject: [PATCH 457/617] fix(core): Correct Ticket read-only fields ref: #248 #378 --- app/assistance/serializers/request.py | 27 +++++++++++++++++-- app/itim/serializers/change.py | 27 +++++++++++++++++-- app/itim/serializers/incident.py | 27 +++++++++++++++++-- app/itim/serializers/problem.py | 27 +++++++++++++++++-- .../serializers/project_task.py | 27 +++++++++++++++++-- 5 files changed, 125 insertions(+), 10 deletions(-) diff --git a/app/assistance/serializers/request.py b/app/assistance/serializers/request.py index b45fa96ab..3d737db68 100644 --- a/app/assistance/serializers/request.py +++ b/app/assistance/serializers/request.py @@ -35,7 +35,11 @@ class RequestTicketModelSerializer( required = False ) - status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Request]) + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Request], + default = Ticket.TicketStatus.All.NEW, + required = False, + ) class Meta( TicketModelSerializer.Meta ): @@ -66,6 +70,10 @@ class Meta( TicketModelSerializer.Meta ): 'milestone', 'subscribed_teams', 'subscribed_users', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', '_urls', ] @@ -91,6 +99,11 @@ class RequestAddTicketModelSerializer( """ + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + class Meta(RequestTicketModelSerializer.Meta): read_only_fields = [ @@ -134,6 +147,17 @@ class RequestChangeTicketModelSerializer( RequestTicketModelSerializer (class): Request Model Serializer """ + + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Request], + read_only = True, + ) + + class Meta(RequestTicketModelSerializer.Meta): read_only_fields = [ @@ -198,7 +222,6 @@ class Meta(RequestTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', '_urls', ] diff --git a/app/itim/serializers/change.py b/app/itim/serializers/change.py index b20a224bd..5342b9e99 100644 --- a/app/itim/serializers/change.py +++ b/app/itim/serializers/change.py @@ -36,7 +36,11 @@ class ChangeTicketModelSerializer( required = False ) - status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Change]) + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Change], + default = Ticket.TicketStatus.All.NEW, + required = False, + ) class Meta( TicketModelSerializer.Meta ): @@ -66,6 +70,10 @@ class Meta( TicketModelSerializer.Meta ): 'organization', 'project', 'milestone', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', 'subscribed_teams', 'subscribed_users', '_urls', @@ -93,6 +101,11 @@ class ChangeAddTicketModelSerializer( """ + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + class Meta(ChangeTicketModelSerializer.Meta): read_only_fields = [ @@ -136,6 +149,17 @@ class ChangeChangeTicketModelSerializer( ChangeTicketModelSerializer (class): Change Model Serializer """ + + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Change], + read_only = True, + ) + + class Meta(ChangeTicketModelSerializer.Meta): read_only_fields = [ @@ -200,7 +224,6 @@ class Meta(ChangeTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', '_urls', ] diff --git a/app/itim/serializers/incident.py b/app/itim/serializers/incident.py index 50b5dde69..9afea61e5 100644 --- a/app/itim/serializers/incident.py +++ b/app/itim/serializers/incident.py @@ -34,7 +34,11 @@ class IncidentTicketModelSerializer( required = False ) - status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Incident]) + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Incident], + default = Ticket.TicketStatus.All.NEW, + required = False, + ) class Meta( TicketModelSerializer.Meta ): @@ -64,6 +68,10 @@ class Meta( TicketModelSerializer.Meta ): 'organization', 'project', 'milestone', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', 'subscribed_teams', 'subscribed_users', '_urls', @@ -91,6 +99,11 @@ class IncidentAddTicketModelSerializer( """ + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + class Meta(IncidentTicketModelSerializer.Meta): read_only_fields = [ @@ -134,6 +147,17 @@ class IncidentChangeTicketModelSerializer( IncidentTicketModelSerializer (class): Incident Model Serializer """ + + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Incident], + read_only = True, + ) + + class Meta(IncidentTicketModelSerializer.Meta): read_only_fields = [ @@ -198,7 +222,6 @@ class Meta(IncidentTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', '_urls', ] diff --git a/app/itim/serializers/problem.py b/app/itim/serializers/problem.py index dae7e46b4..8f8781704 100644 --- a/app/itim/serializers/problem.py +++ b/app/itim/serializers/problem.py @@ -35,7 +35,11 @@ class ProblemTicketModelSerializer( required = False ) - status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.Problem]) + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Problem], + default = Ticket.TicketStatus.All.NEW, + required = False, + ) class Meta( TicketModelSerializer.Meta ): @@ -65,6 +69,10 @@ class Meta( TicketModelSerializer.Meta ): 'organization', 'project', 'milestone', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', 'subscribed_teams', 'subscribed_users', '_urls', @@ -92,6 +100,11 @@ class ProblemAddTicketModelSerializer( """ + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + class Meta(ProblemTicketModelSerializer.Meta): read_only_fields = [ @@ -135,6 +148,17 @@ class ProblemChangeTicketModelSerializer( ProblemTicketModelSerializer (class): Problem Model Serializer """ + + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.Problem], + read_only = True, + ) + + class Meta(ProblemTicketModelSerializer.Meta): read_only_fields = [ @@ -199,7 +223,6 @@ class Meta(ProblemTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', '_urls', ] diff --git a/app/project_management/serializers/project_task.py b/app/project_management/serializers/project_task.py index b2e4e4113..467141cb6 100644 --- a/app/project_management/serializers/project_task.py +++ b/app/project_management/serializers/project_task.py @@ -34,7 +34,11 @@ class ProjectTaskTicketModelSerializer( required = False ) - status = serializers.ChoiceField([(e.value, e.label) for e in Ticket.TicketStatus.ProjectTask]) + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.ProjectTask], + default = Ticket.TicketStatus.All.NEW, + required = False, + ) class Meta( TicketModelSerializer.Meta ): @@ -64,6 +68,10 @@ class Meta( TicketModelSerializer.Meta ): 'organization', 'project', 'milestone', + 'planned_start_date', + 'planned_finish_date', + 'real_start_date', + 'real_finish_date', 'subscribed_teams', 'subscribed_users', '_urls', @@ -91,6 +99,11 @@ class ProjectTaskAddTicketModelSerializer( """ + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + class Meta(ProjectTaskTicketModelSerializer.Meta): read_only_fields = [ @@ -133,6 +146,17 @@ class ProjectTaskChangeTicketModelSerializer( ProjectTaskTicketModelSerializer (class): ProjectTask Model Serializer """ + + category = serializers.PrimaryKeyRelatedField( + read_only = True, + ) + + status = serializers.ChoiceField( + [(e.value, e.label) for e in Ticket.TicketStatus.ProjectTask], + read_only = True, + ) + + class Meta(ProjectTaskTicketModelSerializer.Meta): read_only_fields = [ @@ -197,7 +221,6 @@ class Meta(ProjectTaskTicketModelSerializer.Meta): 'real_start_date', 'real_finish_date', 'opened_by', - 'organization', '_urls', ] From e771631a040b02d239e8d36efa782107149c17ba Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:46:21 +0930 Subject: [PATCH 458/617] fix(core): Ensure that when creating a ticket an organization is specified ref: #248 #378 --- app/core/serializers/ticket.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index fb059120e..b1b773891 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -214,11 +214,24 @@ def validate_field_organization(self) -> bool: is_valid = False - centurion_exception.ValidationError( + raise centurion_exception.ValidationError( detail = 'cant edit field: organization', code = 'cant_edit_field_organization', ) + elif self.instance is None: + + if 'organization' not in self.initial_data: + + is_valid = False + + raise centurion_exception.ValidationError( + detail = { + 'organization': 'this field is required' + }, + code = 'required', + ) + return is_valid From 3bb7978d155e7c2c6eff8d37553595f8ef0ccac6 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:46:50 +0930 Subject: [PATCH 459/617] fix(core): When creating a ticket, by default give it a status of new ref: #248 #378 --- app/core/serializers/ticket.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index b1b773891..3767ff692 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -301,6 +301,8 @@ def validate(self, data): data['subscribed_users'] = subscribed_users + [ data['opened_by_id'] ] + data['status'] = int(Ticket.TicketStatus.All.NEW) + self.validate_field_organization() From 552bce4d475f63474608a4264b0cb18df3549330 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:50:52 +0930 Subject: [PATCH 460/617] fix(core): correct missing or incomplete ticket model fields ref: #248 #378 --- ...5_alter_relatedtickets_options_and_more.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/core/migrations/0015_alter_relatedtickets_options_and_more.py diff --git a/app/core/migrations/0015_alter_relatedtickets_options_and_more.py b/app/core/migrations/0015_alter_relatedtickets_options_and_more.py new file mode 100644 index 000000000..b98d93ca3 --- /dev/null +++ b/app/core/migrations/0015_alter_relatedtickets_options_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2024-11-03 14:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0014_alter_ticketcomment_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='relatedtickets', + options={'ordering': ['id'], 'verbose_name': 'Related Ticket', 'verbose_name_plural': 'Related Tickets'}, + ), + migrations.AlterModelOptions( + name='ticketcomment', + options={'ordering': ['created', 'ticket', 'parent_id'], 'verbose_name': 'Ticket Comment', 'verbose_name_plural': 'Ticket Comments'}, + ), + ] From 18db1f58ffa667791da9d26a0efd9e36d1c6a73d Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:51:55 +0930 Subject: [PATCH 461/617] test(core): Common Ticket Test Cases for API v2 serializers ref: #15 #248 #368 #378 --- .../tests/abstract/test_ticket_serializer.py | 2050 +++++++++++++++++ 1 file changed, 2050 insertions(+) create mode 100644 app/core/tests/abstract/test_ticket_serializer.py diff --git a/app/core/tests/abstract/test_ticket_serializer.py b/app/core/tests/abstract/test_ticket_serializer.py new file mode 100644 index 000000000..77ba8f7a0 --- /dev/null +++ b/app/core/tests/abstract/test_ticket_serializer.py @@ -0,0 +1,2050 @@ +import pytest + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from access.models import Organization, Team, TeamUsers, Permission + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_category import TicketCategory + + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone + + + +class MockView: + + _ticket_type_id: Ticket.TicketType = None + action: str = None + + kwargs: dict = {} + + + +class MockRequest: + + user = None + + + +class TicketValidationAPI( + +): + + model = Ticket + + add_serializer = None + change_serializer = None + import_serializer = None + triage_serializer = None + + ticket_type: str = None + """Ticket type name in lowercase""" + + ticket_type_enum: Ticket.TicketType = None + """Ticket type enum value""" + + add_data:dict = {} + """data to add. Amend to this dict in each parent class""" + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + organization_two = Organization.objects.create(name='test_org_two') + + self.organization_two = organization_two + + self.ticket_category = TicketCategory.objects.create( + organization = self.organization, + name = 'ticket category', + ) + + + self.project = Project.objects.create( + organization = self.organization, + name = 'project name' + ) + + self.project_two = Project.objects.create( + organization = self.organization, + name = 'project name two' + ) + + self.project_milestone = ProjectMilestone.objects.create( + organization = self.organization, + name = 'project name', + project = self.project + ) + + + + + self.add_data.update({ + 'organization': self.organization.id, + 'title': 'a ticket' + self.ticket_type, + 'description': 'ticket description', + 'project': self.project.id + }) + + + self.add_user = User.objects.create_user(username="test_user_add", password="password") + + add_permissions = Permission.objects.get( + codename = 'add_' + self.model._meta.model_name + '_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + self.add_team = Team.objects.create( + team_name = 'add_team', + organization = organization, + ) + + self.add_team.permissions.set([add_permissions]) + + teamuser = TeamUsers.objects.create( + team = self.add_team, + user = self.add_user + ) + + + self.change_user = User.objects.create_user(username="test_user_change", password="password") + + change_permissions = Permission.objects.get( + codename = 'change_' + self.model._meta.model_name + '_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + self.change_team = Team.objects.create( + team_name = 'change_team', + organization = organization, + ) + + self.change_team.permissions.set([change_permissions]) + + teamuser = TeamUsers.objects.create( + team = self.change_team, + user = self.change_user + ) + + + self.triage_user = User.objects.create_user(username="test_user_triage", password="password") + + triage_permissions = Permission.objects.get( + codename = 'triage_' + self.model._meta.model_name + '_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + triage_team = Team.objects.create( + team_name = 'triage_team', + organization = organization, + ) + + triage_team.permissions.set([triage_permissions]) + + teamuser = TeamUsers.objects.create( + team = triage_team, + user = self.triage_user + ) + + + self.import_user = User.objects.create_user(username="test_user_import", password="password") + + import_permissions = Permission.objects.get( + codename = 'import_' + self.model._meta.model_name + '_' + self.ticket_type, + content_type = ContentType.objects.get( + app_label = self.model._meta.app_label, + model = self.model._meta.model_name, + ) + ) + + import_team = Team.objects.create( + team_name = 'import_team', + organization = organization, + ) + + import_team.permissions.set([import_permissions]) + + teamuser = TeamUsers.objects.create( + team = import_team, + user = self.import_user + ) + + + self.ticket = Ticket.objects.create( + organization=organization, + title = 'ticket title', + description = 'some text', + opened_by = self.add_user, + status = Ticket.TicketStatus.All.NEW, + ticket_type = self.ticket_type_enum, + ) + + + # use serializer with all fields to add an item (use save) + # and check to ensure the fields not allowed, are not saved to db. + + self.all_fields_data: dict = { + # Required Fields + 'organization': self.organization.id, + 'title': 'a ticket ' + self.ticket_type + ' all fields', + 'description': 'ticket description', + + # Remaining Fields + 'assigned_teams': [ self.add_team.id ], + 'assigned_users': [ self.add_user.id ], + 'category': self.ticket_category.id, + 'created': '2024-01-01T01:02:03Z', + 'modified': '2024-01-01T04:05:06Z', + 'status': int(Ticket.TicketStatus.All.CLOSED), + 'estimate': 1, + 'duration_ticket': 2, + 'urgency': int(Ticket.TicketUrgency.HIGH), + 'impact': int(Ticket.TicketImpact.MEDIUM), + 'priority': int(Ticket.TicketPriority.LOW), + 'external_ref': 3, + 'external_system': int(Ticket.Ticket_ExternalSystem.CUSTOM_1), + 'ticket_type': int(self.ticket_type_enum), + 'is_deleted': True, + 'date_closed': '2024-01-01T07:08:09Z', + 'planned_start_date': '2024-01-02T01:02:03Z', + 'planned_finish_date': '2024-01-02T02:03:04Z', + 'real_start_date': '2024-01-03T01:02:03Z', + 'real_finish_date': '2024-01-04T01:02:03Z', + 'opened_by': self.change_user.id, + 'organization': self.organization_two.id, + 'project': self.project.id, + 'milestone': self.project_milestone.id, + 'subscribed_teams': [ self.change_team.id ], + 'subscribed_users': [ self.change_user.id ] + } + + + + # + # Add Serializer + # + + + mock_view = MockView() + mock_view.action = 'create' + mock_view._ticket_type_id = self.ticket_type_enum + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + serializer = self.add_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = self.all_fields_data + ) + + serializer.is_valid( raise_exception = True ) + + serializer.save() + + self.created_ticket_add_serializer = serializer.instance + + + + # + # Change Serializer + # + + + self.ticket_for_change = Ticket.objects.create( + organization=organization, + title = 'ticket title for change serializer ' + self.ticket_type, + description = 'some text', + opened_by = self.add_user, + status = Ticket.TicketStatus.All.NEW, + ticket_type = self.ticket_type_enum, + project = self.project_two + ) + + self.ticket_for_change.subscribed_users.add( self.add_user.id ) + + self.all_fields_data_change = self.all_fields_data.copy() + + self.all_fields_data_change.update({ + 'title': 'a change ticket ' + self.ticket_type + ' all fields', + }) + + del self.all_fields_data_change['organization'] # ToDo: Test seperatly + + + mock_view = MockView() + mock_view.action = 'partial_update' + mock_view._ticket_type_id = self.ticket_type_enum + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + serializer = self.change_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = self.all_fields_data_change, + instance = self.ticket_for_change, + partial = True, + ) + + serializer.is_valid( raise_exception = True ) + + serializer.save() + + self.created_ticket_change_serializer = serializer.instance + + + # + # Triage Serializer Add New Ticket + # + + + self.all_fields_data_triage = self.all_fields_data.copy() + + self.all_fields_data_triage.update({ + 'title': 'a triage ticket ' + self.ticket_type + ' all fields', + }) + + + mock_view = MockView() + mock_view.action = 'create' + mock_view._ticket_type_id = self.ticket_type_enum + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + serializer = self.triage_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = self.all_fields_data_triage, + ) + + serializer.is_valid( raise_exception = True ) + + serializer.save() + + self.created_ticket_triage_serializer = serializer.instance + + + # + # Triage Serializer Change existing Ticket + # + + + self.ticket_for_triage_change = Ticket.objects.create( + organization=organization, + title = 'ticket title for change serializer ' + self.ticket_type, + description = 'some text', + opened_by = self.add_user, + status = Ticket.TicketStatus.All.NEW, + ticket_type = self.ticket_type_enum, + project = self.project_two + ) + + self.ticket_for_triage_change.subscribed_users.add( self.add_user.id ) + + self.all_fields_data_triage_change = self.all_fields_data.copy() + + self.all_fields_data_triage_change.update({ + 'title': 'a triage change ticket ' + self.ticket_type + ' all fields', + }) + + del self.all_fields_data_triage_change['organization'] # ToDo: Test seperatly + + + mock_view = MockView() + mock_view.action = 'partial_update' + mock_view._ticket_type_id = self.ticket_type_enum + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + serializer = self.triage_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = self.all_fields_data_triage_change, + instance = self.ticket_for_triage_change, + partial = True, + ) + + serializer.is_valid( raise_exception = True ) + + serializer.save() + + self.changed_ticket_triage_serializer = serializer.instance + + + + + # + # Import Serializer Add New Ticket + # + + + self.all_fields_data_import = self.all_fields_data.copy() + + self.all_fields_data_import.update({ + 'title': 'a import ticket ' + self.ticket_type + ' all fields', + }) + + + mock_view = MockView() + mock_view.action = 'create' + mock_view._ticket_type_id = self.ticket_type_enum + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + serializer = self.import_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = self.all_fields_data_import, + ) + + serializer.is_valid( raise_exception = True ) + + serializer.save() + + self.created_ticket_import_serializer = serializer.instance + + + + + + + + def test_serializer_validation_add_valid_ok(self): + """Serializer Validation Check + + Ensure that valid data has no validation errors. + """ + + mock_view = MockView() + mock_view.action = 'create' + mock_view._ticket_type_id = self.ticket_type_enum + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + serializer = self.add_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = self.add_data + ) + + assert serializer.is_valid(raise_exception = True) + + + + + def test_serializer_validation_change_valid_ok(self): + """Serializer Validation Check + + Ensure that valid data has no validation errors. + """ + + mock_view = MockView() + mock_view.action = 'partial_update' + + mock_request = MockRequest() + mock_request.user = self.change_user + + mock_view.request = mock_request + + + serializer = self.change_serializer( + instance = self.ticket, + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = { + 'title': 'changed title' + }, + partial = True, + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_import_valid_ok(self): + """Serializer Validation Check + + Ensure that valid data has no validation errors. + """ + + data = self.add_data.copy() + + data.update({ + 'opened_by': self.add_user.id, + }) + + mock_view = MockView() + mock_view.action = 'create' + + mock_request = MockRequest() + mock_request.user = self.import_user + + mock_view.request = mock_request + + + serializer = self.import_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = data + ) + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_validation_add_no_title(self): + """Serializer Validation Check + + Ensure that if creating and a title + is not provided a validation error occurs + """ + + data = self.add_data.copy() + + del data['title'] + + mock_view = MockView() + mock_view.action = 'create' + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + with pytest.raises(ValidationError) as err: + + serializer = self.add_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['title'][0] == 'required' + + + + def test_serializer_validation_add_no_description(self): + """Serializer Validation Check + + Ensure that if creating and a description + is not provided a validation error occurs + """ + + data = self.add_data.copy() + + del data['description'] + + + mock_view = MockView() + mock_view.action = 'create' + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + with pytest.raises(ValidationError) as err: + + serializer = self.add_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['description'][0] == 'required' + + + + def test_serializer_validation_add_no_organization(self): + """Serializer Validation Check + + Ensure that if creating and a organization + is not provided a validation error occurs + """ + + data = self.add_data.copy() + + del data['organization'] + + + mock_view = MockView() + mock_view.action = 'create' + + mock_request = MockRequest() + mock_request.user = self.add_user + + mock_view.request = mock_request + + + with pytest.raises(ValidationError) as err: + + serializer = self.add_serializer( + context = { + 'request': mock_request, + 'view': mock_view, + }, + data = data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['organization'][0] == 'required' + + + + + def test_serializer_add_field_remains_default_assigned_teams(self): + """Ensure serializer doesn't allow edit + + For an ADD operation assigned_teams should not be editable + """ + + assert len(list(self.created_ticket_add_serializer.assigned_teams.all())) == 0 + + + + def test_serializer_add_field_remains_default_assigned_users(self): + """Ensure serializer doesn't allow edit + + For an ADD operation assigned_users should not be editable + """ + + assert len(list(self.created_ticket_add_serializer.assigned_users.all())) == 0 + + + + def test_serializer_add_field_remains_default_category(self): + """Ensure serializer doesn't allow edit + + For an ADD operation category should not be editable + """ + + assert self.created_ticket_add_serializer.category_id is None + + + + def test_serializer_add_field_remains_default_created(self): + """Ensure serializer doesn't allow edit + + For an ADD operation created should not be editable + """ + + assert ( + str(self.created_ticket_add_serializer.created) is not None + and str(self.created_ticket_add_serializer.created) != self.all_fields_data['created'] + ) + + + + def test_serializer_add_field_remains_default_modified(self): + """Ensure serializer doesn't allow edit + + For an ADD operation modified should not be editable + """ + + assert ( + str(self.created_ticket_add_serializer.modified) is not None + and str(self.created_ticket_add_serializer.modified) != self.all_fields_data['modified'] + ) + + + + def test_serializer_add_field_remains_default_status(self): + """Ensure serializer doesn't allow edit + + For an ADD operation status should not be editable + """ + + assert self.created_ticket_add_serializer.status == int(Ticket.TicketStatus.All.NEW) + + + + def test_serializer_add_field_remains_default_estimate(self): + """Ensure serializer doesn't allow edit + + For an ADD operation estimate should not be editable + """ + + assert self.created_ticket_add_serializer.estimate == 0 + + + + def test_serializer_add_field_remains_default_duration_ticket(self): + """Ensure serializer doesn't allow edit + + For an ADD operation duration_ticket should not be editable + """ + + assert int(self.created_ticket_add_serializer.duration_ticket) == 0 + + + + def test_serializer_add_field_remains_default_impact(self): + """Ensure serializer doesn't allow edit + + For an ADD operation impact should not be editable + """ + + assert self.created_ticket_add_serializer.impact == int(Ticket.TicketImpact.VERY_LOW) + + + + def test_serializer_add_field_remains_default_priority(self): + """Ensure serializer doesn't allow edit + + For an ADD operation priority should not be editable + """ + + assert self.created_ticket_add_serializer.priority == int(Ticket.TicketPriority.VERY_LOW) + + + + def test_serializer_add_field_remains_default_external_ref(self): + """Ensure serializer doesn't allow edit + + For an ADD operation external_ref should not be editable + """ + + assert self.created_ticket_add_serializer.external_ref is None + + + + def test_serializer_add_field_remains_default_external_system(self): + """Ensure serializer doesn't allow edit + + For an ADD operation external_system should not be editable + """ + + assert self.created_ticket_add_serializer.external_system is None + + + + def test_serializer_add_field_remains_default_ticket_type(self): + """Ensure serializer doesn't allow edit + + For an ADD operation ticket_type should not be editable + """ + + assert self.created_ticket_add_serializer.ticket_type == self.ticket_type_enum + + + + def test_serializer_add_field_remains_default_is_deleted(self): + """Ensure serializer doesn't allow edit + + For an ADD operation is_deleted should not be editable + """ + + assert self.created_ticket_add_serializer.is_deleted == False + + + + def test_serializer_add_field_remains_default_date_closed(self): + """Ensure serializer doesn't allow edit + + For an ADD operation date_closed should not be editable + """ + + assert self.created_ticket_add_serializer.date_closed is None + + + + def test_serializer_add_field_remains_default_planned_start_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation planned_start_date should not be editable + """ + + assert self.created_ticket_add_serializer.planned_start_date is None + + + + def test_serializer_add_field_remains_default_planned_finish_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation planned_finish_date should not be editable + """ + + assert self.created_ticket_add_serializer.planned_finish_date is None + + + + def test_serializer_add_field_remains_default_real_start_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation real_start_date should not be editable + """ + + assert self.created_ticket_add_serializer.real_start_date is None + + + + def test_serializer_add_field_remains_default_real_finish_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation real_finish_date should not be editable + """ + + assert self.created_ticket_add_serializer.real_finish_date is None + + + + def test_serializer_add_field_remains_default_opened_by(self): + """Ensure serializer doesn't allow edit + + For an ADD operation opened_by should not be editable + """ + + assert self.created_ticket_add_serializer.opened_by.id == self.add_user.id + + + + def test_serializer_add_field_remains_default_milestone(self): + """Ensure serializer doesn't allow edit + + For an ADD operation milestone should not be editable + """ + + assert self.created_ticket_add_serializer.milestone is None + + + + def test_serializer_add_field_remains_default_subscribed_teams(self): + """Ensure serializer doesn't allow edit + + For an ADD operation subscribed_teams should not be editable + """ + + assert len(list(self.created_ticket_add_serializer.subscribed_teams.all())) == 0 + + + + def test_serializer_add_field_remains_default_subscribed_users(self): + """Ensure serializer doesn't allow edit + + For an ADD operation subscribed_users should not be editable + """ + + assert list(self.created_ticket_add_serializer.subscribed_users.all())[0].id == self.add_user.id + + + + def test_serializer_add_field_editable_urgency(self): + """Ensure serializer allows edit + + For an ADD operation urgency should be settable + """ + + assert self.created_ticket_add_serializer.urgency == self.all_fields_data['urgency'] + + + + def test_serializer_add_field_editable_organization(self): + """Ensure serializer allows edit + + For an ADD operation organization should be settable + """ + + assert self.created_ticket_add_serializer.organization.id == self.all_fields_data['organization'] + + + + + + + + + + + + def test_serializer_change_field_remains_default_assigned_teams(self): + """Ensure serializer doesn't allow edit + + For an Change operation assigned_teams should not be editable + """ + + assert len(list(self.created_ticket_change_serializer.assigned_teams.all())) == 0 + + + + def test_serializer_change_field_remains_default_assigned_users(self): + """Ensure serializer doesn't allow edit + + For an Change operation assigned_users should not be editable + """ + + assert len(list(self.created_ticket_change_serializer.assigned_users.all())) == 0 + + + + def test_serializer_change_field_remains_default_category(self): + """Ensure serializer doesn't allow edit + + For an Change operation category should not be editable + """ + + assert self.created_ticket_change_serializer.category_id is None + + + + def test_serializer_change_field_remains_default_created(self): + """Ensure serializer doesn't allow edit + + For an Change operation created should not be editable + """ + + assert ( + str(self.created_ticket_change_serializer.created) is not None + and str(self.created_ticket_change_serializer.created) != self.all_fields_data_change['created'] + ) + + + + def test_serializer_change_field_remains_default_modified(self): + """Ensure serializer doesn't allow edit + + For an Change operation modified should not be editable + """ + + assert ( + str(self.created_ticket_change_serializer.modified) is not None + and str(self.created_ticket_change_serializer.modified) != self.all_fields_data_change['modified'] + ) + + + + def test_serializer_change_field_remains_default_status(self): + """Ensure serializer doesn't allow edit + + For an Change operation status should not be editable + """ + + assert self.created_ticket_change_serializer.status == int(Ticket.TicketStatus.All.NEW) + + + + def test_serializer_change_field_remains_default_estimate(self): + """Ensure serializer doesn't allow edit + + For an Change operation estimate should not be editable + """ + + assert self.created_ticket_change_serializer.estimate == 0 + + + + def test_serializer_change_field_remains_default_duration_ticket(self): + """Ensure serializer doesn't allow edit + + For an Change operation duration_ticket should not be editable + """ + + assert int(self.created_ticket_change_serializer.duration_ticket) == 0 + + + + def test_serializer_change_field_remains_default_impact(self): + """Ensure serializer doesn't allow edit + + For an Change operation impact should not be editable + """ + + assert self.created_ticket_change_serializer.impact == int(Ticket.TicketImpact.VERY_LOW) + + + + def test_serializer_change_field_remains_default_priority(self): + """Ensure serializer doesn't allow edit + + For an Change operation priority should not be editable + """ + + assert self.created_ticket_change_serializer.priority == int(Ticket.TicketPriority.VERY_LOW) + + + + def test_serializer_change_field_remains_default_external_ref(self): + """Ensure serializer doesn't allow edit + + For an Change operation external_ref should not be editable + """ + + assert self.created_ticket_change_serializer.external_ref is None + + + + def test_serializer_change_field_remains_default_external_system(self): + """Ensure serializer doesn't allow edit + + For an Change operation external_system should not be editable + """ + + assert self.created_ticket_change_serializer.external_system is None + + + + def test_serializer_change_field_remains_default_ticket_type(self): + """Ensure serializer doesn't allow edit + + For an Change operation ticket_type should not be editable + """ + + assert self.created_ticket_change_serializer.ticket_type == self.ticket_type_enum + + + + def test_serializer_change_field_remains_default_is_deleted(self): + """Ensure serializer doesn't allow edit + + For an Change operation is_deleted should not be editable + """ + + assert self.created_ticket_change_serializer.is_deleted == False + + + + def test_serializer_change_field_remains_default_date_closed(self): + """Ensure serializer doesn't allow edit + + For an Change operation date_closed should not be editable + """ + + assert self.created_ticket_change_serializer.date_closed is None + + + + def test_serializer_change_field_remains_default_planned_start_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation planned_start_date should not be editable + """ + + assert self.created_ticket_change_serializer.planned_start_date is None + + + + def test_serializer_change_field_remains_default_planned_finish_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation planned_finish_date should not be editable + """ + + assert self.created_ticket_change_serializer.planned_finish_date is None + + + + def test_serializer_change_field_remains_default_real_start_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation real_start_date should not be editable + """ + + assert self.created_ticket_change_serializer.real_start_date is None + + + + def test_serializer_change_field_remains_default_real_finish_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation real_finish_date should not be editable + """ + + assert self.created_ticket_change_serializer.real_finish_date is None + + + + def test_serializer_change_field_remains_default_opened_by(self): + """Ensure serializer doesn't allow edit + + For an Change operation opened_by should not be editable + """ + + assert self.created_ticket_change_serializer.opened_by.id == self.add_user.id + + + + def test_serializer_change_field_remains_default_milestone(self): + """Ensure serializer doesn't allow edit + + For an Change operation milestone should not be editable + """ + + assert self.created_ticket_change_serializer.milestone is None + + + + def test_serializer_change_field_remains_default_subscribed_teams(self): + """Ensure serializer doesn't allow edit + + For an Change operation subscribed_teams should not be editable + """ + + assert len(list(self.created_ticket_change_serializer.subscribed_teams.all())) == 0 + + + + def test_serializer_change_field_remains_default_subscribed_users(self): + """Ensure serializer doesn't allow edit + + For an Change operation subscribed_users should not be editable + """ + + assert list(self.created_ticket_change_serializer.subscribed_users.all())[0].id == self.add_user.id + + + + def test_serializer_change_field_editable_urgency(self): + """Ensure serializer allows edit + + For an Change operation urgency should be settable + """ + + assert self.created_ticket_change_serializer.urgency == self.all_fields_data_change['urgency'] + + + + def test_serializer_change_field_remains_same_project(self): + """Ensure serializer doesn't allow edit + + For an Change operation project should not be editable + """ + + assert self.created_ticket_change_serializer.project.id == self.ticket_for_change.project.id + + + + # def test_serializer_change_field_editable_organization(self): + # """Ensure serializer allows edit + + # For an Change operation organization should be settable + # """ + + # assert self.created_ticket_change_serializer.organization.id == self.all_fields_data_change['organization'] + + + + + + + + + + + + + + + + + + + + + + + + def test_serializer_triage_add_field_remains_default_created(self): + """Ensure serializer doesn't allow edit + + For an ADD operation created should not be editable + """ + + assert ( + str(self.created_ticket_triage_serializer.created) is not None + and str(self.created_ticket_triage_serializer.created) != self.all_fields_data['created'] + ) + + + + def test_serializer_triage_add_field_remains_default_modified(self): + """Ensure serializer doesn't allow edit + + For an ADD operation modified should not be editable + """ + + assert ( + str(self.created_ticket_triage_serializer.modified) is not None + and str(self.created_ticket_triage_serializer.modified) != self.all_fields_data['modified'] + ) + + + + def test_serializer_triage_add_field_remains_default_estimate(self): + """Ensure serializer doesn't allow edit + + For an ADD operation estimate should not be editable + """ + + assert self.created_ticket_triage_serializer.estimate == 0 + + + + def test_serializer_triage_add_field_remains_default_duration_ticket(self): + """Ensure serializer doesn't allow edit + + For an ADD operation duration_ticket should not be editable + """ + + assert int(self.created_ticket_triage_serializer.duration_ticket) == 0 + + + + def test_serializer_triage_add_field_remains_default_external_ref(self): + """Ensure serializer doesn't allow edit + + For an ADD operation external_ref should not be editable + """ + + assert self.created_ticket_triage_serializer.external_ref is None + + + + def test_serializer_triage_add_field_remains_default_external_system(self): + """Ensure serializer doesn't allow edit + + For an ADD operation external_system should not be editable + """ + + assert self.created_ticket_triage_serializer.external_system is None + + + + def test_serializer_triage_add_field_remains_default_ticket_type(self): + """Ensure serializer doesn't allow edit + + For an ADD operation ticket_type should not be editable + """ + + assert self.created_ticket_triage_serializer.ticket_type == self.ticket_type_enum + + + + def test_serializer_triage_add_field_remains_default_is_deleted(self): + """Ensure serializer doesn't allow edit + + For an ADD operation is_deleted should not be editable + """ + + assert self.created_ticket_triage_serializer.is_deleted == False + + + + def test_serializer_triage_add_field_remains_default_date_closed(self): + """Ensure serializer doesn't allow edit + + For an ADD operation date_closed should not be editable + """ + + assert self.created_ticket_triage_serializer.date_closed is None + + + + def test_serializer_triage_add_field_remains_default_planned_start_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation planned_start_date should not be editable + """ + + assert self.created_ticket_triage_serializer.planned_start_date is None + + + + def test_serializer_triage_add_field_remains_default_planned_finish_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation planned_finish_date should not be editable + """ + + assert self.created_ticket_triage_serializer.planned_finish_date is None + + + + def test_serializer_triage_add_field_remains_default_real_start_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation real_start_date should not be editable + """ + + assert self.created_ticket_triage_serializer.real_start_date is None + + + + def test_serializer_triage_add_field_remains_default_real_finish_date(self): + """Ensure serializer doesn't allow edit + + For an ADD operation real_finish_date should not be editable + """ + + assert self.created_ticket_triage_serializer.real_finish_date is None + + + + def test_serializer_triage_add_field_remains_default_opened_by(self): + """Ensure serializer doesn't allow edit + + For an ADD operation opened_by should not be editable + """ + + assert self.created_ticket_triage_serializer.opened_by.id == self.add_user.id + + + + def test_serializer_triage_add_field_editable_urgency(self): + """Ensure serializer allows edit + + For an ADD operation urgency should be settable + """ + + assert self.created_ticket_triage_serializer.urgency == self.all_fields_data['urgency'] + + + + def test_serializer_triage_add_field_editable_organization(self): + """Ensure serializer allows edit + + For an ADD operation organization should be settable + """ + + assert self.created_ticket_triage_serializer.organization.id == self.all_fields_data['organization'] + + + + + + + + + + + + def test_serializer_triage_add_field_editable_assigned_teams(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) assigned_teams should be settable + """ + + assert list(self.created_ticket_triage_serializer.assigned_teams.all())[0].id == self.all_fields_data_triage['assigned_teams'][0] + + + + def test_serializer_triage_add_field_editable_assigned_users(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) assigned_users should be settable + """ + + assert list(self.created_ticket_triage_serializer.assigned_users.all())[0].id == self.all_fields_data_triage['assigned_users'][0] + + + def test_serializer_triage_add_field_editable_category(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) category_id should be settable + """ + + assert self.created_ticket_triage_serializer.category_id == self.all_fields_data_triage['category'] + + + + @pytest.mark.skip( reason = 'When a ticket is assigned the status changes. rewrite test to create ticket with serializer then edit field.') + def test_serializer_triage_add_field_editable_status(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) status should be settable + """ + + assert int(self.created_ticket_triage_serializer.status) == int(self.all_fields_data_triage['status']) + + + + def test_serializer_triage_add_field_editable_impact(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) impact should be settable + """ + + assert self.created_ticket_triage_serializer.impact == self.all_fields_data_triage['impact'] + + + + def test_serializer_triage_add_field_editable_priority(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) priority should be settable + """ + + assert self.created_ticket_triage_serializer.priority == self.all_fields_data_triage['priority'] + + + + def test_serializer_triage_add_field_editable_milestone(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) milestone should be settable + """ + + assert self.created_ticket_triage_serializer.milestone.id == self.all_fields_data_triage['milestone'] + + + + def test_serializer_triage_add_field_editable_subscribed_teams(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) subscribed_teams should be settable + """ + + assert list(self.created_ticket_triage_serializer.subscribed_teams.all())[0].id == self.all_fields_data_triage['subscribed_teams'][0] + + + + def test_serializer_triage_add_field_editable_subscribed_users(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) subscribed_users should be settable + """ + + assert ( + len(list(self.created_ticket_triage_serializer.subscribed_users.all())) == 2 + and list(self.created_ticket_triage_serializer.subscribed_users.all())[1].id == self.all_fields_data_triage['subscribed_users'][0] + ) + + + + + + + + + + + + + + + + + + + + + + def test_serializer_triage_change_field_remains_default_created(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) created should not be editable + """ + + assert ( + str(self.changed_ticket_triage_serializer.created) is not None + and str(self.changed_ticket_triage_serializer.created) != self.all_fields_data['created'] + ) + + + + def test_serializer_triage_change_field_remains_default_modified(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) modified should not be editable + """ + + assert ( + str(self.changed_ticket_triage_serializer.modified) is not None + and str(self.changed_ticket_triage_serializer.modified) != self.all_fields_data['modified'] + ) + + + + def test_serializer_triage_change_field_remains_default_estimate(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) estimate should not be editable + """ + + assert self.changed_ticket_triage_serializer.estimate == 0 + + + + def test_serializer_triage_change_field_remains_default_duration_ticket(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) duration_ticket should not be editable + """ + + assert int(self.changed_ticket_triage_serializer.duration_ticket) == 0 + + + + def test_serializer_triage_change_field_remains_default_external_ref(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) external_ref should not be editable + """ + + assert self.changed_ticket_triage_serializer.external_ref is None + + + + def test_serializer_triage_change_field_remains_default_external_system(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) external_system should not be editable + """ + + assert self.changed_ticket_triage_serializer.external_system is None + + + + def test_serializer_triage_change_field_remains_default_ticket_type(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) ticket_type should not be editable + """ + + assert self.changed_ticket_triage_serializer.ticket_type == self.ticket_type_enum + + + + def test_serializer_triage_change_field_remains_default_is_deleted(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) is_deleted should not be editable + """ + + assert self.changed_ticket_triage_serializer.is_deleted == False + + + + def test_serializer_triage_change_field_remains_default_date_closed(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) date_closed should not be editable + """ + + assert self.changed_ticket_triage_serializer.date_closed is None + + + + def test_serializer_triage_change_field_remains_default_planned_start_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) planned_start_date should not be editable + """ + + assert self.changed_ticket_triage_serializer.planned_start_date is None + + + + def test_serializer_triage_change_field_remains_default_planned_finish_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) planned_finish_date should not be editable + """ + + assert self.changed_ticket_triage_serializer.planned_finish_date is None + + + + def test_serializer_triage_change_field_remains_default_real_start_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) real_start_date should not be editable + """ + + assert self.changed_ticket_triage_serializer.real_start_date is None + + + + def test_serializer_triage_change_field_remains_default_real_finish_date(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) real_finish_date should not be editable + """ + + assert self.changed_ticket_triage_serializer.real_finish_date is None + + + + def test_serializer_triage_change_field_remains_default_opened_by(self): + """Ensure serializer doesn't allow edit + + For an Change operation (triage serializer) opened_by should not be editable + """ + + assert self.changed_ticket_triage_serializer.opened_by.id == self.add_user.id + + + + def test_serializer_triage_change_field_editable_urgency(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) urgency should be settable + """ + + assert self.changed_ticket_triage_serializer.urgency == self.all_fields_data['urgency'] + + + + @pytest.mark.skip( reason = 'When a ticket is assigned the status changes. rewrite test to create ticket with serializer then edit field.') + def test_serializer_triage_change_field_editable_organization(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) organization should be settable + """ + + assert self.changed_ticket_triage_serializer.organization.id == self.all_fields_data['organization'] + + + + + + + + + + + + def test_serializer_triage_change_field_editable_assigned_teams(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) assigned_teams should be settable + """ + + assert list(self.changed_ticket_triage_serializer.assigned_teams.all())[0].id == self.all_fields_data_triage_change['assigned_teams'][0] + + + + def test_serializer_triage_change_field_editable_assigned_users(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) assigned_users should be settable + """ + + assert list(self.changed_ticket_triage_serializer.assigned_users.all())[0].id == self.all_fields_data_triage_change['assigned_users'][0] + + + def test_serializer_triage_change_field_editable_category(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) category_id should be settable + """ + + assert self.changed_ticket_triage_serializer.category_id == self.all_fields_data_triage_change['category'] + + + + @pytest.mark.skip( reason = 'When a ticket is assigned the status changes. rewrite test to create ticket with serializer then edit field.') + def test_serializer_triage_change_field_editable_status(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) status should be settable + """ + + assert int(self.changed_ticket_triage_serializer.status) == int(self.all_fields_data_triage_change['status']) + + + + def test_serializer_triage_change_field_editable_impact(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) impact should be settable + """ + + assert self.changed_ticket_triage_serializer.impact == self.all_fields_data_triage_change['impact'] + + + + def test_serializer_triage_change_field_editable_priority(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) priority should be settable + """ + + assert self.changed_ticket_triage_serializer.priority == self.all_fields_data_triage_change['priority'] + + + + def test_serializer_triage_change_field_editable_milestone(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) milestone should be settable + """ + + assert self.changed_ticket_triage_serializer.milestone.id == self.all_fields_data_triage_change['milestone'] + + + + def test_serializer_triage_change_field_editable_subscribed_teams(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) subscribed_teams should be settable + """ + + assert list(self.changed_ticket_triage_serializer.subscribed_teams.all())[0].id == self.all_fields_data_triage_change['subscribed_teams'][0] + + + + def test_serializer_triage_change_field_editable_subscribed_users(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) subscribed_users should be settable + """ + + assert ( + len(list(self.changed_ticket_triage_serializer.subscribed_users.all())) == 1 + and list(self.changed_ticket_triage_serializer.subscribed_users.all())[0].id == self.all_fields_data_triage_change['subscribed_users'][0] + ) + + + + + + + + + + + + def test_serializer_import_add_field_editable_created(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) created should be settable + """ + + assert str(self.created_ticket_import_serializer.created) == str(self.all_fields_data_import['created']).replace('T', ' ').replace('Z', '+00:00') + + + @pytest.mark.skip( reason = 'any edit to object updates the field' ) + def test_serializer_import_add_field_editable_modified(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) modified should be settable + """ + + assert str(self.created_ticket_import_serializer.modified) == str(self.all_fields_data_import['modified']).replace('T', ' ').replace('Z', '+00:00') + + + + def test_serializer_import_add_field_editable_estimate(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) estimate should be settable + """ + + assert self.created_ticket_import_serializer.estimate == self.all_fields_data_import['estimate'] + + + + # def test_serializer_import_add_field_editable_duration_ticket(self): + # """Ensure serializer allows edit + + # For an Add operation (import serializer) duration_ticket should be settable + # """ + + # assert int(self.created_ticket_import_serializer.duration_ticket) == 'fixme not editable' + + + def test_serializer_import_add_field_remains_default_duration_ticket(self): + """Ensure serializer doesn't allow edit + + For an Add operation (import serializer) duration_ticket should not be editable + """ + + assert int(self.created_ticket_import_serializer.duration_ticket) == 0 + + + + + + def test_serializer_import_add_field_editable_external_ref(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) external_ref should be settable + """ + + assert self.created_ticket_import_serializer.external_ref == self.all_fields_data_import['external_ref'] + + + + def test_serializer_import_add_field_editable_external_system(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) external_system should be settable + """ + + assert self.created_ticket_import_serializer.external_system == self.all_fields_data_import['external_system'] + + + + def test_serializer_import_add_field_editable_ticket_type(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) ticket_type should be settable + """ + + assert self.created_ticket_import_serializer.ticket_type == self.ticket_type_enum + + + + def test_serializer_import_add_field_editable_is_deleted(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) is_deleted should be settable + """ + + assert self.created_ticket_import_serializer.is_deleted == self.all_fields_data_import['is_deleted'] + + + + def test_serializer_import_add_field_editable_date_closed(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) date_closed should be settable + """ + + assert str(self.created_ticket_import_serializer.date_closed) == str(self.all_fields_data_import['date_closed']).replace('T', ' ').replace('Z', '+00:00') + + + + def test_serializer_import_add_field_editable_planned_start_date(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) planned_start_date should be settable + """ + + assert str(self.created_ticket_import_serializer.planned_start_date) == str(self.all_fields_data_import['planned_start_date']).replace('T', ' ').replace('Z', '+00:00') + + + + def test_serializer_import_add_field_editable_planned_finish_date(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) planned_finish_date should be settable + """ + + assert str(self.created_ticket_import_serializer.planned_finish_date) == str(self.all_fields_data_import['planned_finish_date']).replace('T', ' ').replace('Z', '+00:00') + + + + def test_serializer_import_add_field_editable_real_start_date(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) real_start_date should be settable + """ + + assert str(self.created_ticket_import_serializer.real_start_date) == str(self.all_fields_data_import['real_start_date']).replace('T', ' ').replace('Z', '+00:00') + + + + def test_serializer_import_add_field_editable_real_finish_date(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) real_finish_date should be settable + """ + + assert str(self.created_ticket_import_serializer.real_finish_date) == str(self.all_fields_data_import['real_finish_date']).replace('T', ' ').replace('Z', '+00:00') + + + + def test_serializer_import_add_field_editable_opened_by(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) opened_by should be settable + """ + + assert self.created_ticket_import_serializer.opened_by.id == self.all_fields_data_import['opened_by'] + + + + def test_serializer_import_add_field_editable_urgency(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) urgency should be settable + """ + + assert self.created_ticket_import_serializer.urgency == self.all_fields_data_import['urgency'] + + + + def test_serializer_import_add_field_editable_organization(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) organization should be settable + """ + + assert self.created_ticket_import_serializer.organization.id == self.all_fields_data_import['organization'] + + + + + + def test_serializer_import_add_field_editable_assigned_teams(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) assigned_teams should be settable + """ + + assert list(self.created_ticket_import_serializer.assigned_teams.all())[0].id == self.all_fields_data_triage['assigned_teams'][0] + + + + def test_serializer_import_add_field_editable_assigned_users(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) assigned_users should be settable + """ + + assert list(self.created_ticket_import_serializer.assigned_users.all())[0].id == self.all_fields_data_triage['assigned_users'][0] + + + def test_serializer_import_add_field_editable_category(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) category_id should be settable + """ + + assert self.created_ticket_import_serializer.category_id == self.all_fields_data_triage['category'] + + + + @pytest.mark.skip( reason = 'When a ticket is assigned the status changes. rewrite test to create ticket with serializer then edit field.') + def test_serializer_import_add_field_editable_status(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) status should be settable + """ + + assert int(self.created_ticket_import_serializer.status) == int(self.all_fields_data_triage['status']) + + + + def test_serializer_import_add_field_editable_impact(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) impact should be settable + """ + + assert self.created_ticket_import_serializer.impact == self.all_fields_data_triage['impact'] + + + + def test_serializer_import_add_field_editable_priority(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) priority should be settable + """ + + assert self.created_ticket_import_serializer.priority == self.all_fields_data_triage['priority'] + + + + def test_serializer_import_add_field_editable_milestone(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) milestone should be settable + """ + + assert self.created_ticket_import_serializer.milestone.id == self.all_fields_data_triage['milestone'] + + + + def test_serializer_import_add_field_editable_subscribed_teams(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) subscribed_teams should be settable + """ + + assert list(self.created_ticket_import_serializer.subscribed_teams.all())[0].id == self.all_fields_data_triage['subscribed_teams'][0] + + + + def test_serializer_import_add_field_editable_subscribed_users(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) subscribed_users should be settable + """ + + assert ( + len(list(self.created_ticket_import_serializer.subscribed_users.all())) == 2 + and list(self.created_ticket_import_serializer.subscribed_users.all())[1].id == self.all_fields_data_triage['subscribed_users'][0] + ) + + + + + + From ed872417631a20b3fd1d1b4f24f1aee7ae87c615 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:52:24 +0930 Subject: [PATCH 462/617] test(core): Request Ticket API v2 Serializer Checks ref: #15 #248 #368 #378 --- .../test_ticket_request_serializer.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 app/assistance/tests/unit/ticket_request/test_ticket_request_serializer.py diff --git a/app/assistance/tests/unit/ticket_request/test_ticket_request_serializer.py b/app/assistance/tests/unit/ticket_request/test_ticket_request_serializer.py new file mode 100644 index 000000000..08284ddba --- /dev/null +++ b/app/assistance/tests/unit/ticket_request/test_ticket_request_serializer.py @@ -0,0 +1,83 @@ +import pytest + +from django.test import TestCase + +from assistance.serializers.request import ( + Ticket, + RequestAddTicketModelSerializer, + RequestChangeTicketModelSerializer, + RequestImportTicketModelSerializer, + RequestTriageTicketModelSerializer +) + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_category import TicketCategory + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone + + +from core.tests.abstract.test_ticket_serializer import TicketValidationAPI + + + +class RequestTicketValidationAPI( + TicketValidationAPI, + TestCase, +): + + add_serializer = RequestAddTicketModelSerializer + change_serializer = RequestChangeTicketModelSerializer + import_serializer = RequestImportTicketModelSerializer + triage_serializer = RequestTriageTicketModelSerializer + + ticket_type = 'request' + + ticket_type_enum = Ticket.TicketType.REQUEST + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + + + def test_serializer_add_field_remains_default_project(self): + """Ensure serializer doesn't allow edit + + For an ADD operation project should not be editable + """ + + assert self.created_ticket_add_serializer.project is None + + + + def test_serializer_triage_add_field_remains_default_project(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) project should be settable + """ + + assert self.created_ticket_triage_serializer.project.id == self.all_fields_data_triage['project'] + + + + def test_serializer_triage_change_field_remains_default_project(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_triage_change['project'] + + + + def test_serializer_import_add_field_editable_project(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_import['project'] + + From 84dafcae87589c1c3f650ba4c67294c553a29240 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:52:54 +0930 Subject: [PATCH 463/617] test(itim): Change Ticket API v2 Serializer Checks ref: #15 #248 #368 #378 --- .../test_ticket_change_serializer.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/itim/tests/unit/ticket_change/test_ticket_change_serializer.py diff --git a/app/itim/tests/unit/ticket_change/test_ticket_change_serializer.py b/app/itim/tests/unit/ticket_change/test_ticket_change_serializer.py new file mode 100644 index 000000000..a85ed7b8c --- /dev/null +++ b/app/itim/tests/unit/ticket_change/test_ticket_change_serializer.py @@ -0,0 +1,80 @@ +import pytest + +from django.test import TestCase + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_category import TicketCategory + +from itim.serializers.change import ( + ChangeAddTicketModelSerializer, + ChangeChangeTicketModelSerializer, + ChangeImportTicketModelSerializer, + ChangeTriageTicketModelSerializer, +) + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone + +from core.tests.abstract.test_ticket_serializer import TicketValidationAPI + + + +class ChangeTicketValidationAPI( + TicketValidationAPI, + TestCase, +): + + add_serializer = ChangeAddTicketModelSerializer + change_serializer = ChangeChangeTicketModelSerializer + import_serializer = ChangeImportTicketModelSerializer + triage_serializer = ChangeTriageTicketModelSerializer + + ticket_type = 'change' + + ticket_type_enum = Ticket.TicketType.CHANGE + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + + + def test_serializer_add_field_remains_default_project(self): + """Ensure serializer doesn't allow edit + + For an ADD operation project should not be editable + """ + + assert self.created_ticket_add_serializer.project is None + + + + def test_serializer_triage_add_field_remains_default_project(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) project should be settable + """ + + assert self.created_ticket_triage_serializer.project.id == self.all_fields_data_triage['project'] + + + + def test_serializer_triage_change_field_remains_default_project(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_triage_change['project'] + + + + def test_serializer_import_add_field_editable_project(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_import['project'] + From 80575e02c7be173386ac64f186c29e33bc1b84aa Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:53:07 +0930 Subject: [PATCH 464/617] test(itim): Problem Ticket API v2 Serializer Checks ref: #15 #248 #368 #378 --- .../test_ticket_problem_serializer.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/itim/tests/unit/ticket_problem/test_ticket_problem_serializer.py diff --git a/app/itim/tests/unit/ticket_problem/test_ticket_problem_serializer.py b/app/itim/tests/unit/ticket_problem/test_ticket_problem_serializer.py new file mode 100644 index 000000000..81a72575d --- /dev/null +++ b/app/itim/tests/unit/ticket_problem/test_ticket_problem_serializer.py @@ -0,0 +1,79 @@ +import pytest + +from django.test import TestCase + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_category import TicketCategory + +from itim.serializers.problem import ( + ProblemAddTicketModelSerializer, + ProblemChangeTicketModelSerializer, + ProblemImportTicketModelSerializer, + ProblemTriageTicketModelSerializer, +) + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone + +from core.tests.abstract.test_ticket_serializer import TicketValidationAPI + + + +class ProblemTicketValidationAPI( + TicketValidationAPI, + TestCase, +): + + add_serializer = ProblemAddTicketModelSerializer + change_serializer = ProblemChangeTicketModelSerializer + import_serializer = ProblemImportTicketModelSerializer + triage_serializer = ProblemTriageTicketModelSerializer + + ticket_type = 'problem' + + ticket_type_enum = Ticket.TicketType.PROBLEM + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + + + def test_serializer_add_field_remains_default_project(self): + """Ensure serializer doesn't allow edit + + For an ADD operation project should not be editable + """ + + assert self.created_ticket_add_serializer.project is None + + + + def test_serializer_triage_add_field_remains_default_project(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) project should be settable + """ + + assert self.created_ticket_triage_serializer.project.id == self.all_fields_data_triage['project'] + + + + def test_serializer_triage_change_field_remains_default_project(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_triage_change['project'] + + + + def test_serializer_import_add_field_editable_project(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_import['project'] From 5e6d675cb9a7c37df5d5a03344c0d046cb884175 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:53:16 +0930 Subject: [PATCH 465/617] test(itim): Incident Ticket API v2 Serializer Checks ref: #15 #248 #368 #378 --- .../test_ticket_incident_serializer.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/itim/tests/unit/ticket_incident/test_ticket_incident_serializer.py diff --git a/app/itim/tests/unit/ticket_incident/test_ticket_incident_serializer.py b/app/itim/tests/unit/ticket_incident/test_ticket_incident_serializer.py new file mode 100644 index 000000000..7819f77f4 --- /dev/null +++ b/app/itim/tests/unit/ticket_incident/test_ticket_incident_serializer.py @@ -0,0 +1,80 @@ +import pytest + +from django.test import TestCase + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_category import TicketCategory + +from itim.serializers.incident import ( + IncidentAddTicketModelSerializer, + IncidentChangeTicketModelSerializer, + IncidentImportTicketModelSerializer, + IncidentTriageTicketModelSerializer +) + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone + +from core.tests.abstract.test_ticket_serializer import TicketValidationAPI + + + +class IncidentTicketValidationAPI( + TicketValidationAPI, + TestCase, +): + + add_serializer = IncidentAddTicketModelSerializer + change_serializer = IncidentChangeTicketModelSerializer + import_serializer = IncidentImportTicketModelSerializer + triage_serializer = IncidentTriageTicketModelSerializer + + ticket_type = 'incident' + + ticket_type_enum = Ticket.TicketType.INCIDENT + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + + + def test_serializer_add_field_remains_default_project(self): + """Ensure serializer doesn't allow edit + + For an ADD operation project should not be editable + """ + + assert self.created_ticket_add_serializer.project is None + + + + def test_serializer_triage_add_field_remains_default_project(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) project should be settable + """ + + assert self.created_ticket_triage_serializer.project.id == self.all_fields_data_triage['project'] + + + + def test_serializer_triage_change_field_remains_default_project(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_triage_change['project'] + + + + def test_serializer_import_add_field_editable_project(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_import['project'] + From 7a4edc69ba5b24b18534adf8afaf5b754b3bdb98 Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 4 Nov 2024 00:53:40 +0930 Subject: [PATCH 466/617] test(project_management): Project Task API v2 Serializer Checks ref: #15 #248 #378 closes #368 --- .../test_project_task_serializer.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/project_management/tests/unit/project_task/test_project_task_serializer.py diff --git a/app/project_management/tests/unit/project_task/test_project_task_serializer.py b/app/project_management/tests/unit/project_task/test_project_task_serializer.py new file mode 100644 index 000000000..b536ea9c1 --- /dev/null +++ b/app/project_management/tests/unit/project_task/test_project_task_serializer.py @@ -0,0 +1,80 @@ +import pytest + +from django.test import TestCase + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_category import TicketCategory + +from project_management.models.projects import Project +from project_management.models.project_milestone import ProjectMilestone +from project_management.serializers.project_task import ( + ProjectTaskAddTicketModelSerializer, + ProjectTaskChangeTicketModelSerializer, + ProjectTaskImportTicketModelSerializer, + ProjectTaskTriageTicketModelSerializer, +) + + +from core.tests.abstract.test_ticket_serializer import TicketValidationAPI + + + +class ProjectTaskTicketValidationAPI( + TicketValidationAPI, + TestCase, +): + + add_serializer = ProjectTaskAddTicketModelSerializer + change_serializer = ProjectTaskChangeTicketModelSerializer + import_serializer = ProjectTaskImportTicketModelSerializer + triage_serializer = ProjectTaskTriageTicketModelSerializer + + ticket_type = 'project_task' + + ticket_type_enum = Ticket.TicketType.PROJECT_TASK + + @classmethod + def setUpTestData(self): + + super().setUpTestData() + + + + def test_serializer_add_field_editable_project(self): + """Ensure serializer allows edit + + For an ADD operation project should be settable + """ + + assert self.created_ticket_add_serializer.project.id == self.all_fields_data['project'] + + + + def test_serializer_triage_add_field_remains_default_project(self): + """Ensure serializer allows edit + + For an ADD operation (triage serializer) project should be settable + """ + + assert self.created_ticket_triage_serializer.project.id == self.all_fields_data_triage['project'] + + + + def test_serializer_triage_change_field_remains_default_project(self): + """Ensure serializer allows edit + + For an Change operation (triage serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_triage_change['project'] + + + + def test_serializer_import_add_field_editable_project(self): + """Ensure serializer allows edit + + For an Add operation (import serializer) project should be settable + """ + + assert self.changed_ticket_triage_serializer.project.id == self.all_fields_data_import['project'] + From 72fe8b8422215d22b045e7110d400f431906807d Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 17:04:14 +0930 Subject: [PATCH 467/617] test(core): Related Item Ticket Slash command checks. ref: #15 #248 #376 #381 --- .../test_slash_command_related.py | 496 ++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 app/core/tests/unit/slash_commands/test_slash_command_related.py diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py new file mode 100644 index 000000000..b777d2e40 --- /dev/null +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -0,0 +1,496 @@ +import pytest + +from django.contrib.auth.models import User +from django.test import TestCase + +from access.models import Organization + +from core.models.ticket.ticket import Ticket +from core.models.ticket.ticket_comment import TicketComment +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +from itam.models.device import Device +from itam.models.software import Software + + +class SlashCommands: + """Slash Command Test cases + + Test cases designed for testing scenarios: + - Ticket Comment, single command single item + - Ticket Comment, single command multiple items + - Ticket Comment, multiple command single item + - Ticket Description, single command single item + - Ticket Description, single command multiple items + - Ticket Description, multiple command single item + + Tests ensure the commands work and that command is removed from the location it + was used. parent test classes must check: + + - slash commend item does not exist in comment + - slash commend item does not exist in ticket body + - slash commend added to item/data to the correct location for ticket body + - slash commend added to item/data to the correct location for ticket comment + """ + + slash_command: str = None + """Slash command to test""" + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create ticket + 2. Create another ticket with the slash command within the description. + 3. create a ticket comment with the slash command within the comment body. + """ + + self.user = User.objects.create_user(username="test_user_add", password="password") + + + self.ticket = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + # + # single_command_single_item + # + + self.ticket_single_command_single_item = Ticket.objects.create( + organization = self.organization, + title = 'single_command_single_item ' + self.slash_command + ' ticket body command', + description = "the ticket body\r\n" + self.command_single_command_single_item + "\r\n", + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + self.comment_single_command_single_item = TicketComment.objects.create( + ticket = self.ticket, + comment_type = TicketComment.CommentType.COMMENT, + body = "random text\r\n" + self.command_single_command_single_item + "\r\n" + ) + + + # + # single_command_multiple_item + # + + self.ticket_single_command_multiple_item = Ticket.objects.create( + organization = self.organization, + title = 'single_command_multiple_item ' + self.slash_command + ' ticket body command', + description = "the ticket body\r\n" + self.command_single_command_multiple_item + "\r\n", + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + self.single_command_multiple_item = TicketComment.objects.create( + ticket = self.ticket, + comment_type = TicketComment.CommentType.COMMENT, + body = "random text\r\n" + self.command_single_command_multiple_item + "\r\n" + ) + + + # + # multiple_command_single_item + # + + self.ticket_multiple_command_single_item = Ticket.objects.create( + organization = self.organization, + title = 'multiple_command_single_item ' + self.slash_command + ' ticket body command', + description = "the ticket body\r\n" + self.command_multiple_command_single_item + "\r\n", + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + self.multiple_command_single_item = TicketComment.objects.create( + ticket = self.ticket, + comment_type = TicketComment.CommentType.COMMENT, + body = "random text\r\n" + self.command_multiple_command_single_item + "\r\n" + ) + + + + def test_slash_command_comment_single_command_single_item_comment_command_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + assert '/' + self.slash_command not in self.comment_single_command_single_item.body + + + def test_slash_command_ticket_single_command_single_item_comment_command_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the ticket + """ + + assert '/' + self.slash_command not in self.ticket_single_command_single_item.description + + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_comment_command_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + assert '/' + self.slash_command not in self.single_command_multiple_item.body + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_comment_command_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert '/' + self.slash_command not in self.ticket_single_command_multiple_item.description + + + + + def test_slash_command_comment_multiple_command_single_item_comment_command_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + assert '/' + self.slash_command not in self.multiple_command_single_item.body + + + + def test_slash_command_ticket_multiple_command_single_item_comment_command_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + assert '/' + self.slash_command not in self.ticket_multiple_command_single_item.description + + + +class RelatedItemSlashCommand( + SlashCommands, + TestCase, +): + """Related Item test cases. + + Must test the following: + + - Can link an item via ticket + - Can link an item via ticket comment + - Can link multiple items via ticket (single command, multiple items) + - Can link multiple items via ticket comment (single command, multiple items) + - Can link multiple items via ticket (multiple commands, single item) + - Can link multiple items via ticket comment (multiple commands, single item) + + Args: + SlashCommands (class): Test cases common to ALL slash commands. + """ + + + slash_command = 'link' + + + @classmethod + def setUpTestData(self): + + + organization = Organization.objects.create(name='test_org ' + self.slash_command) + + self.organization = organization + + + self.device = Device.objects.create( + organization=organization, + name = 'device-' + self.slash_command + ) + + self.device_two = Device.objects.create( + organization=organization, + name = 'device-two-' + self.slash_command + ) + + self.device_three = Device.objects.create( + organization=organization, + name = 'device-three-' + self.slash_command + ) + + self.software = Software.objects.create( + organization=organization, + name = 'software ' + self.slash_command + ) + + self.software_two = Software.objects.create( + organization=organization, + name = 'software two ' + self.slash_command + ) + + self.item_one = "$device-"+ str(self.device.id) + self.item_two = "$software-"+ str(self.software.id) + self.item_three = "$device-"+ str(self.device_two.id) + self.item_four = "$software-"+ str(self.software_two.id) + self.item_five = "$device-"+ str(self.device_three.id) + + self.command_single_command_single_item = '/' + self.slash_command + ' ' + self.item_one + self.command_single_command_multiple_item = '/' + self.slash_command + ' ' + self.item_two + ' ' + self.item_three + self.command_multiple_command_single_item = '/' + self.slash_command + ' ' + self.item_four + "\r\n/" + self.slash_command + ' ' + self.item_five + + + super().setUpTestData() + + + self.ticket_linked_items = TicketLinkedItem.objects.all() + + + + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + assert self.item_one not in self.comment_single_command_single_item.body + + + + def test_slash_command_comment_single_command_single_item_linked_item_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.DEVICE, + item = self.device.id, + ticket = self.ticket + ) + + assert len(list(linked_item)) == 1 + + + + + def test_slash_command_ticket_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert self.item_one not in self.ticket_single_command_single_item.description + + + + def test_slash_command_ticket_single_command_single_item_linked_item_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the ticket + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.DEVICE, + item = self.device.id, + ticket = self.ticket_single_command_single_item + ) + + assert len(list(linked_item)) == 1 + + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + assert ( + self.item_two not in self.single_command_multiple_item.body + and self.item_three not in self.single_command_multiple_item.body + ) + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_linked_item_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.SOFTWARE, + item = self.software.id, + ticket = self.ticket + ) + + assert len(list(linked_item)) == 1 + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_linked_item_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.DEVICE, + item = self.device_two.id, + ticket = self.ticket + ) + + assert len(list(linked_item)) == 1 + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert ( + self.item_two not in self.ticket_single_command_multiple_item.description + and self.item_three not in self.ticket_single_command_multiple_item.description + ) + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_linked_item_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.SOFTWARE, + item = self.software.id, + ticket = self.ticket_single_command_multiple_item + ) + + assert len(list(linked_item)) == 1 + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_linked_item_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.DEVICE, + item = self.device_two.id, + ticket = self.ticket_single_command_multiple_item + ) + + assert len(list(linked_item)) == 1 + + + + + def test_slash_command_comment_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + assert ( + self.item_four not in self.multiple_command_single_item.body + and self.item_five not in self.multiple_command_single_item.body + ) + + + + def test_slash_command_comment_multiple_command_single_item_linked_item_created_one(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.SOFTWARE, + item = self.software_two.id, + ticket = self.ticket + ) + + assert len(list(linked_item)) == 1 + + + def test_slash_command_comment_multiple_command_single_item_item_created_two(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.DEVICE, + item = self.device_three.id, + ticket = self.ticket + ) + + assert len(list(linked_item)) == 1 + + + + + def test_slash_command_ticket_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + assert ( + self.item_four not in self.ticket_multiple_command_single_item.description + and self.item_five not in self.ticket_multiple_command_single_item.description + ) + + + + def test_slash_command_ticket_multiple_command_single_item_linked_item_created_one(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.SOFTWARE, + item = self.software_two.id, + ticket = self.ticket_multiple_command_single_item + ) + + assert len(list(linked_item)) == 1 + + + def test_slash_command_ticket_multiple_command_single_item_item_created_two(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + linked_item = self.ticket_linked_items.filter( + item_type = TicketLinkedItem.Modules.DEVICE, + item = self.device_three.id, + ticket = self.ticket_multiple_command_single_item + ) + + assert len(list(linked_item)) == 1 From df73e86c887d80c9c164dc0cd685991d9a599178 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 17:51:16 +0930 Subject: [PATCH 468/617] fix(core): Related ticket slash command requires model to be imported ref: #248 #376 #381 --- app/core/lib/slash_commands/related_ticket.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/core/lib/slash_commands/related_ticket.py b/app/core/lib/slash_commands/related_ticket.py index 4e0287c54..e205a2b11 100644 --- a/app/core/lib/slash_commands/related_ticket.py +++ b/app/core/lib/slash_commands/related_ticket.py @@ -1,6 +1,7 @@ import re + class CommandRelatedTicket: # This summary is used for the user documentation """Add to the current ticket a relationship to another ticket. Supports all ticket @@ -41,14 +42,12 @@ def command_related_ticket(self, match) -> str: None: On successfully processing the command """ - a = 'a' - command = match.group('command') ticket_id:int = str(match.group('ticket')) if ticket_id is not None: - from core.serializers.ticket_related import RelatedTicketModelSerializer + from core.serializers.ticket_related import RelatedTickets, RelatedTicketModelSerializer if command == 'relate': From ce170ff9aa41889226b6c9d64ae997dce199fe48 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 18:39:29 +0930 Subject: [PATCH 469/617] test(core): Blocks Slash command Checks. ref: #15 #248 #376 #381 --- .../test_slash_command_related.py | 334 +++++++++++++++++- 1 file changed, 326 insertions(+), 8 deletions(-) diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py index b777d2e40..863e80141 100644 --- a/app/core/tests/unit/slash_commands/test_slash_command_related.py +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -91,7 +91,7 @@ def setUpTestData(self): status = int(Ticket.TicketStatus.All.NEW.value) ) - self.single_command_multiple_item = TicketComment.objects.create( + self.comment_single_command_multiple_item = TicketComment.objects.create( ticket = self.ticket, comment_type = TicketComment.CommentType.COMMENT, body = "random text\r\n" + self.command_single_command_multiple_item + "\r\n" @@ -111,7 +111,7 @@ def setUpTestData(self): status = int(Ticket.TicketStatus.All.NEW.value) ) - self.multiple_command_single_item = TicketComment.objects.create( + self.comment_multiple_command_single_item = TicketComment.objects.create( ticket = self.ticket, comment_type = TicketComment.CommentType.COMMENT, body = "random text\r\n" + self.command_multiple_command_single_item + "\r\n" @@ -146,7 +146,7 @@ def test_slash_command_comment_single_command_multiple_item_comment_command_remo When slash command made, the command (single command multiple item) must be removed from the comment """ - assert '/' + self.slash_command not in self.single_command_multiple_item.body + assert '/' + self.slash_command not in self.comment_single_command_multiple_item.body @@ -168,7 +168,7 @@ def test_slash_command_comment_multiple_command_single_item_comment_command_remo When slash command made, the command (multiple command single item) must be removed from the comment """ - assert '/' + self.slash_command not in self.multiple_command_single_item.body + assert '/' + self.slash_command not in self.comment_multiple_command_single_item.body @@ -319,8 +319,8 @@ def test_slash_command_comment_single_command_multiple_item_comment_item_removed """ assert ( - self.item_two not in self.single_command_multiple_item.body - and self.item_three not in self.single_command_multiple_item.body + self.item_two not in self.comment_single_command_multiple_item.body + and self.item_three not in self.comment_single_command_multiple_item.body ) @@ -415,8 +415,8 @@ def test_slash_command_comment_multiple_command_single_item_comment_item_removed """ assert ( - self.item_four not in self.multiple_command_single_item.body - and self.item_five not in self.multiple_command_single_item.body + self.item_four not in self.comment_multiple_command_single_item.body + and self.item_five not in self.comment_multiple_command_single_item.body ) @@ -494,3 +494,321 @@ def test_slash_command_ticket_multiple_command_single_item_item_created_two(self ) assert len(list(linked_item)) == 1 + + + +class RelatedTicketBlocksSlashCommand( + SlashCommands, + TestCase, +): + """Related Item test cases. + + Must test the following: + + - Can link an item via ticket + - Can link an item via ticket comment + - Can link multiple items via ticket (single command, multiple items) + - Can link multiple items via ticket comment (single command, multiple items) + - Can link multiple items via ticket (multiple commands, single item) + - Can link multiple items via ticket comment (multiple commands, single item) + + - Action comment add for each related ticket. + + Args: + SlashCommands (class): Test cases common to ALL slash commands. + """ + + + slash_command = 'blocks' + + + @classmethod + def setUpTestData(self): + + + organization = Organization.objects.create(name='test_org ' + self.slash_command) + + self.organization = organization + + self.user_two = User.objects.create_user(username="test_user_two", password="password") + + + self.ticket_two = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number two', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_three = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number three', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_four = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number four', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_five = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number five', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_six = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number six', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.item_one = '#' + str(self.ticket_two.id) + self.item_two = '#' + str(self.ticket_three.id) + self.item_three = '#' + str(self.ticket_four.id) + self.item_four = '#' + str(self.ticket_five.id) + self.item_five = '#' + str(self.ticket_six.id) + + self.command_single_command_single_item = '/' + self.slash_command + ' ' + self.item_one + self.command_single_command_multiple_item = '/' + self.slash_command + ' ' + self.item_two + ' ' + self.item_three + self.command_multiple_command_single_item = '/' + self.slash_command + ' ' + self.item_four + "\r\n/" + self.slash_command + ' ' + self.item_five + + + super().setUpTestData() + + + self.ticket_comments = TicketComment.objects.all() + + + + + + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + assert self.item_one not in self.comment_single_command_single_item.body + + + + def test_slash_command_ticket_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert self.item_one not in self.ticket_single_command_single_item.description + + + + + + def test_slash_command_comment_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocking ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_single_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_single_item.id) + ' as blocking ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + assert ( + self.item_two not in self.comment_single_command_multiple_item.body + and self.item_three not in self.comment_single_command_multiple_item.body + ) + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert ( + self.item_two not in self.ticket_single_command_multiple_item.description + and self.item_three not in self.ticket_single_command_multiple_item.description + ) + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocking ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocking ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_multiple_item.id) + ' as blocking ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_multiple_item.id) + ' as blocking ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + + + + def test_slash_command_comment_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + assert ( + self.item_four not in self.comment_multiple_command_single_item.body + and self.item_five not in self.comment_multiple_command_single_item.body + ) + + + + def test_slash_command_ticket_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + assert ( + self.item_four not in self.ticket_multiple_command_single_item.description + and self.item_five not in self.ticket_multiple_command_single_item.description + ) + + + + def test_slash_command_comment_multiple_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocking ' + self.item_four + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_multiple_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocking ' + self.item_five + ) + + assert len(list(comment)) == 1 From 5dabf00980a7a6860829d1d80c08243f6d97b2e3 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 18:40:01 +0930 Subject: [PATCH 470/617] test(core): Blocked by Slash command Checks. ref: #15 #248 #376 #381 --- .../test_slash_command_related.py | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py index 863e80141..92e68eb4d 100644 --- a/app/core/tests/unit/slash_commands/test_slash_command_related.py +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -812,3 +812,322 @@ def test_slash_command_ticket_multiple_command_single_item_action_comment_create ) assert len(list(comment)) == 1 + + + + +class RelatedTicketBlockedBySlashCommand( + SlashCommands, + TestCase, +): + """Related Item test cases. + + Must test the following: + + - Can link an item via ticket + - Can link an item via ticket comment + - Can link multiple items via ticket (single command, multiple items) + - Can link multiple items via ticket comment (single command, multiple items) + - Can link multiple items via ticket (multiple commands, single item) + - Can link multiple items via ticket comment (multiple commands, single item) + + - Action comment add for each related ticket. + + Args: + SlashCommands (class): Test cases common to ALL slash commands. + """ + + + slash_command = 'blocked_by' + + + @classmethod + def setUpTestData(self): + + + organization = Organization.objects.create(name='test_org ' + self.slash_command) + + self.organization = organization + + self.user_two = User.objects.create_user(username="test_user_two", password="password") + + + self.ticket_two = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number two', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_three = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number three', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_four = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number four', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_five = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number five', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_six = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number six', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.item_one = '#' + str(self.ticket_two.id) + self.item_two = '#' + str(self.ticket_three.id) + self.item_three = '#' + str(self.ticket_four.id) + self.item_four = '#' + str(self.ticket_five.id) + self.item_five = '#' + str(self.ticket_six.id) + + self.command_single_command_single_item = '/' + self.slash_command + ' ' + self.item_one + self.command_single_command_multiple_item = '/' + self.slash_command + ' ' + self.item_two + ' ' + self.item_three + self.command_multiple_command_single_item = '/' + self.slash_command + ' ' + self.item_four + "\r\n/" + self.slash_command + ' ' + self.item_five + + + super().setUpTestData() + + + self.ticket_comments = TicketComment.objects.all() + + + + + + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + assert self.item_one not in self.comment_single_command_single_item.body + + + + def test_slash_command_ticket_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert self.item_one not in self.ticket_single_command_single_item.description + + + + + + def test_slash_command_comment_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocked by ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_single_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_single_item.id) + ' as blocked by ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + assert ( + self.item_two not in self.comment_single_command_multiple_item.body + and self.item_three not in self.comment_single_command_multiple_item.body + ) + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert ( + self.item_two not in self.ticket_single_command_multiple_item.description + and self.item_three not in self.ticket_single_command_multiple_item.description + ) + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocked by ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocked by ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_multiple_item.id) + ' as blocked by ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_multiple_item.id) + ' as blocked by ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + + + + def test_slash_command_comment_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + assert ( + self.item_four not in self.comment_multiple_command_single_item.body + and self.item_five not in self.comment_multiple_command_single_item.body + ) + + + + def test_slash_command_ticket_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + assert ( + self.item_four not in self.ticket_multiple_command_single_item.description + and self.item_five not in self.ticket_multiple_command_single_item.description + ) + + + + def test_slash_command_comment_multiple_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocked by ' + self.item_four + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_multiple_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as blocked by ' + self.item_five + ) + + assert len(list(comment)) == 1 From 5ef5103ea9764b4c355b2d695e77346ae6d82a11 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 18:57:00 +0930 Subject: [PATCH 471/617] test(core): Action command Related Item Ticket Slash command checks. ref: #15 #248 --- .../test_slash_command_related.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py index 92e68eb4d..06c2be953 100644 --- a/app/core/tests/unit/slash_commands/test_slash_command_related.py +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -255,6 +255,8 @@ def setUpTestData(self): self.ticket_linked_items = TicketLinkedItem.objects.all() + self.ticket_comments = TicketComment.objects.all() + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): @@ -283,6 +285,24 @@ def test_slash_command_comment_single_command_single_item_linked_item_created(se + def test_slash_command_comment_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + + def test_slash_command_ticket_single_command_single_item_comment_item_removed(self): """Slash command Test Case @@ -310,6 +330,22 @@ def test_slash_command_ticket_single_command_single_item_linked_item_created(sel + def test_slash_command_ticket_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_single_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) def test_slash_command_comment_single_command_multiple_item_comment_item_removed(self): @@ -342,6 +378,38 @@ def test_slash_command_comment_single_command_multiple_item_linked_item_created_ + def test_slash_command_comment_single_command_single_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_comment_single_command_single_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) def test_slash_command_comment_single_command_multiple_item_linked_item_created_two(self): """Slash command Test Case @@ -390,6 +458,40 @@ def test_slash_command_ticket_single_command_multiple_item_linked_item_created_o + + + def test_slash_command_ticket_single_command_single_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked' + self.item_two + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_single_command_single_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked' + self.item_three + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) def test_slash_command_ticket_single_command_multiple_item_linked_item_created_two(self): """Slash command Test Case @@ -436,6 +538,39 @@ def test_slash_command_comment_multiple_command_single_item_linked_item_created_ assert len(list(linked_item)) == 1 + + def test_slash_command_comment_single_command_single_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_four + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_comment_single_command_single_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_five + ) + + assert len(list(comment)) == 1 + + + def test_slash_command_comment_multiple_command_single_item_item_created_two(self): """Slash command Test Case @@ -497,6 +632,38 @@ def test_slash_command_ticket_multiple_command_single_item_item_created_two(self + def test_slash_command_ticket_single_command_single_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_multiple_command_single_item, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_four + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_single_command_single_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_multiple_command_single_item, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked ' + self.item_five + ) + + assert len(list(comment)) == 1 + + + class RelatedTicketBlocksSlashCommand( SlashCommands, TestCase, From e89dff1c2f3de62bdf52e3822b4b7127afa44c47 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 19:40:31 +0930 Subject: [PATCH 472/617] test(core): Ensure a non-existing item cant be Linked to a Ticket. ref: #15 #248 #336 #376 #381 --- .../test_slash_command_related.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py index 06c2be953..452f9023d 100644 --- a/app/core/tests/unit/slash_commands/test_slash_command_related.py +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -257,6 +257,107 @@ def setUpTestData(self): self.ticket_comments = TicketComment.objects.all() + # + # Non existant item + # + + self.ticket_item_not_exist = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket for non-existing item', + description = 'the ticket body' + '/' + self.slash_command + " $device-9999\r\n", + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.comment_item_not_exist = TicketComment.objects.create( + ticket = self.ticket, + comment_type = TicketComment.CommentType.COMMENT, + body = "random text\r\n" + '/' + self.slash_command + " $device-9999\r\n" + ) + + + + + def test_slash_command_comment_non_existing_item_no_link_command_in_comment(self): + """Slash command Test Case + + When slash command made, for an item that does not exist, dont sanitize the command + from the comment + """ + + assert '/' + self.slash_command in self.comment_item_not_exist.body + + + + def test_slash_command_comment_non_existing_item_no_link_item_in_comment(self): + """Slash command Test Case + + When slash command made, for an item that does not exist, dont sanitize the item + from the comment + """ + + assert '$device-9999' in self.comment_item_not_exist.body + + + def test_slash_command_comment_non_existing_item_no_action_comment_created(self): + """Slash command Test Case + + When slash command made, for an item that does not exist, no action command + is to be created + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked $device-9999' + ) + + assert len(list(comment)) == 0 + + + + def test_slash_command_ticket_non_existing_item_no_link_command_in_comment(self): + """Slash command Test Case + + When slash command made, for an item that does not exist, dont sanitize the command + from the ticket body + """ + + assert '/' + self.slash_command in self.ticket_item_not_exist.description + + + + def test_slash_command_ticket_non_existing_item_no_link_item_in_comment(self): + """Slash command Test Case + + When slash command made, for an item that does not exist, dont sanitize the item + from the ticket body + """ + + assert '$device-9999' in self.ticket_item_not_exist.description + + + def test_slash_command_ticket_non_existing_item_no_action_comment_created(self): + """Slash command Test Case + + When slash command made, for an item that does not exist, no action command + is to be created + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_item_not_exist, + comment_type = TicketComment.CommentType.ACTION, + body = 'linked $device-9999' + ) + + assert len(list(comment)) == 0 + + + + + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): From 86d4f7684fbf106dcedb5de76d2f2911ba8587fe Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 22:12:28 +0930 Subject: [PATCH 473/617] feat(core): New signal for cleaning linked ticket items when the item is deleted ref: #15 #248 #336 #376 #381 --- app/core/signal/__init__.py | 0 app/core/signal/ticket_linked_item_delete.py | 22 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 app/core/signal/__init__.py create mode 100644 app/core/signal/ticket_linked_item_delete.py diff --git a/app/core/signal/__init__.py b/app/core/signal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/core/signal/ticket_linked_item_delete.py b/app/core/signal/ticket_linked_item_delete.py new file mode 100644 index 000000000..8154a3acc --- /dev/null +++ b/app/core/signal/ticket_linked_item_delete.py @@ -0,0 +1,22 @@ + +from django import dispatch +from django.dispatch import receiver + +from core.models.ticket.ticket_linked_items import TicketLinkedItem + +deleted_model = dispatch.Signal() + + +@receiver(deleted_model) +def signal_deleted_model(sender, item_id, item_type, **kwargs): + """Clean up model TicketLinkedItems + + a model was deleted, remove its link to any tickets it had. + """ + + items = TicketLinkedItem.objects.filter( + item_type = item_type, + item = item_id + ) + + items.delete() From c415d53708884746e052c67df86e5591bb731a78 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 22:16:07 +0930 Subject: [PATCH 474/617] fix(core): When an item that may be linked to a ticket is deleted, remove the ticket link ref: #15 #248 #336 #376 #381 --- app/config_management/models/groups.py | 10 ++++++++++ app/itam/models/device.py | 12 ++++++++++++ app/itam/models/operating_system.py | 11 +++++++++++ app/itam/models/software.py | 10 ++++++++++ app/itim/models/clusters.py | 11 +++++++++++ app/itim/models/services.py | 11 +++++++++++ 6 files changed, 65 insertions(+) diff --git a/app/config_management/models/groups.py b/app/config_management/models/groups.py index 8b9ae72c5..dc06a8c7c 100644 --- a/app/config_management/models/groups.py +++ b/app/config_management/models/groups.py @@ -1,6 +1,8 @@ import re from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from django.forms import ValidationError from rest_framework.reverse import reverse @@ -11,6 +13,7 @@ from app.helpers.merge_software import merge_software from core.mixin.history_save import SaveHistory +from core.signal.ticket_linked_item_delete import TicketLinkedItem, deleted_model from itam.models.device import Device, DeviceSoftware from itam.models.software import Software, SoftwareVersion @@ -346,6 +349,13 @@ def __str__(self): +@receiver(post_delete, sender=ConfigGroups, dispatch_uid='config_group_delete_signal') +def signal_deleted_model(sender, instance, using, **kwargs): + + deleted_model.send(sender='config_group_deleted', item_id=instance.id, item_type = TicketLinkedItem.Modules.CONFIG_GROUP) + + + class ConfigGroupHosts(GroupsCommonFields, SaveHistory): diff --git a/app/itam/models/device.py b/app/itam/models/device.py index 492086cac..4c6570913 100644 --- a/app/itam/models/device.py +++ b/app/itam/models/device.py @@ -4,6 +4,8 @@ from datetime import timedelta from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from django.forms import ValidationError from rest_framework import serializers @@ -16,6 +18,7 @@ from core.classes.icon import Icon from core.mixin.history_save import SaveHistory +from core.signal.ticket_linked_item_delete import TicketLinkedItem, deleted_model from itam.models.device_common import DeviceCommonFields, DeviceCommonFieldsName from itam.models.device_models import DeviceModel @@ -500,6 +503,15 @@ def get_configuration(self): return config + +@receiver(post_delete, sender=Device, dispatch_uid='device_delete_signal') +def signal_deleted_model(sender, instance, using, **kwargs): + + deleted_model.send(sender='device_deleted', item_id=instance.id, item_type = TicketLinkedItem.Modules.DEVICE) + + + + class DeviceSoftware(DeviceCommonFields, SaveHistory): """ A way for the device owner to configure software to install/remove """ diff --git a/app/itam/models/operating_system.py b/app/itam/models/operating_system.py index 8f592eb21..6810a5578 100644 --- a/app/itam/models/operating_system.py +++ b/app/itam/models/operating_system.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from rest_framework.reverse import reverse @@ -7,6 +9,7 @@ from core.mixin.history_save import SaveHistory from core.models.manufacturer import Manufacturer +from core.signal.ticket_linked_item_delete import TicketLinkedItem, deleted_model @@ -163,6 +166,14 @@ def __str__(self): return self.name + +@receiver(post_delete, sender=OperatingSystem, dispatch_uid='operating_system_delete_signal') +def signal_deleted_model(sender, instance, using, **kwargs): + + deleted_model.send(sender='operating_system_deleted', item_id=instance.id, item_type = TicketLinkedItem.Modules.OPERATING_SYSTEM) + + + class OperatingSystemVersion(OperatingSystemCommonFields, SaveHistory): diff --git a/app/itam/models/software.py b/app/itam/models/software.py index 59ac3c135..e848b3e94 100644 --- a/app/itam/models/software.py +++ b/app/itam/models/software.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from rest_framework.reverse import reverse @@ -7,6 +9,7 @@ from core.mixin.history_save import SaveHistory from core.models.manufacturer import Manufacturer +from core.signal.ticket_linked_item_delete import TicketLinkedItem, deleted_model from settings.models.app_settings import AppSettings @@ -248,6 +251,13 @@ def __str__(self): +@receiver(post_delete, sender=Software, dispatch_uid='software_delete_signal') +def signal_deleted_model(sender, instance, using, **kwargs): + + deleted_model.send(sender='software_deleted', item_id=instance.id, item_type = TicketLinkedItem.Modules.SOFTWARE) + + + class SoftwareVersion(SoftwareCommonFields, SaveHistory): diff --git a/app/itim/models/clusters.py b/app/itim/models/clusters.py index f9702096c..71030f55b 100644 --- a/app/itim/models/clusters.py +++ b/app/itim/models/clusters.py @@ -1,5 +1,7 @@ from django.contrib.auth.models import User from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from django.forms import ValidationError from rest_framework.reverse import reverse @@ -7,6 +9,8 @@ from access.fields import * from access.models import Team, TenancyObject +from core.signal.ticket_linked_item_delete import TicketLinkedItem, deleted_model + from itam.models.device import Device @@ -329,3 +333,10 @@ def rendered_config(self): def __str__(self): return self.name + + + +@receiver(post_delete, sender=Cluster, dispatch_uid='cluster_delete_signal') +def signal_deleted_model(sender, instance, using, **kwargs): + + deleted_model.send(sender='cluster_deleted', item_id=instance.id, item_type = TicketLinkedItem.Modules.CLUSTER) diff --git a/app/itim/models/services.py b/app/itim/models/services.py index c79d26da3..2aff94f48 100644 --- a/app/itim/models/services.py +++ b/app/itim/models/services.py @@ -2,6 +2,8 @@ from django.contrib.auth.models import User from django.db import models +from django.db.models.signals import post_delete +from django.dispatch import receiver from django.forms import ValidationError from rest_framework.reverse import reverse @@ -9,6 +11,8 @@ from access.fields import * from access.models import Team, TenancyObject +from core.signal.ticket_linked_item_delete import TicketLinkedItem, deleted_model + from itam.models.device import Device from itim.models.clusters import Cluster @@ -374,3 +378,10 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields def __str__(self): return self.name + + + +@receiver(post_delete, sender=Service, dispatch_uid='service_delete_signal') +def signal_deleted_model(sender, instance, using, **kwargs): + + deleted_model.send(sender='service_deleted', item_id=instance.id, item_type = TicketLinkedItem.Modules.SERVICE) From 8fbbf124dfb7118c2bbd25436a83ca70d00360c3 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 22:19:00 +0930 Subject: [PATCH 475/617] test(core): Ensure that an item that may be linked to a ticket, when its deleted, the ticket link is removed ref: #15 #248 #376 #381 fixes #336 --- .../test_ticket_linked_item.py | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 app/core/tests/unit/ticket_linked_item/test_ticket_linked_item.py diff --git a/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item.py b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item.py new file mode 100644 index 000000000..ccd829042 --- /dev/null +++ b/app/core/tests/unit/ticket_linked_item/test_ticket_linked_item.py @@ -0,0 +1,228 @@ +from django.contrib.auth.models import User +from django.test import Client, TestCase + +from access.models import Organization + +from core.models.ticket.ticket_linked_items import Ticket, TicketLinkedItem + +from config_management.models.groups import ConfigGroups + +from itam.models.device import Device +from itam.models.operating_system import OperatingSystem +from itam.models.software import Software + +from itim.models.clusters import Cluster +from itim.models.services import Service + + + +class TicketLinkedItemBase: + """ Test Cases common to ALL ticket types """ + + ticket_type_enum = Ticket.TicketType.REQUEST + + item_type_enum = None + + + @classmethod + def CreateOrg(self): + + organization = Organization.objects.create(name='test_org') + + self.organization = organization + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an organization for user and item + 2. Create a ticket + 3. link the item to the ticket + 4. store the item id + 5. delete to item + 6. test to confirm `TicketLinkedItem` was cleaned up + """ + + self.user = User.objects.create_user(username="test_user_view", password="password") + + self.ticket = Ticket.objects.create( + organization = self.organization, + title = 'one', + description = 'some text for body', + opened_by = self.user, + ticket_type = self.ticket_type_enum, + status = Ticket.TicketStatus.All.NEW + ) + + self.item = TicketLinkedItem.objects.create( + organization = self.organization, + item = self.linked_item.id, + item_type = self.item_type_enum, + ticket = self.ticket, + ) + + self.item_id: int = self.linked_item.id + + self.linked_item.delete() + + + + + def test_item_deleted_cleanup(self): + + items_found = TicketLinkedItem.objects.filter( + item_type = self.item_type_enum, + item = self.item_id + ) + + + assert len(list(items_found)) == 0 + + + +class TicketLinkedItemCluster( + TicketLinkedItemBase, + TestCase +): + + item_type_enum = TicketLinkedItem.Modules.CLUSTER + + item_model = Cluster + + + @classmethod + def setUpTestData(self): + + + self.CreateOrg() + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'one', + ) + + super().setUpTestData() + + + + +class TicketLinkedItemConfigGroup( + TicketLinkedItemBase, + TestCase +): + + + item_type_enum = TicketLinkedItem.Modules.CONFIG_GROUP + + item_model = ConfigGroups + + + @classmethod + def setUpTestData(self): + + self.CreateOrg() + + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'one', + ) + + super().setUpTestData() + + + +class TicketLinkedItemDevice( + TicketLinkedItemBase, + TestCase +): + + item_type_enum = TicketLinkedItem.Modules.DEVICE + + item_model = Device + + + @classmethod + def setUpTestData(self): + + self.CreateOrg() + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'one', + ) + + super().setUpTestData() + + + +class TicketLinkedItemOperatingSystem( + TicketLinkedItemBase, + TestCase +): + + item_type_enum = TicketLinkedItem.Modules.OPERATING_SYSTEM + + item_model = OperatingSystem + + + @classmethod + def setUpTestData(self): + + self.CreateOrg() + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'one', + ) + + super().setUpTestData() + + + +class TicketLinkedItemSoftware( + TicketLinkedItemBase, + TestCase +): + + item_type_enum = TicketLinkedItem.Modules.SOFTWARE + + item_model = Software + + + @classmethod + def setUpTestData(self): + + self.CreateOrg() + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'one', + ) + + super().setUpTestData() + + + +class TicketLinkedItemService( + TicketLinkedItemBase, + TestCase +): + + item_type_enum = TicketLinkedItem.Modules.SERVICE + + item_model = Service + + + @classmethod + def setUpTestData(self): + + self.CreateOrg() + + self.linked_item = self.item_model.objects.create( + organization = self.organization, + name = 'one', + ) + + super().setUpTestData() From e28d25b13784881769996828561607e8725e8f82 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 5 Nov 2024 22:27:36 +0930 Subject: [PATCH 476/617] test(core): Relate Slash command Checks. ref: #15 #248 #376 #381 --- .../test_slash_command_related.py | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py index 452f9023d..e0bb9870e 100644 --- a/app/core/tests/unit/slash_commands/test_slash_command_related.py +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -1399,3 +1399,322 @@ def test_slash_command_ticket_multiple_command_single_item_action_comment_create ) assert len(list(comment)) == 1 + + + + +class RelatedTicketRelateSlashCommand( + SlashCommands, + TestCase, +): + """Related Item test cases. + + Must test the following: + + - Can link an item via ticket + - Can link an item via ticket comment + - Can link multiple items via ticket (single command, multiple items) + - Can link multiple items via ticket comment (single command, multiple items) + - Can link multiple items via ticket (multiple commands, single item) + - Can link multiple items via ticket comment (multiple commands, single item) + + - Action comment add for each related ticket. + + Args: + SlashCommands (class): Test cases common to ALL slash commands. + """ + + + slash_command = 'relate' + + + @classmethod + def setUpTestData(self): + + + organization = Organization.objects.create(name='test_org ' + self.slash_command) + + self.organization = organization + + self.user_two = User.objects.create_user(username="test_user_two", password="password") + + + self.ticket_two = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number two', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_three = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number three', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_four = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number four', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_five = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number five', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.ticket_six = Ticket.objects.create( + organization = self.organization, + title = 'A ' + self.slash_command + ' ticket number six', + description = 'the ticket body', + ticket_type = Ticket.TicketType.REQUEST, + opened_by = self.user_two, + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + self.item_one = '#' + str(self.ticket_two.id) + self.item_two = '#' + str(self.ticket_three.id) + self.item_three = '#' + str(self.ticket_four.id) + self.item_four = '#' + str(self.ticket_five.id) + self.item_five = '#' + str(self.ticket_six.id) + + self.command_single_command_single_item = '/' + self.slash_command + ' ' + self.item_one + self.command_single_command_multiple_item = '/' + self.slash_command + ' ' + self.item_two + ' ' + self.item_three + self.command_multiple_command_single_item = '/' + self.slash_command + ' ' + self.item_four + "\r\n/" + self.slash_command + ' ' + self.item_five + + + super().setUpTestData() + + + self.ticket_comments = TicketComment.objects.all() + + + + + + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + assert self.item_one not in self.comment_single_command_single_item.body + + + + def test_slash_command_ticket_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert self.item_one not in self.ticket_single_command_single_item.description + + + + + + def test_slash_command_comment_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as related to ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_single_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_single_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_single_item.id) + ' as related to ' + self.item_one + ) + + assert len(list(comment)) == 1 + + + + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + assert ( + self.item_two not in self.comment_single_command_multiple_item.body + and self.item_three not in self.comment_single_command_multiple_item.body + ) + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert ( + self.item_two not in self.ticket_single_command_multiple_item.description + and self.item_three not in self.ticket_single_command_multiple_item.description + ) + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as related to ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_comment_single_command_multiple_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the comment + """ + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as related to ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_action_comment_created_one(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_multiple_item.id) + ' as related to ' + self.item_two + ) + + assert len(list(comment)) == 1 + + + @pytest.mark.skip( reason = 'Feature to be implemented' ) + def test_slash_command_ticket_single_command_multiple_item_action_comment_created_two(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_multiple_item.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.ticket_single_command_multiple_item.id) + ' as related to ' + self.item_three + ) + + assert len(list(comment)) == 1 + + + + + + def test_slash_command_comment_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + assert ( + self.item_four not in self.comment_multiple_command_single_item.body + and self.item_five not in self.comment_multiple_command_single_item.body + ) + + + + def test_slash_command_ticket_multiple_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + assert ( + self.item_four not in self.ticket_multiple_command_single_item.description + and self.item_five not in self.ticket_multiple_command_single_item.description + ) + + + + def test_slash_command_comment_multiple_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the comment + """ + + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as related to ' + self.item_four + ) + + assert len(list(comment)) == 1 + + + + def test_slash_command_ticket_multiple_command_single_item_action_comment_created(self): + """Slash command Test Case + + When slash command made, the command (multiple command single item) must be removed from the ticket + """ + + + comment = self.ticket_comments.filter( + ticket = self.comment_single_command_single_item.ticket.id, + comment_type = TicketComment.CommentType.ACTION, + body = 'added #' + str(self.comment_single_command_single_item.ticket.id) + ' as related to ' + self.item_five + ) + + assert len(list(comment)) == 1 From da9799cb3cec109f225afa63bccac88a9f55bff3 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 7 Nov 2024 13:46:35 +0930 Subject: [PATCH 477/617] test(core): Spend Slash command Checks. ref: #15 #248 #382 closes #376 --- .../test_slash_command_related.py | 180 +++++++++++++++++- 1 file changed, 171 insertions(+), 9 deletions(-) diff --git a/app/core/tests/unit/slash_commands/test_slash_command_related.py b/app/core/tests/unit/slash_commands/test_slash_command_related.py index e0bb9870e..38cb1dee6 100644 --- a/app/core/tests/unit/slash_commands/test_slash_command_related.py +++ b/app/core/tests/unit/slash_commands/test_slash_command_related.py @@ -1,4 +1,5 @@ import pytest +import unittest from django.contrib.auth.models import User from django.test import TestCase @@ -13,16 +14,12 @@ from itam.models.software import Software -class SlashCommands: +class SlashCommandsCommon: """Slash Command Test cases Test cases designed for testing scenarios: - Ticket Comment, single command single item - - Ticket Comment, single command multiple items - - Ticket Comment, multiple command single item - Ticket Description, single command single item - - Ticket Description, single command multiple items - - Ticket Description, multiple command single item Tests ensure the commands work and that command is removed from the location it was used. parent test classes must check: @@ -138,6 +135,26 @@ def test_slash_command_ticket_single_command_single_item_comment_command_removed +class SlashCommandsMulti( + SlashCommandsCommon +): + + """Slash Command Test cases (Multiple commands) + + Test cases designed for testing scenarios: + - Ticket Comment, single command multiple items + - Ticket Comment, multiple command single item + - Ticket Description, single command multiple items + - Ticket Description, multiple command single item + + Tests ensure the commands work and that command is removed from the location it + was used. parent test classes must check: + + - slash commend item does not exist in comment + - slash commend item does not exist in ticket body + - slash commend added to item/data to the correct location for ticket body + - slash commend added to item/data to the correct location for ticket comment + """ @pytest.mark.skip( reason = 'Feature to be implemented' ) def test_slash_command_comment_single_command_multiple_item_comment_command_removed(self): @@ -183,7 +200,7 @@ def test_slash_command_ticket_multiple_command_single_item_comment_command_remov class RelatedItemSlashCommand( - SlashCommands, + SlashCommandsMulti, TestCase, ): """Related Item test cases. @@ -766,7 +783,7 @@ def test_slash_command_ticket_single_command_single_item_action_comment_created_ class RelatedTicketBlocksSlashCommand( - SlashCommands, + SlashCommandsMulti, TestCase, ): """Related Item test cases. @@ -1085,7 +1102,7 @@ def test_slash_command_ticket_multiple_command_single_item_action_comment_create class RelatedTicketBlockedBySlashCommand( - SlashCommands, + SlashCommandsMulti, TestCase, ): """Related Item test cases. @@ -1404,7 +1421,7 @@ def test_slash_command_ticket_multiple_command_single_item_action_comment_create class RelatedTicketRelateSlashCommand( - SlashCommands, + SlashCommandsMulti, TestCase, ): """Related Item test cases. @@ -1718,3 +1735,148 @@ def test_slash_command_ticket_multiple_command_single_item_action_comment_create ) assert len(list(comment)) == 1 + + + + + +class SpendSlashCommand( + SlashCommandsCommon, + TestCase, +): + """Spend slash command test cases + + Must test the following: + + - Can add duration via ticket + - Can add duration via ticket comment + - Can add duration multiple times via ticket (single command, multiple items) + - Can add duration multiple times via ticket comment (single command, multiple items) + - Can add duration multiple times via ticket (multiple commands, single item) + - Can add duration multiple times via ticket comment (multiple commands, single item) + + Commands with the following formats: + + - 1s + - 1m + - 1h + - 1m 1s + - 1m1s + - 1h 1m 1s + - 1h1m1s + + Args: + SlashCommands (class): Test cases common to ALL slash commands. + """ + + + slash_command = 'spend' + + + @classmethod + def setUpTestData(self): + + + organization = Organization.objects.create(name='test_org ' + self.slash_command) + + self.organization = organization + + + self.item_one = '5m' + self.item_two = '5m' + self.item_three = '10m' + self.item_four = '5m' + self.item_five = '10m' + + self.command_single_command_single_item = '/' + self.slash_command + ' ' + self.item_one + self.command_single_command_multiple_item = '/' + self.slash_command + ' ' + self.item_two + ' ' + self.item_three + self.command_multiple_command_single_item = '/' + self.slash_command + ' ' + self.item_four + "\r\n/" + self.slash_command + ' ' + self.item_five + + + super().setUpTestData() + + + self.ticket_comments = TicketComment.objects.all() + + + + def test_slash_command_comment_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command single item) must be removed from the comment + """ + + assert self.item_one not in self.comment_single_command_single_item.body + + + + def test_slash_command_ticket_single_command_single_item_comment_item_removed(self): + """Slash command Test Case + + When slash command made, the command (single command multiple item) must be removed from the ticket + """ + + assert self.item_one not in self.ticket_single_command_single_item.description + + + + def test_slash_command_comment_single_command_single_item_comment_item_action_comment_correct(self): + """Slash command Test Case + + Ensure that the duration field was correctly updated + """ + + assert self.comment_single_command_single_item.duration == 300 + + + + def test_slash_command_ticket_single_command_single_item_comment_item_action_comment_correct(self): + """Slash command Test Case + + Ensure that the duration field was correctly updated + """ + + comment = self.ticket_comments.filter( + ticket = self.ticket_single_command_single_item, + comment_type = TicketComment.CommentType.ACTION, + body = f'added {self.item_one} of time spent' + ) + + assert list(comment)[0].duration == 300 + + +@pytest.mark.django_db +@pytest.mark.parametrize("test_input,expected", [ + ('1s', 1), + ('1m', 60), + ('1h', 3600), + ('1m 1s', 61), + ('1m1s', 61), + ('1h 1m 1s', 3661), + ('1h1m1s', 3661), +]) +def test_slash_command_spend_comment_time_format_comment_correct(test_input, expected): + """Slash command Test Case + + Ensure that the duration field was correctly updated + """ + + + ticket = Ticket.objects.create( + organization = Organization.objects.create(name='test_org ' + str(expected)), + title = 'single_command_single_item ' + str(expected), + description = "the ticket body", + ticket_type = Ticket.TicketType.REQUEST, + opened_by = User.objects.create_user(username="test_user_add" + str(expected), password="password"), + status = int(Ticket.TicketStatus.All.NEW.value) + ) + + + comment = TicketComment.objects.create( + ticket = ticket, + comment_type = TicketComment.CommentType.COMMENT, + body = f"random text\r\n /spend {test_input}\r\n" + ) + + assert comment.duration == expected + From 89b0a6b00392208bde551fae05dbbbc3aaa510b6 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 7 Nov 2024 13:55:05 +0930 Subject: [PATCH 478/617] fix(core): Correct duration slash command regex ref: #248 #376 #382 --- app/core/lib/slash_commands/duration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/lib/slash_commands/duration.py b/app/core/lib/slash_commands/duration.py index db54409f3..3d273fc5f 100644 --- a/app/core/lib/slash_commands/duration.py +++ b/app/core/lib/slash_commands/duration.py @@ -22,7 +22,7 @@ class Duration: """ - time_spent: str = r'[\s|\n]\/(?P[spend|spent]+)\s(?P