diff --git a/app/access/serializers/organization.py b/app/access/serializers/organization.py index c57c3e30c..6884617f9 100644 --- a/app/access/serializers/organization.py +++ b/app/access/serializers/organization.py @@ -12,7 +12,7 @@ class OrganizationBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class OrganizationModelSerializer(OrganizationBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_organization-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/access/serializers/team_user.py b/app/access/serializers/team_user.py index a910bef85..82e797e7f 100644 --- a/app/access/serializers/team_user.py +++ b/app/access/serializers/team_user.py @@ -12,13 +12,13 @@ class TeamUserBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: return reverse( "v2:_api_v2_organization_team_user-detail", @@ -53,7 +53,7 @@ class TeamUserModelSerializer(TeamUserBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/access/serializers/teams.py b/app/access/serializers/teams.py index 9b4132274..e2c9743f6 100644 --- a/app/access/serializers/teams.py +++ b/app/access/serializers/teams.py @@ -14,13 +14,13 @@ class TeamBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: return reverse( "v2:_api_v2_organization_team-detail", @@ -57,7 +57,7 @@ class TeamModelSerializer(TeamBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/access/viewsets/team.py b/app/access/viewsets/team.py index be9703631..0c3b4fc9b 100644 --- a/app/access/viewsets/team.py +++ b/app/access/viewsets/team.py @@ -1,4 +1,4 @@ -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from access.serializers.teams import ( Team, @@ -15,6 +15,13 @@ create=extend_schema( summary = 'Create a team within this organization', description='', + parameters = [ + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='Allready exists', response=TeamViewSerializer), 201: OpenApiResponse(description='Created', response=TeamViewSerializer), @@ -25,6 +32,18 @@ destroy = extend_schema( summary = 'Delete a team from this organization', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -33,6 +52,13 @@ list = extend_schema( summary = 'Fetch all teams from this organization', description='', + parameters = [ + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TeamViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -41,6 +67,18 @@ retrieve = extend_schema( summary = 'Fetch a single team from this organization', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TeamViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -50,6 +88,18 @@ partial_update = extend_schema( summary = 'Update a team within this organization', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TeamViewSerializer), # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), diff --git a/app/access/viewsets/team_user.py b/app/access/viewsets/team_user.py index e6c464d38..2d753a213 100644 --- a/app/access/viewsets/team_user.py +++ b/app/access/viewsets/team_user.py @@ -1,4 +1,4 @@ -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from access.serializers.team_user import ( TeamUsers, @@ -14,6 +14,18 @@ create=extend_schema( summary = 'Create a user within this team', description='', + parameters = [ + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'team_id', + location = 'path', + type = int + ), + ], responses = { # 200: OpenApiResponse(description='Allready exists', response=TeamUserViewSerializer), 201: OpenApiResponse(description='Created', response=TeamUserViewSerializer), @@ -24,6 +36,23 @@ destroy = extend_schema( summary = 'Delete a user from this team', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'team_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -32,6 +61,18 @@ list = extend_schema( summary = 'Fetch all users from this team', description='', + parameters = [ + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'team_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TeamUserViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -40,6 +81,23 @@ retrieve = extend_schema( summary = 'Fetch a single user from this team', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'team_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TeamUserViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -49,6 +107,23 @@ partial_update = extend_schema( summary = 'Update a user within this team', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'organization_id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'team_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TeamUserViewSerializer), # 201: OpenApiResponse(description='Created', response=OrganizationViewSerializer), diff --git a/app/api/auth.py b/app/api/auth.py index 5c8f20c95..4117a148f 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -5,6 +5,20 @@ from api.models.tokens import AuthToken +# scheme.py +from drf_spectacular.extensions import OpenApiAuthenticationExtension + +class TokenScheme(OpenApiAuthenticationExtension): + target_class = "api.auth.TokenAuthentication" + name = "TokenAuthentication" + + def get_security_definition(self, auto_schema): + return { + "type": "apiKey", + "in": "header", + "name": "Token Authorization", + "description": "Token-based authentication with required prefix 'Token'", + } class TokenAuthentication(BaseAuthentication): diff --git a/app/api/tasks.py b/app/api/tasks.py index db39b78b4..1cf07e517 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -9,7 +9,7 @@ from access.models import Organization -from api.serializers.inventory import Inventory +from itam.serializers.inventory import InventorySerializer from itam.models.device import Device, DeviceType, DeviceOperatingSystem, DeviceSoftware from itam.models.operating_system import OperatingSystem, OperatingSystemVersion @@ -32,8 +32,15 @@ def process_inventory(self, data, organization: int): logger.info('Begin Processing Inventory') - data = json.loads(data) - data = Inventory(data) + if type(data) is str: + + data = json.loads(data) + + data = InventorySerializer( + data = data + ) + + data.is_valid() organization = Organization.objects.get(id=organization) @@ -42,13 +49,13 @@ def process_inventory(self, data, organization: int): device_serial_number = None device_uuid = None - if data.details.serial_number and str(data.details.serial_number).lower() != 'na': + if data.validated_data['details']['serial_number'] and str(data.validated_data['details']['serial_number']).lower() != 'na': - device_serial_number = str(data.details.serial_number) + device_serial_number = str(data.validated_data['details']['serial_number']) - if data.details.uuid and str(data.details.uuid).lower() != 'na': + if data.validated_data['details']['uuid'] and str(data.validated_data['details']['uuid']).lower() != 'na': - device_uuid = str(data.details.uuid) + device_uuid = str(data.validated_data['details']['uuid']) if device_serial_number: # Search for device by serial number. @@ -88,13 +95,13 @@ def process_inventory(self, data, organization: int): if not device: # Search for device by Name. device = Device.objects.filter( - name__iexact=str(data.details.name).lower() + name__iexact=str(data.validated_data['details']['name']).lower() ) if device.exists(): device = Device.objects.get( - name__iexact=str(data.details.name).lower() + name__iexact=str(data.validated_data['details']['name']).lower() ) else: @@ -107,7 +114,7 @@ def process_inventory(self, data, organization: int): if not device: # Create the device device = Device.objects.create( - name = data.details.name, + name = data.validated_data['details']['name'], device_type = None, serial_number = device_serial_number, uuid = device_uuid, @@ -131,14 +138,14 @@ def process_inventory(self, data, organization: int): if not device.serial_number and device_serial_number: - device.serial_number = data.details.serial_number + device.serial_number = data.validated_data['details']['serial_number'] device_edited = True - if str(device.name).lower() != str(data.details.name).lower(): # Update device Name + if str(device.name).lower() != str(data.validated_data['details']['name']).lower(): # Update device Name - device.name = data.details.name + device.name = data.validated_data['details']['name'] device_edited = True @@ -149,14 +156,14 @@ def process_inventory(self, data, organization: int): operating_system = OperatingSystem.objects.filter( - name=data.operating_system.name, + name = data.validated_data['os']['name'], is_global = True ) if operating_system.exists(): operating_system = OperatingSystem.objects.get( - name=data.operating_system.name, + name = data.validated_data['os']['name'], is_global = True ) @@ -170,7 +177,7 @@ def process_inventory(self, data, organization: int): if not operating_system: operating_system = OperatingSystem.objects.filter( - name=data.operating_system.name, + name = data.validated_data['os']['name'], organization = organization ) @@ -178,7 +185,7 @@ def process_inventory(self, data, organization: int): if operating_system.exists(): operating_system = OperatingSystem.objects.get( - name=data.operating_system.name, + name = data.validated_data['os']['name'], organization = organization ) @@ -190,22 +197,22 @@ def process_inventory(self, data, organization: int): if not operating_system: operating_system = OperatingSystem.objects.create( - name = data.operating_system.name, + name = data.validated_data['os']['name'], organization = organization, is_global = True ) operating_system_version = OperatingSystemVersion.objects.filter( - name=data.operating_system.version_major, - operating_system=operating_system + name = data.validated_data['os']['version_major'], + operating_system = operating_system ) if operating_system_version.exists(): operating_system_version = OperatingSystemVersion.objects.get( - name=data.operating_system.version_major, - operating_system=operating_system + name = data.validated_data['os']['version_major'], + operating_system = operating_system ) else: @@ -218,7 +225,7 @@ def process_inventory(self, data, organization: int): operating_system_version = OperatingSystemVersion.objects.create( organization = organization, is_global = True, - name = data.operating_system.version_major, + name = data.validated_data['os']['version_major'], operating_system = operating_system, ) @@ -241,8 +248,8 @@ def process_inventory(self, data, organization: int): device_operating_system = DeviceOperatingSystem.objects.create( organization = organization, - device=device, - version = data.operating_system.version, + device = device, + version = data.validated_data['os']['version'], operating_system_version = operating_system_version, installdate = timezone.now() ) @@ -261,9 +268,9 @@ def process_inventory(self, data, organization: int): device_operating_system.save() - if device_operating_system.version != data.operating_system.version: + if device_operating_system.version != data.validated_data['os']['version']: - device_operating_system.version = data.operating_system.version + device_operating_system.version = data.validated_data['os']['version'] device_operating_system.save() @@ -287,7 +294,7 @@ def process_inventory(self, data, organization: int): inventoried_software: list = [] - for inventory in list(data.software): + for inventory in list(data.validated_data['software']): software = None software_category = None @@ -295,13 +302,13 @@ def process_inventory(self, data, organization: int): device_software = None - software_category = SoftwareCategory.objects.filter( name = inventory.category ) + software_category = SoftwareCategory.objects.filter( name = inventory['category'] ) if software_category.exists(): software_category = SoftwareCategory.objects.get( - name = inventory.category + name = inventory['category'] ) else: # Create Software Category @@ -309,16 +316,16 @@ def process_inventory(self, data, organization: int): software_category = SoftwareCategory.objects.create( organization = software_category_organization, is_global = True, - name = inventory.category, + name = inventory['category'], ) - if software_category.name == inventory.category: + if software_category.name == inventory['category']: - if Software.objects.filter( name = inventory.name ).exists(): + if Software.objects.filter( name = inventory['name'] ).exists(): software = Software.objects.get( - name = inventory.name + name = inventory['name'] ) if not software.category: @@ -331,16 +338,16 @@ def process_inventory(self, data, organization: int): software = Software.objects.create( organization = software_organization, is_global = True, - name = inventory.name, + name = inventory['name'], category = software_category, ) - if software.name == inventory.name: + if software.name == inventory['name']: pattern = r"^(\d+:)?(?P\d+\.\d+(\.\d+)?)" - semver = re.search(pattern, str(inventory.version), re.DOTALL) + semver = re.search(pattern, str(inventory['version']), re.DOTALL) if semver: @@ -348,7 +355,7 @@ def process_inventory(self, data, organization: int): semver = semver['semver'] else: - semver = inventory.version + semver = inventory['version'] if SoftwareVersion.objects.filter( name = semver, software = software ).exists(): diff --git a/app/api/urls_v2.py b/app/api/urls_v2.py index a5e8d78ce..a017ad05a 100644 --- a/app/api/urls_v2.py +++ b/app/api/urls_v2.py @@ -54,6 +54,7 @@ device_model as device_model_v2, device_type as device_type_v2, device_software as device_software_v2, + inventory, operating_system as operating_system_v2, operating_system_version as operating_system_version_v2, software as software_v2, @@ -134,6 +135,7 @@ 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/inventory', inventory.ViewSet, basename='_api_v2_inventory') 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') diff --git a/app/api/views/itam/inventory.py b/app/api/views/itam/inventory.py index aee5c22a4..6009fd8cc 100644 --- a/app/api/views/itam/inventory.py +++ b/app/api/views/itam/inventory.py @@ -33,6 +33,7 @@ def permission_check(self, request, view, obj=None) -> bool: +@extend_schema( deprecated = True ) class Collect(OrganizationPermissionAPI, views.APIView): queryset = Device.objects.all() diff --git a/app/api/viewsets/common.py b/app/api/viewsets/common.py index bebe7a030..b8613849f 100644 --- a/app/api/viewsets/common.py +++ b/app/api/viewsets/common.py @@ -5,6 +5,7 @@ from access.mixin import OrganizationMixin +from api.auth import TokenScheme from api.react_ui_metadata import ReactUIMetadata from api.views.mixin import OrganizationPermissionAPI @@ -225,6 +226,15 @@ class ModelViewSet( +class ModelCreateViewSet( + ModelViewSetBase, + viewsets.mixins.CreateModelMixin, +): + + pass + + + class ModelListRetrieveDeleteViewSet( viewsets.mixins.ListModelMixin, viewsets.mixins.RetrieveModelMixin, diff --git a/app/app/serializers/content_type.py b/app/app/serializers/content_type.py index ae846077e..67ad0fae8 100644 --- a/app/app/serializers/content_type.py +++ b/app/app/serializers/content_type.py @@ -10,7 +10,7 @@ class ContentTypeBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -43,7 +43,7 @@ class ContentTypeViewSerializer(ContentTypeBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_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 eb8d45e7c..299cab343 100644 --- a/app/app/serializers/permission.py +++ b/app/app/serializers/permission.py @@ -11,7 +11,7 @@ class PermissionBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -46,7 +46,7 @@ class PermissionViewSerializer(PermissionBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_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 b21b3387b..f4671cf12 100644 --- a/app/app/serializers/user.py +++ b/app/app/serializers/user.py @@ -9,7 +9,7 @@ class UserBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) diff --git a/app/app/settings.py b/app/app/settings.py index d7e1cf873..5175c33e2 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -33,7 +33,7 @@ CELERY_ACCEPT_CONTENT = ['json'] CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True # broker_connection_retry_on_startup -CELERY_BROKER_URL = 'amqp://guest:guest@172.16.10.102:30712/itsm' +CELERY_BROKER_URL = 'amqp://admin:admin@127.0.0.1:5672/itsm' # https://docs.celeryq.dev/en/stable/userguide/configuration.html#broker-use-ssl # import ssl diff --git a/app/assistance/serializers/knowledge_base.py b/app/assistance/serializers/knowledge_base.py index 4d52558eb..7da60b719 100644 --- a/app/assistance/serializers/knowledge_base.py +++ b/app/assistance/serializers/knowledge_base.py @@ -17,13 +17,13 @@ class KnowledgeBaseBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: return reverse( "v2:_api_v2_knowledge_base-detail", @@ -59,7 +59,7 @@ class KnowledgeBaseModelSerializer(KnowledgeBaseBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/assistance/serializers/knowledge_base_category.py b/app/assistance/serializers/knowledge_base_category.py index ab5b976df..9193c7216 100644 --- a/app/assistance/serializers/knowledge_base_category.py +++ b/app/assistance/serializers/knowledge_base_category.py @@ -18,13 +18,13 @@ class KnowledgeBaseCategoryBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: return reverse( "v2:_api_v2_knowledge_base_category-detail", @@ -60,7 +60,7 @@ class KnowledgeBaseCategoryModelSerializer(KnowledgeBaseCategoryBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/assistance/tests/unit/test_assistance_viewset.py b/app/assistance/tests/functional/test_assistance_viewset.py similarity index 100% rename from app/assistance/tests/unit/test_assistance_viewset.py rename to app/assistance/tests/functional/test_assistance_viewset.py diff --git a/app/assistance/tests/unit/ticket_request/test_ticket_request_serializer.py b/app/assistance/tests/functional/ticket_request/test_ticket_request_serializer.py similarity index 100% rename from app/assistance/tests/unit/ticket_request/test_ticket_request_serializer.py rename to app/assistance/tests/functional/ticket_request/test_ticket_request_serializer.py diff --git a/app/assistance/tests/unit/ticket_request/test_ticket_request_viewset.py b/app/assistance/tests/functional/ticket_request/test_ticket_request_viewset.py similarity index 100% rename from app/assistance/tests/unit/ticket_request/test_ticket_request_viewset.py rename to app/assistance/tests/functional/ticket_request/test_ticket_request_viewset.py diff --git a/app/config_management/serializers/config_group.py b/app/config_management/serializers/config_group.py index 529501d48..9e02cba5b 100644 --- a/app/config_management/serializers/config_group.py +++ b/app/config_management/serializers/config_group.py @@ -13,13 +13,13 @@ class ConfigGroupBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: request = None @@ -57,7 +57,7 @@ class ConfigGroupModelSerializer(ConfigGroupBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None diff --git a/app/config_management/serializers/config_group_software.py b/app/config_management/serializers/config_group_software.py index f2c6b7d5b..bb4937726 100644 --- a/app/config_management/serializers/config_group_software.py +++ b/app/config_management/serializers/config_group_software.py @@ -17,13 +17,13 @@ class ConfigGroupSoftwareBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: return reverse( "v2:_api_v2_config_group-detail", @@ -60,7 +60,7 @@ class ConfigGroupSoftwareModelSerializer(ConfigGroupSoftwareBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/core/fields/badge.py b/app/core/fields/badge.py index 6b8c5872c..c1ce6cff9 100644 --- a/app/core/fields/badge.py +++ b/app/core/fields/badge.py @@ -7,7 +7,7 @@ -class BadgeField(serializers.Field): +class BadgeField(serializers.DictField): source = '' diff --git a/app/core/fields/icon.py b/app/core/fields/icon.py index 1e561714e..1544b07bb 100644 --- a/app/core/fields/icon.py +++ b/app/core/fields/icon.py @@ -5,7 +5,7 @@ -class IconField(serializers.Field): +class IconField(serializers.DictField): source = '' diff --git a/app/core/serializers/celery_log.py b/app/core/serializers/celery_log.py index e82d0e30a..492937b46 100644 --- a/app/core/serializers/celery_log.py +++ b/app/core/serializers/celery_log.py @@ -15,7 +15,7 @@ class TaskResultBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -46,7 +46,7 @@ class TaskResultModelSerializer(TaskResultBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_celery_log-detail", diff --git a/app/core/serializers/history.py b/app/core/serializers/history.py index deaca3d18..c169c7e97 100644 --- a/app/core/serializers/history.py +++ b/app/core/serializers/history.py @@ -11,13 +11,13 @@ class HistoryBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_my_url') - def get_my_url(self, item): + def get_my_url(self, item) -> str: return reverse("v2:_api_v2_model_history-detail", request=self._context['view'].request, @@ -55,7 +55,7 @@ class HistoryModelSerializer(HistoryBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_model_history-detail", diff --git a/app/core/serializers/manufacturer.py b/app/core/serializers/manufacturer.py index 29dab0ffc..5cfdc1eb9 100644 --- a/app/core/serializers/manufacturer.py +++ b/app/core/serializers/manufacturer.py @@ -13,7 +13,7 @@ class ManufacturerBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class ManufacturerModelSerializer(ManufacturerBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_manufacturer-detail", diff --git a/app/core/serializers/notes.py b/app/core/serializers/notes.py index dcfa3426e..8624a3ec4 100644 --- a/app/core/serializers/notes.py +++ b/app/core/serializers/notes.py @@ -21,7 +21,7 @@ class NoteBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -54,7 +54,7 @@ class NoteModelSerializer(NoteBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: if 'group_id' in self._kwargs['context']['view'].kwargs: diff --git a/app/core/serializers/ticket.py b/app/core/serializers/ticket.py index 18b5321f2..5174c27cf 100644 --- a/app/core/serializers/ticket.py +++ b/app/core/serializers/ticket.py @@ -23,14 +23,14 @@ class TicketBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('my_url') - def my_url(self, item): + def my_url(self, item) -> str: context = self.context.copy() @@ -82,7 +82,7 @@ class TicketModelSerializer(TicketBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: context = self.context.copy() diff --git a/app/core/serializers/ticket_category.py b/app/core/serializers/ticket_category.py index ccc93b069..ea8b7d940 100644 --- a/app/core/serializers/ticket_category.py +++ b/app/core/serializers/ticket_category.py @@ -11,7 +11,7 @@ class TicketCategoryBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class TicketCategoryModelSerializer(TicketCategoryBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("API:_api_v2_ticket_category-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/core/serializers/ticket_comment.py b/app/core/serializers/ticket_comment.py index 11a9d6d98..4c31902e5 100644 --- a/app/core/serializers/ticket_comment.py +++ b/app/core/serializers/ticket_comment.py @@ -19,7 +19,7 @@ class TicketCommentBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -61,7 +61,7 @@ class TicketCommentModelSerializer( _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = self.context.get('request') diff --git a/app/core/serializers/ticket_comment_category.py b/app/core/serializers/ticket_comment_category.py index 33a950e51..b1924c15c 100644 --- a/app/core/serializers/ticket_comment_category.py +++ b/app/core/serializers/ticket_comment_category.py @@ -13,7 +13,7 @@ class TicketCommentCategoryBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class TicketCommentCategoryModelSerializer(TicketCommentCategoryBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_ticket_comment_category-detail", diff --git a/app/core/serializers/ticket_linked_item.py b/app/core/serializers/ticket_linked_item.py index 33aa8696d..9684949f8 100644 --- a/app/core/serializers/ticket_linked_item.py +++ b/app/core/serializers/ticket_linked_item.py @@ -16,14 +16,14 @@ class TicketLinkedItemBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('my_url') - def my_url(self, item): + def my_url(self, item) -> str: return item.get_url( request = self._context['view'].request ) @@ -54,7 +54,7 @@ class TicketLinkedItemModelSerializer(TicketLinkedItemBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': item.get_url( request = self._context['view'].request ) diff --git a/app/core/serializers/ticket_related.py b/app/core/serializers/ticket_related.py index 76c0a192a..846b7d80c 100644 --- a/app/core/serializers/ticket_related.py +++ b/app/core/serializers/ticket_related.py @@ -15,7 +15,7 @@ class RelatedTicketBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -64,7 +64,7 @@ class RelatedTicketModelSerializer(RelatedTicketBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None diff --git a/app/core/tests/abstract/test_ticket_serializer.py b/app/core/tests/abstract/test_ticket_serializer.py index 055769b53..f90c2c7b3 100644 --- a/app/core/tests/abstract/test_ticket_serializer.py +++ b/app/core/tests/abstract/test_ticket_serializer.py @@ -462,7 +462,20 @@ def setUpTestData(self): + def test_assigned_ticket_status_updates(self): + ticket = Ticket.objects.create( + organization=self.organization, + title = 'ticket title test status', + description = 'some text', + opened_by = self.add_user, + status = Ticket.TicketStatus.All.NEW, + ticket_type = self.ticket_type_enum, + ) + + ticket.assigned_users.add(self.triage_user.id) + + assert ticket.status == Ticket.TicketStatus.All.ASSIGNED def test_serializer_validation_add_valid_ok(self): diff --git a/app/core/viewsets/related_ticket.py b/app/core/viewsets/related_ticket.py index 9e597c118..f4149b28b 100644 --- a/app/core/viewsets/related_ticket.py +++ b/app/core/viewsets/related_ticket.py @@ -1,6 +1,6 @@ from django.db.models import Q -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from access.mixin import OrganizationMixin @@ -18,6 +18,13 @@ destroy = extend_schema( summary = 'Delete a related ticket', description = '', + parameters = [ + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -26,6 +33,13 @@ list = extend_schema( summary = 'Fetch all related tickets', description='', + parameters = [ + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=RelatedTicketViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -34,6 +48,18 @@ retrieve = extend_schema( summary = 'Fetch a related ticket', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=RelatedTicketViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), diff --git a/app/core/viewsets/ticket_comment.py b/app/core/viewsets/ticket_comment.py index 8fbc93205..f640aac9f 100644 --- a/app/core/viewsets/ticket_comment.py +++ b/app/core/viewsets/ticket_comment.py @@ -1,7 +1,7 @@ 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 drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer from rest_framework import generics, viewsets from rest_framework.response import Response @@ -46,6 +46,13 @@ Responses from the API are the same for all users when the request returns status `HTTP/20x`. """, + parameters = [ + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], request = PolymorphicProxySerializer( component_name = 'TicketComment', serializers=[ @@ -74,6 +81,18 @@ destroy = extend_schema( summary = 'Delete a ticket comment', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -82,6 +101,13 @@ list = extend_schema( summary = 'Fetch all ticket comments', description='', + parameters = [ + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TicketCommentViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -90,6 +116,18 @@ retrieve = extend_schema( summary = 'Fetch a single ticket comment', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TicketCommentViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -99,6 +137,18 @@ partial_update = extend_schema( summary = 'Update a ticket comment', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TicketCommentViewSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/core/viewsets/ticket_linked_item.py b/app/core/viewsets/ticket_linked_item.py index 8d6dc1077..686d7c249 100644 --- a/app/core/viewsets/ticket_linked_item.py +++ b/app/core/viewsets/ticket_linked_item.py @@ -1,4 +1,4 @@ -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from core.serializers.ticket_linked_item import ( TicketLinkedItem, @@ -15,6 +15,13 @@ create=extend_schema( summary = 'Create a Ticket Linked Item', description='', + parameters = [ + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 201: OpenApiResponse(description='Created', response=TicketLinkedItemViewSerializer), 403: OpenApiResponse(description='User is missing add permissions'), @@ -23,6 +30,18 @@ destroy = extend_schema( summary = 'Delete a Ticket Linked Item', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -31,6 +50,13 @@ list = extend_schema( summary = 'Fetch all Ticket Linked Items', description='', + parameters = [ + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TicketLinkedItemViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -39,6 +65,18 @@ retrieve = extend_schema( summary = 'Fetch a single Ticket Linked Item', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TicketLinkedItemViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -48,6 +86,18 @@ partial_update = extend_schema( summary = 'Update a Ticket Linked Item', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'ticket_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=TicketLinkedItemViewSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/itam/serializers/device.py b/app/itam/serializers/device.py index 445e9e685..3bfa08211 100644 --- a/app/itam/serializers/device.py +++ b/app/itam/serializers/device.py @@ -22,7 +22,7 @@ class DeviceBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -52,7 +52,7 @@ class DeviceModelSerializer(DeviceBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None @@ -108,7 +108,7 @@ def get_cont(self, item) -> dict: rendered_config = serializers.JSONField(source='get_configuration', read_only=True) - def get_rendered_config(self, item): + def get_rendered_config(self, item) -> dict: return item.get_configuration(0) diff --git a/app/itam/serializers/device_model.py b/app/itam/serializers/device_model.py index 6b8e4e8fb..a4cb649e6 100644 --- a/app/itam/serializers/device_model.py +++ b/app/itam/serializers/device_model.py @@ -14,7 +14,7 @@ class DeviceModelBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -47,7 +47,7 @@ class DeviceModelModelSerializer(DeviceModelBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, obj): + def get_url(self, obj) -> dict: return { '_self': reverse("v2:_api_v2_device_model-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) diff --git a/app/itam/serializers/device_software.py b/app/itam/serializers/device_software.py index 73cb536a4..ebcaeeb4d 100644 --- a/app/itam/serializers/device_software.py +++ b/app/itam/serializers/device_software.py @@ -20,7 +20,7 @@ class DeviceSoftwareBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -54,7 +54,7 @@ class DeviceSoftwareModelSerializer(DeviceSoftwareBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, obj): + def get_url(self, obj) -> dict: return { '_self': reverse( diff --git a/app/itam/serializers/device_type.py b/app/itam/serializers/device_type.py index 2b5a755ce..abb02bdb5 100644 --- a/app/itam/serializers/device_type.py +++ b/app/itam/serializers/device_type.py @@ -11,7 +11,7 @@ class DeviceTypeBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -43,7 +43,7 @@ class DeviceTypeModelSerializer(DeviceTypeBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, obj): + def get_url(self, obj) -> dict: return { '_self': reverse("v2:_api_v2_device_type-detail", request=self._context['view'].request, kwargs={'pk': obj.pk}) diff --git a/app/itam/serializers/inventory.py b/app/itam/serializers/inventory.py new file mode 100644 index 000000000..f13d94d65 --- /dev/null +++ b/app/itam/serializers/inventory.py @@ -0,0 +1,93 @@ +from django.urls import reverse + +from rest_framework import serializers + +from core import exceptions as centurion_exceptions + +from itam.models.device import Device + + + + +class InventorySerializer(serializers.Serializer): + """ Serializer for Inventory Upload """ + + + class DetailsSerializer(serializers.Serializer): + + name = serializers.CharField( + help_text = 'Host name', + required = True + ) + + serial_number = serializers.CharField( + default = None, + help_text = 'Devices serial number', + required = False + ) + + uuid = serializers.CharField( + default = None, + help_text = 'Device system UUID', + required = False + ) + + + def validate(self, data): + + if( + data['serial_number'] is None + and data['uuid'] is None + ): + + raise centurion_exceptions.ValidationError( + detail = 'Serial Number or UUID is required', + code = 'no_serial_or_uuid' + ) + + return data + + + class OperatingSystemSerializer(serializers.Serializer): + + name = serializers.CharField( + help_text='Name of the operating system installed on the device', + required = True, + ) + + version_major = serializers.IntegerField( + help_text='Major semver version number of the OS version', + required = True, + ) + + version = serializers.CharField( + help_text='semver version number of the OS', + required = True + ) + + + class SoftwareSerializer(serializers.Serializer): + + name = serializers.CharField( + help_text='Name of the software', + required = True + ) + + category = serializers.CharField( + help_text='Category of the software', + default = None, + required = False + ) + + version = serializers.CharField( + default = None, + help_text='semver version number of the software', + required = False + ) + + + details = DetailsSerializer() + + os = OperatingSystemSerializer( required = False ) + + software = SoftwareSerializer( many = True, required = False ) diff --git a/app/itam/serializers/operating_system.py b/app/itam/serializers/operating_system.py index 3f540ccd5..1b3b3fe89 100644 --- a/app/itam/serializers/operating_system.py +++ b/app/itam/serializers/operating_system.py @@ -14,7 +14,7 @@ class OperatingSystemBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -48,7 +48,7 @@ class OperatingSystemModelSerializer(OperatingSystemBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None diff --git a/app/itam/serializers/operating_system_version.py b/app/itam/serializers/operating_system_version.py index ce9e4cbd3..62adb469e 100644 --- a/app/itam/serializers/operating_system_version.py +++ b/app/itam/serializers/operating_system_version.py @@ -15,14 +15,14 @@ class OperatingSystemVersionBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('my_url') - def my_url(self, item): + def my_url(self, item) -> str: return reverse( "v2:_api_v2_operating_system_version-detail", @@ -60,7 +60,7 @@ class OperatingSystemVersionModelSerializer(OperatingSystemVersionBaseSerializer _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/itam/serializers/software.py b/app/itam/serializers/software.py index 9caf0c94b..f409d9c32 100644 --- a/app/itam/serializers/software.py +++ b/app/itam/serializers/software.py @@ -14,7 +14,7 @@ class SoftwareBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -46,7 +46,7 @@ class SoftwareModelSerializer(SoftwareBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None @@ -88,7 +88,7 @@ def get_url(self, item): } - def get_rendered_config(self, item): + def get_rendered_config(self, item) -> dict: return item.get_configuration(0) diff --git a/app/itam/serializers/software_category.py b/app/itam/serializers/software_category.py index 747e87b00..b33c52654 100644 --- a/app/itam/serializers/software_category.py +++ b/app/itam/serializers/software_category.py @@ -13,7 +13,7 @@ class SoftwareCategoryBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -45,7 +45,7 @@ class SoftwareCategoryModelSerializer(SoftwareCategoryBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_software_category-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), @@ -54,7 +54,7 @@ def get_url(self, item): } - def get_rendered_config(self, item): + def get_rendered_config(self, item) -> dict: return item.get_configuration(0) diff --git a/app/itam/serializers/software_version.py b/app/itam/serializers/software_version.py index f5514acd9..ca48b57ad 100644 --- a/app/itam/serializers/software_version.py +++ b/app/itam/serializers/software_version.py @@ -12,14 +12,14 @@ class SoftwareVersionBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('my_url') - def my_url(self, item): + def my_url(self, item) -> str: return reverse( "v2:_api_v2_software_version-detail", @@ -54,7 +54,7 @@ class SoftwareVersionModelSerializer(SoftwareVersionBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/itam/tests/functional/inventory/test_inventory_serializer.py b/app/itam/tests/functional/inventory/test_inventory_serializer.py new file mode 100644 index 000000000..0572d32b5 --- /dev/null +++ b/app/itam/tests/functional/inventory/test_inventory_serializer.py @@ -0,0 +1,206 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from itam.serializers.inventory import ( + InventorySerializer +) + + + +class InventoryValidationAPI( + TestCase, +): + + + @classmethod + def setUpTestData(self): + """Setup Test + + 1. Create an org + 2. Create an item + """ + + self.valid_data: dict = { + "details": { + "name": "string", + "serial_number": "string", + "uuid": "string" + }, + "os": { + "name": "string", + "version_major": 0, + "version": "string" + }, + "software": [ + { + "name": "string", + "category": "string", + "version": "string" + } + ] + } + + + def test_serializer_valid_data(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data it creates + with no errors + """ + + serializer = InventorySerializer( + data = self.valid_data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_valid_data_no_os(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that is missing the + os field, the item is still created + """ + + data = self.valid_data.copy() + + del data['os'] + + serializer = InventorySerializer( + data = data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_valid_data_empty_software(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that has an empty + software field, the item is still created + """ + + data = self.valid_data.copy() + + data['software'] = [] + + serializer = InventorySerializer( + data = data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_valid_data_no_software(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that is missing the + software field, the item is still created + """ + + data = self.valid_data.copy() + + del data['software'] + + serializer = InventorySerializer( + data = data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_valid_data_details_only(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that is missing the + os and software field, the item is still created + """ + + data = self.valid_data.copy() + + del data['os'] + del data['software'] + + serializer = InventorySerializer( + data = data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_valid_data_details_no_serial(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that is missing the + details.os and software field, the item is still created + """ + + data = self.valid_data.copy() + + del data['details']['serial_number'] + + serializer = InventorySerializer( + data = data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + def test_serializer_valid_data_details_no_uuid(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that is missing the + details.uuid and software field, the item is still created + """ + + data = self.valid_data.copy() + + del data['details']['uuid'] + + serializer = InventorySerializer( + data = data + ) + + + assert serializer.is_valid(raise_exception = True) + + + + + def test_serializer_validation_no_serial_and_uuid(self): + """Serializer Validation Check + + Ensure that if creating and no name is provided a validation error occurs + """ + + data = self.valid_data.copy() + + del data['details']['uuid'] + del data['details']['serial_number'] + + with pytest.raises(ValidationError) as err: + + serializer = InventorySerializer( + data = data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['details']['non_field_errors'][0] == 'no_serial_or_uuid' diff --git a/app/itam/viewsets/device_software.py b/app/itam/viewsets/device_software.py index ae74f0aff..9a99aabe3 100644 --- a/app/itam/viewsets/device_software.py +++ b/app/itam/viewsets/device_software.py @@ -4,7 +4,7 @@ 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 drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from rest_framework.fields import empty from rest_framework import generics, viewsets @@ -29,6 +29,13 @@ create=extend_schema( summary = 'Add device software', description='', + parameters = [ + OpenApiParameter( + name = 'device_id', + location = 'path', + type = int + ), + ], responses = { 201: OpenApiResponse(description='Device created', response=DeviceSoftwareModelSerializer), 400: OpenApiResponse(description='Validation failed.'), @@ -38,6 +45,18 @@ destroy = extend_schema( summary = 'Delete a device software', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'device_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -46,6 +65,13 @@ list = extend_schema( summary = 'Fetch all device software', description='', + parameters = [ + OpenApiParameter( + name = 'device_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=DeviceSoftwareModelSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -54,6 +80,18 @@ retrieve = extend_schema( summary = 'Fetch a single device software', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'device_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=DeviceSoftwareModelSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -63,6 +101,18 @@ partial_update = extend_schema( summary = 'Update a device software', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'device_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=DeviceSoftwareModelSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/itam/viewsets/index.py b/app/itam/viewsets/index.py index 96833e072..f0e9c0f3b 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('v2:_api_v2_device-list', request=request), + "inventory": reverse('v2:_api_v2_inventory-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/itam/viewsets/inventory.py b/app/itam/viewsets/inventory.py new file mode 100644 index 000000000..fd6f7f251 --- /dev/null +++ b/app/itam/viewsets/inventory.py @@ -0,0 +1,196 @@ +import json + +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from rest_framework.response import Response + + +from api.tasks import process_inventory +from api.viewsets.common import ModelCreateViewSet + +from core import exceptions as centurion_exception +from core.http.common import Http + +from itam.models.device import Device +from itam.serializers.inventory import InventorySerializer + +from settings.models.user_settings import UserSettings + + + +@extend_schema_view( + create=extend_schema( + summary = "Upload a device's inventory", + description = """After inventorying a device, it's inventory file, `.json` is uploaded to this endpoint. +If the device does not exist, it will be created. If the device does exist the existing +device will be updated with the information within the inventory. + +matching for an existing device is by slug which is the hostname converted to lower case +letters. This conversion is automagic. + +**NOTE:** _for device creation, the API user must have user setting 'Default Organization'. Without +this setting populated, no device will be created and the endpoint will return HTTP/403_ + +## Permissions + +- `itam.add_device` Required to upload inventory + """, + request = InventorySerializer, + responses = { + 200: OpenApiResponse( + description='Inventory upload successful', + response = { + 'OK' + } + ), + 400: OpenApiResponse(description='Error Occured, see output retured'), + 401: OpenApiResponse(description='User Not logged in'), + 403: OpenApiResponse(description='User is missing permission or in different organization'), + 500: OpenApiResponse(description='Exception occured. View server logs for the Stack Trace'), + } + ) +) +class ViewSet( ModelCreateViewSet ): + """Device Inventory + + Use this endpoint to upload your device inventories. + """ + + model = Device + + serializer_class = InventorySerializer + + documentation: str = 'https://nofusscomputing.com/docs/not_model_docs' + + view_name = 'Device Inventory' + + view_description = __doc__ + + inventory_action: str = None + """Inventory action, choice. new|update""" + + + def create(self, request, *args, **kwargs): + """Upload a device inventory + + Raises: + centurion_exceptions.PermissionDenied: User is missing the required permissions + + Returns: + Response: string denoting what has occured + """ + + status = Http.Status.OK + response_data = 'OK' + + try: + + data = InventorySerializer( + data = request.data + ) + + device = None + + if not data.is_valid(): + + raise centurion_exception.ValidationError( + detail = 'Uploaded inventory is not valid', + code = 'invalid_inventory' + ) + + + self.default_organization = UserSettings.objects.get(user=request.user).default_organization + + if Device.objects.filter(slug=str(data.validated_data['details']['name']).lower()).exists(): + + self.obj = Device.objects.get(slug=str(data.validated_data['details']['name']).lower()) + + device = self.obj + + task = process_inventory.delay(data.validated_data, self.default_organization.id) + + response_data: dict = {"task_id": f"{task.id}"} + + + except centurion_exception.PermissionDenied as e: + + status = Http.Status.FORBIDDEN + response_data = e.detail + + except centurion_exception.ValidationError as e: + + status = Http.Status.BAD_REQUEST + response_data = e.detail + + except Exception as e: + + print(f'An error occured{e}') + + status = Http.Status.SERVER_ERROR + response_data = f'Unknown Server Error occured: {e}' + + + return Response(data=response_data,status=status) + + + + def get_dynamic_permissions(self): + """Obtain the permissions required to upload an inventory. + + Returns: + list: Permissions required for Inventory Upload + """ + + organization = None + + device_search = None + + if 'details' in self.request.data: + + if 'name' in self.request.data['details']: + + device_search = Device.objects.filter( + slug = str(self.request.data['details']['name']).lower() + ) + + else: + + centurion_exception.ParseError( + detail = { + 'name': 'Device name is required' + }, + code = 'missing_device_name' + ) + + else: + + centurion_exception.ParseError( + detail = { + 'details': 'Details dict is required' + }, + code = 'missing_details_dict' + ) + + + if device_search: # Existing device + + if len(list(device_search)) == 1: + + self.obj = list(device_search)[0] + + self.permission_required = [ + 'itam.change_device' + ] + + self.inventory_action = 'update' + + else: # New device + + self.permission_required = [ + 'itam.add_device' + ] + + self.inventory_action = 'new' + + + return super().get_permission_required() diff --git a/app/itam/viewsets/operating_system_version.py b/app/itam/viewsets/operating_system_version.py index 7202a00e6..fc8d3bb05 100644 --- a/app/itam/viewsets/operating_system_version.py +++ b/app/itam/viewsets/operating_system_version.py @@ -1,4 +1,4 @@ -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from itam.serializers.operating_system_version import ( OperatingSystemVersion, @@ -13,6 +13,13 @@ create=extend_schema( summary = 'Create an operating system version', description='', + parameters = [ + OpenApiParameter( + name = 'operating_system_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='Software allready exists', response=OperatingSystemVersionViewSerializer), 201: OpenApiResponse(description='Software created', response=OperatingSystemVersionViewSerializer), @@ -23,6 +30,18 @@ destroy = extend_schema( summary = 'Delete an operating system version', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'operating_system_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -31,6 +50,13 @@ list = extend_schema( summary = 'Fetch all operating system versions', description='', + parameters = [ + OpenApiParameter( + name = 'operating_system_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=OperatingSystemVersionViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -39,6 +65,18 @@ retrieve = extend_schema( summary = 'Fetch a single operating system version', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'operating_system_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=OperatingSystemVersionViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -48,6 +86,18 @@ partial_update = extend_schema( summary = 'Update an operating system version', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'operating_system_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=OperatingSystemVersionViewSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/itam/viewsets/software_version.py b/app/itam/viewsets/software_version.py index baa92fc27..aec358b5d 100644 --- a/app/itam/viewsets/software_version.py +++ b/app/itam/viewsets/software_version.py @@ -1,4 +1,4 @@ -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from itam.serializers.software_version import ( SoftwareVersion, @@ -13,6 +13,13 @@ create=extend_schema( summary = 'Create a software version', description='', + parameters = [ + OpenApiParameter( + name = 'software_id', + location = 'path', + type = int + ), + ], responses = { 201: OpenApiResponse(description='Software created', response=SoftwareVersionViewSerializer), 400: OpenApiResponse(description='Validation failed.'), @@ -22,6 +29,18 @@ destroy = extend_schema( summary = 'Delete a software version', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'software_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -30,6 +49,13 @@ list = extend_schema( summary = 'Fetch all software versions', description='', + parameters = [ + OpenApiParameter( + name = 'software_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=SoftwareVersionViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -38,6 +64,18 @@ retrieve = extend_schema( summary = 'Fetch a single software version', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'software_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=SoftwareVersionViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -47,6 +85,18 @@ partial_update = extend_schema( summary = 'Update a software version', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'software_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=SoftwareVersionViewSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/itim/serializers/cluster.py b/app/itim/serializers/cluster.py index 7ff2f8a35..af63c5078 100644 --- a/app/itim/serializers/cluster.py +++ b/app/itim/serializers/cluster.py @@ -14,7 +14,7 @@ class ClusterBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -45,7 +45,7 @@ class ClusterModelSerializer(ClusterBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None diff --git a/app/itim/serializers/cluster_type.py b/app/itim/serializers/cluster_type.py index 524a3fac2..f834d1f02 100644 --- a/app/itim/serializers/cluster_type.py +++ b/app/itim/serializers/cluster_type.py @@ -13,7 +13,7 @@ class ClusterTypeBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class ClusterTypeModelSerializer(ClusterTypeBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_cluster_type-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/itim/serializers/port.py b/app/itim/serializers/port.py index 28eb9bd22..081c758d4 100644 --- a/app/itim/serializers/port.py +++ b/app/itim/serializers/port.py @@ -13,7 +13,7 @@ class PortBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -47,7 +47,7 @@ class PortModelSerializer(PortBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_port-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/itim/serializers/service.py b/app/itim/serializers/service.py index ded8e8f4c..7e5bff7ce 100644 --- a/app/itim/serializers/service.py +++ b/app/itim/serializers/service.py @@ -16,7 +16,7 @@ class ServiceBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -47,7 +47,7 @@ class ServiceModelSerializer(ServiceBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: request = None diff --git a/app/project_management/serializers/project.py b/app/project_management/serializers/project.py index 46362880c..16cf885a1 100644 --- a/app/project_management/serializers/project.py +++ b/app/project_management/serializers/project.py @@ -20,7 +20,7 @@ class ProjectBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -52,7 +52,7 @@ class ProjectModelSerializer(ProjectBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_project-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/project_management/serializers/project_milestone.py b/app/project_management/serializers/project_milestone.py index 369aabd36..560405e38 100644 --- a/app/project_management/serializers/project_milestone.py +++ b/app/project_management/serializers/project_milestone.py @@ -13,13 +13,13 @@ class ProjectMilestoneBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) url = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> str: context = self.context.copy() @@ -56,7 +56,7 @@ class ProjectMilestoneModelSerializer(ProjectMilestoneBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/project_management/serializers/project_states.py b/app/project_management/serializers/project_states.py index 52a657aae..6e7ad2e3e 100644 --- a/app/project_management/serializers/project_states.py +++ b/app/project_management/serializers/project_states.py @@ -14,7 +14,7 @@ class ProjectStateBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -47,7 +47,7 @@ class ProjectStateModelSerializer(ProjectStateBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/project_management/serializers/project_type.py b/app/project_management/serializers/project_type.py index a8e79b04e..74ea6dbdc 100644 --- a/app/project_management/serializers/project_type.py +++ b/app/project_management/serializers/project_type.py @@ -14,7 +14,7 @@ class ProjectTypeBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -46,7 +46,7 @@ class ProjectTypeModelSerializer(ProjectTypeBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse( diff --git a/app/project_management/viewsets/project_milestone.py b/app/project_management/viewsets/project_milestone.py index 6b078ef62..ad05845bf 100644 --- a/app/project_management/viewsets/project_milestone.py +++ b/app/project_management/viewsets/project_milestone.py @@ -1,4 +1,4 @@ -from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse from api.viewsets.common import ModelViewSet @@ -14,6 +14,13 @@ create=extend_schema( summary = 'Create a cluster', description='', + parameters = [ + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 201: OpenApiResponse(description='Device created', response=ProjectMilestoneViewSerializer), 400: OpenApiResponse(description='Validation failed.'), @@ -23,6 +30,18 @@ destroy = extend_schema( summary = 'Delete a cluster', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -31,6 +50,13 @@ list = extend_schema( summary = 'Fetch all clusters', description='', + parameters = [ + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=ProjectMilestoneViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -39,6 +65,18 @@ retrieve = extend_schema( summary = 'Fetch a single cluster', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=ProjectMilestoneViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -48,6 +86,18 @@ partial_update = extend_schema( summary = 'Update a cluster', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=ProjectMilestoneViewSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/project_management/viewsets/project_task.py b/app/project_management/viewsets/project_task.py index 9e94f9396..016f08d66 100644 --- a/app/project_management/viewsets/project_task.py +++ b/app/project_management/viewsets/project_task.py @@ -1,6 +1,7 @@ from drf_spectacular.utils import ( extend_schema, extend_schema_view, + OpenApiParameter, OpenApiResponse, PolymorphicProxySerializer, ) @@ -21,6 +22,13 @@ create=extend_schema( summary = 'Create a Project Task', description='', + parameters = [ + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], request = PolymorphicProxySerializer( component_name = 'ProjectTask', serializers=[ @@ -40,6 +48,18 @@ destroy = extend_schema( summary = 'Delete a Project Task', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 204: OpenApiResponse(description=''), 403: OpenApiResponse(description='User is missing delete permissions'), @@ -48,6 +68,13 @@ list = extend_schema( summary = 'Fetch all Project Task', description='', + parameters = [ + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=ProjectTaskTicketViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -56,6 +83,18 @@ retrieve = extend_schema( summary = 'Fetch a Project Task', description='', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=ProjectTaskTicketViewSerializer), 403: OpenApiResponse(description='User is missing view permissions'), @@ -65,6 +104,18 @@ partial_update = extend_schema( summary = 'Update a Project Task', description = '', + parameters = [ + OpenApiParameter( + name = 'id', + location = 'path', + type = int + ), + OpenApiParameter( + name = 'project_id', + location = 'path', + type = int + ), + ], responses = { 200: OpenApiResponse(description='', response=ProjectTaskTicketViewSerializer), 403: OpenApiResponse(description='User is missing change permissions'), diff --git a/app/settings/serializers/app_settings.py b/app/settings/serializers/app_settings.py index b55500e88..c43b8927c 100644 --- a/app/settings/serializers/app_settings.py +++ b/app/settings/serializers/app_settings.py @@ -12,7 +12,7 @@ class AppSettingsBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class AppSettingsModelSerializer(AppSettingsBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_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 2039a8db1..c63d786ab 100644 --- a/app/settings/serializers/external_links.py +++ b/app/settings/serializers/external_links.py @@ -12,7 +12,7 @@ class ExternalLinkBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class ExternalLinkModelSerializer(ExternalLinkBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_external_link-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/settings/serializers/user_settings.py b/app/settings/serializers/user_settings.py index 5536ee3c6..fb4ded7d5 100644 --- a/app/settings/serializers/user_settings.py +++ b/app/settings/serializers/user_settings.py @@ -12,7 +12,7 @@ class UserSettingsBaseSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField('get_display_name') - def get_display_name(self, item): + def get_display_name(self, item) -> str: return str( item ) @@ -44,7 +44,7 @@ class UserSettingsModelSerializer(UserSettingsBaseSerializer): _urls = serializers.SerializerMethodField('get_url') - def get_url(self, item): + def get_url(self, item) -> dict: return { '_self': reverse("v2:_api_v2_user_settings-detail", request=self._context['view'].request, kwargs={'pk': item.pk}), diff --git a/app/settings/tests/functional/external_links/test_external_link_viewset.py b/app/settings/tests/functional/external_links/test_external_link_viewset.py new file mode 100644 index 000000000..e5502f274 --- /dev/null +++ b/app/settings/tests/functional/external_links/test_external_link_viewset.py @@ -0,0 +1,182 @@ +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 settings.models.external_link import ExternalLink + + + +class ExternalLinkPermissionsAPI(TestCase, APIPermissions): + + model = ExternalLink + + app_namespace = 'v2' + + url_name = '_api_v2_external_link' + + 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, + 'template': 'http://site.tld/{{ template }}' + } + + + 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/settings/tests/functional/external_links/test_external_links_serializer.py b/app/settings/tests/functional/external_links/test_external_links_serializer.py new file mode 100644 index 000000000..fb588b508 --- /dev/null +++ b/app/settings/tests/functional/external_links/test_external_links_serializer.py @@ -0,0 +1,98 @@ +import pytest + +from django.test import TestCase + +from rest_framework.exceptions import ValidationError + +from access.models import Organization + +from settings.serializers.external_links import ( + ExternalLink, + ExternalLinkModelSerializer +) + + + +class ExternalLinkValidationAPI( + TestCase, +): + + model = ExternalLink + + @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.valid_data: dict = { + 'organization': self.organization.id, + 'name': 'a name', + 'template': 'http://example.com/{{ val }}' + } + + + + def test_serializer_valid_data(self): + """Serializer Validation Check + + Ensure that if creating an item with valid data that + no errors occur + """ + + serializer = ExternalLinkModelSerializer( + data = self.valid_data + ) + + 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 + """ + + data = self.valid_data.copy() + + del data['name'] + + with pytest.raises(ValidationError) as err: + + serializer = ExternalLinkModelSerializer( + data = data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['name'][0] == 'required' + + + + + def test_serializer_validation_no_template(self): + """Serializer Validation Check + + Ensure that if creating and no template is provided a validation error occurs + """ + + data = self.valid_data.copy() + + del data['template'] + + with pytest.raises(ValidationError) as err: + + serializer = ExternalLinkModelSerializer( + data = data + ) + + serializer.is_valid(raise_exception = True) + + assert err.value.get_codes()['template'][0] == 'required' diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml index 6aa7af8ce..801514125 100644 --- a/deploy/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -2,26 +2,66 @@ # docker exec -ti centurion-erp python manage.py createsuperuser version: "3.2" +x-app: ¢urion + image: centurion-erp:dev + restart: always + network_mode: "host" + # networks: + # - default + volumes: + - ./volumes/data:/data:rw + - ./volumes/etc/itsm:/etc/itsm:ro + services: - centurion-erp: - image: centurion-erp:dev + + + + + + centurion: + <<: *centurion + container_name: centurion-erp build: context: ../. dockerfile: dockerfile - container_name: centurion-erp hostname: centurion-erp - ports: - - "8002:8000" - volumes: - - ./volumes/data:/data:rw - - ./volumes/etc/itsm:/etc/itsm:ro - restart: always - networks: - - default - - mariadb - - -networks: - mariadb: - external: true + # ports: + # - "8002:8000" + + + + worker: + <<: *centurion + container_name: centurion-worker + environment: + - IS_WORKER=true + hostname: centurion-worker + depends_on: + - centurion + - rabbitmq + + + + rabbitmq: + image: rabbitmq:4.0.3-management-alpine + container_name: rabbitmq + environment: + - RABBITMQ_DEFAULT_USER=admin + - RABBITMQ_DEFAULT_PASS=admin + - RABBITMQ_DEFAULT_VHOST=itsm + # ports: + # - "5672:5672" + # - "15672:15672" + depends_on: + - centurion + network_mode: "host" + # networks: + # - default + + + +# networks: +# default: +# driver: bridge +# external: true