diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c421c78..7492e97 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -70,7 +70,7 @@ jobs: - name: Run Coverage test working-directory: deployment run: | - cat << EOF | docker-compose exec -T dev bash + cat << EOF | docker compose exec -T dev bash python manage.py makemigrations python manage.py migrate python manage.py collectstatic --noinput --verbosity 0 diff --git a/Makefile b/Makefile index 3ee4cb5..5673b9d 100644 --- a/Makefile +++ b/Makefile @@ -6,17 +6,17 @@ build: @echo "------------------------------------------------------------------" @echo "Building in production mode" @echo "------------------------------------------------------------------" - @docker-compose build + @docker compose build build-dev: @echo @echo "------------------------------------------------------------------" @echo "Building in dev mode" @echo "------------------------------------------------------------------" - @docker-compose build dev + @docker compose build dev wait-db: - @docker-compose ${ARGS} exec -T db su - postgres -c "until pg_isready; do sleep 5; done" + @docker compose ${ARGS} exec -T db su - postgres -c "until pg_isready; do sleep 5; done" sleep: @echo @@ -31,53 +31,53 @@ up: @echo "------------------------------------------------------------------" @echo "Running in production mode" @echo "------------------------------------------------------------------" - @docker-compose ${ARGS} up -d nginx django worker celery_beat + @docker compose ${ARGS} up -d nginx django worker celery_beat dev: @echo @echo "------------------------------------------------------------------" @echo "Running in dev mode" @echo "------------------------------------------------------------------" - @docker-compose ${ARGS} up -d dev worker + @docker compose ${ARGS} up -d dev worker down: @echo @echo "------------------------------------------------------------------" @echo "Running in dev mode" @echo "------------------------------------------------------------------" - @docker-compose ${ARGS} down + @docker compose ${ARGS} down migrate: @echo @echo "------------------------------------------------------------------" @echo "Running migration" @echo "------------------------------------------------------------------" - @docker-compose ${ARGS} exec -T dev python manage.py migrate + @docker compose ${ARGS} exec -T dev python manage.py migrate dev-runserver: @echo @echo "------------------------------------------------------------------" @echo "Start django runserver in dev container" @echo "------------------------------------------------------------------" - @docker-compose $(ARGS) exec -T dev bash -c "nohup python manage.py runserver 0.0.0.0:8080 &" + @docker compose $(ARGS) exec -T dev bash -c "nohup python manage.py runserver 0.0.0.0:8080 &" dev-shell: @echo @echo "------------------------------------------------------------------" @echo "Start django runserver in dev container" @echo "------------------------------------------------------------------" - @docker-compose $(ARGS) exec dev bash + @docker compose $(ARGS) exec dev bash scale-worker: @echo @echo "------------------------------------------------------------------" @echo "scale-worker" @echo "------------------------------------------------------------------" - @docker-compose up -d worker --no-deps --no-recreate --scale worker=$(COUNT) + @docker compose up -d worker --no-deps --no-recreate --scale worker=$(COUNT) init-bucket: @echo @echo "------------------------------------------------------------------" @echo "Init createbuckets Minio" @echo "------------------------------------------------------------------" - @docker-compose ${ARGS} up -d createbuckets + @docker compose ${ARGS} up -d createbuckets diff --git a/codecov.yml b/codecov.yml index 4b3df4b..deed761 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,6 +9,6 @@ coverage: changes: false patch: default: - threshold: "5%" + threshold: "10%" ignore: - "**/migrations/*.py" diff --git a/django_project/core/settings/contrib.py b/django_project/core/settings/contrib.py index 1daafaf..c7cfc1c 100644 --- a/django_project/core/settings/contrib.py +++ b/django_project/core/settings/contrib.py @@ -25,7 +25,8 @@ 'DEFAULT_VERSIONING_CLASS': ( 'rest_framework.versioning.NamespaceVersioning' ), - 'EXCEPTION_HANDLER': 'core.tools.custom_exception_handler' + 'EXCEPTION_HANDLER': 'core.tools.custom_exception_handler', + 'DATETIME_FORMAT': '%Y-%m-%dT%H:%M:%SZ' } AUTHENTICATION_BACKENDS = ( diff --git a/django_project/cplus_api/api_views/scenario.py b/django_project/cplus_api/api_views/scenario.py index e3d36b2..0901646 100644 --- a/django_project/cplus_api/api_views/scenario.py +++ b/django_project/cplus_api/api_views/scenario.py @@ -18,11 +18,13 @@ ScenarioTaskStatusSerializer, ScenarioTaskLogListSerializer, ScenarioTaskLogSerializer, - PaginatedScenarioTaskStatusSerializer, - ScenarioDetailSerializer + PaginatedScenarioTaskItemSerializer, + ScenarioDetailSerializer, + ScenarioTaskItemSerializer ) from cplus_api.serializers.common import ( - APIErrorSerializer + APIErrorSerializer, + NoContentSerializer ) from cplus_api.utils.api_helper import ( SCENARIO_API_TAG, @@ -266,9 +268,18 @@ class ScenarioAnalysisHistory(APIView): @swagger_auto_schema( operation_id='scenario-analysis-history', tags=[SCENARIO_API_TAG], - manual_parameters=PARAMS_PAGINATION, + manual_parameters=[ + openapi.Parameter( + 'status', openapi.IN_QUERY, + description=( + 'Scenario status filter' + ), + type=openapi.TYPE_STRING, + required=False + ) + ] + PARAMS_PAGINATION, responses={ - 200: PaginatedScenarioTaskStatusSerializer, + 200: PaginatedScenarioTaskItemSerializer, 400: APIErrorSerializer, 404: APIErrorSerializer } @@ -278,7 +289,12 @@ def get(self, request, *args, **kwargs): page_size = get_page_size(request) scenarios = ScenarioTask.objects.filter( submitted_by=request.user - ).order_by('submitted_on') + ).order_by('-submitted_on') + status = request.GET.get('status', '') + if status: + scenarios = scenarios.filter( + status__in=status.split(',') + ) # set pagination paginator = Paginator(scenarios, page_size) total_page = math.ceil(paginator.count / page_size) @@ -287,7 +303,7 @@ def get(self, request, *args, **kwargs): else: paginated_entities = paginator.get_page(page) output = ( - ScenarioTaskStatusSerializer( + ScenarioTaskItemSerializer( paginated_entities, many=True ).data @@ -322,3 +338,23 @@ def get(self, request, *args, **kwargs): self.validate_user_access(request.user, scenario_task) return Response( status=200, data=ScenarioDetailSerializer(scenario_task).data) + + @swagger_auto_schema( + operation_id='scenario-analysis-remove', + operation_description='API to remove scenario analysis.', + tags=[SCENARIO_API_TAG], + manual_parameters=[PARAM_SCENARIO_UUID_IN_PATH], + responses={ + 204: NoContentSerializer, + 400: APIErrorSerializer, + 403: APIErrorSerializer, + 404: APIErrorSerializer + } + ) + def delete(self, request, *args, **kwargs): + scenario_uuid = kwargs.get('scenario_uuid') + scenario_task = get_object_or_404( + ScenarioTask, uuid=scenario_uuid) + self.validate_user_access(request.user, scenario_task, 'delete') + scenario_task.delete() + return Response(status=204) diff --git a/django_project/cplus_api/serializers/scenario.py b/django_project/cplus_api/serializers/scenario.py index 79c69f2..884513d 100644 --- a/django_project/cplus_api/serializers/scenario.py +++ b/django_project/cplus_api/serializers/scenario.py @@ -561,11 +561,53 @@ class ScenarioTaskLogListSerializer(serializers.ListSerializer): child = ScenarioTaskLogSerializer() -class PaginatedScenarioTaskStatusSerializer(serializers.Serializer): +class ScenarioTaskItemSerializer(ScenarioTaskStatusSerializer): + class Meta: + props = ( + ScenarioTaskStatusSerializer.Meta. + swagger_schema_fields['properties'] + ) + del props['logs'] + swagger_schema_fields = { + 'type': openapi.TYPE_OBJECT, + 'title': 'Scenario Task Item', + 'properties': { + **props, + 'detail': { + **ScenarioInputSerializer.Meta.swagger_schema_fields + } + }, + 'example': { + 'uuid': '8c4582ab-15b1-4ed0-b8e4-00640ec10a65', + 'task_id': '3e0c7dff-51f2-48c5-a316-15d9ca2407cb', + 'plugin_version': '1.0.0', + 'scenario_name': 'Scenario A', + 'status': 'Queued', + 'submitted_on': '2022-08-15T08:09:15.049806Z', + 'created_by': 'admin@admin.com', + 'started_at': '2022-08-15T08:09:15.049806Z', + 'finished_at': '2022-08-15T09:09:15.049806Z', + 'errors': None, + 'progress': 70, + 'progress_text': 'Processing ABC', + 'detail': {} + } + } + model = ScenarioTask + fields = [ + 'uuid', 'task_id', 'plugin_version', + 'scenario_name', 'status', 'submitted_on', + 'created_by', 'started_at', 'finished_at', + 'errors', 'progress', 'progress_text', + 'detail' + ] + + +class PaginatedScenarioTaskItemSerializer(serializers.Serializer): page = serializers.IntegerField() total_page = serializers.IntegerField() page_size = serializers.IntegerField() - results = ScenarioTaskStatusSerializer(many=True) + results = ScenarioTaskItemSerializer(many=True) class Meta: swagger_schema_fields = { @@ -588,7 +630,7 @@ class Meta: title='Results', type=openapi.TYPE_ARRAY, items=openapi.Items( - **ScenarioTaskStatusSerializer. + **ScenarioTaskItemSerializer. Meta.swagger_schema_fields ), ) diff --git a/django_project/cplus_api/tests/test_scenario_api_view.py b/django_project/cplus_api/tests/test_scenario_api_view.py index 246206c..82ee7cb 100644 --- a/django_project/cplus_api/tests/test_scenario_api_view.py +++ b/django_project/cplus_api/tests/test_scenario_api_view.py @@ -323,6 +323,15 @@ def test_scenario_history(self): self.assertEqual(len(response.data['results']), 1) scenario = response.data['results'][0] self.assertEqual(str(scenario_task.uuid), scenario['uuid']) + # filter completed status only + request = self.factory.get( + reverse('v1:scenario-history') + f'?status={TaskStatus.COMPLETED}' + ) + request.resolver_match = FakeResolverMatchV1 + request.user = self.user_1 + response = view(request) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data['results']), 0) def test_scenario_detail(self): view = ScenarioAnalysisTaskDetail.as_view() @@ -348,3 +357,14 @@ def test_scenario_detail(self): response.data['scenario_name'], scenario_task.detail['scenario_name'] ) + # delete scenario + request = self.factory.delete( + reverse('v1:scenario-detail', kwargs=kwargs) + ) + request.resolver_match = FakeResolverMatchV1 + request.user = self.superuser + response = view(request, **kwargs) + self.assertEqual(response.status_code, 204) + self.assertFalse(ScenarioTask.objects.filter( + id=scenario_task.id + ).exists()) diff --git a/django_project/cplus_api/utils/api_helper.py b/django_project/cplus_api/utils/api_helper.py index db52bf4..700a147 100644 --- a/django_project/cplus_api/utils/api_helper.py +++ b/django_project/cplus_api/utils/api_helper.py @@ -4,6 +4,7 @@ import traceback from datetime import datetime from uuid import UUID +from enum import Enum import boto3 import math @@ -232,7 +233,7 @@ class CustomJsonEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, UUID): # if the obj is uuid, we simply return the value of uuid - return obj.hex + return str(obj) if isinstance(obj, datetime): # if the obj is uuid, we simply return the value of uuid return obj.isoformat() @@ -240,7 +241,9 @@ def default(self, obj): def todict(obj, classkey=None): - if isinstance(obj, dict): + if isinstance(obj, Enum): + return obj.value + elif isinstance(obj, dict): data = {} for (k, v) in obj.items(): data[k] = todict(v, classkey)