Skip to content

Commit 1871b0f

Browse files
authored
Merge pull request #230 from dinesh-aot/COMP-352
Delete images
2 parents 006e292 + 5a1ceed commit 1871b0f

File tree

7 files changed

+199
-31
lines changed

7 files changed

+199
-31
lines changed

compliance-api/src/compliance_api/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class _Config: # pylint: disable=too-few-public-methods
8383
KEYCLOAK_ADMIN_SECRET = os.getenv("MET_ADMIN_CLIENT_SECRET")
8484
AUTH_BASE_URL = os.getenv("AUTH_BASE_URL")
8585
EPIC_TRACK_URL = os.getenv("EPIC_TRACK_URL")
86+
DOC_SERVICE_URL = os.getenv("DOC_SERVICE_URL")
8687

8788
JWT_OIDC_WELL_KNOWN_CONFIG = os.getenv("JWT_OIDC_WELL_KNOWN_CONFIG")
8889
JWT_OIDC_ALGORITHMS = os.getenv("JWT_OIDC_ALGORITHMS", "RS256")

compliance-api/src/compliance_api/models/inspection/inspection_req_image.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Model to handle the image uploads in inspection requirements."""
22

3-
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String
3+
from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, func
44
from sqlalchemy.orm import relationship
55

6+
from compliance_api.utils.constant import DELETE_DIC_PARAMS
7+
68
from ..base_model import BaseModelVersioned
79
from ..utils import with_session
810
from .inspection_enum import ImageTypeEnum
@@ -91,3 +93,25 @@ def find_all_images(cls, requirement_id, image_type: ImageTypeEnum):
9193
is_active=True,
9294
is_deleted=False,
9395
).all()
96+
97+
@classmethod
98+
def find_image_by_url(cls, requirement_id, relative_url, image_type):
99+
"""Get image object by url."""
100+
return cls.query.filter(
101+
cls.requirement_id == requirement_id,
102+
func.lower(cls.relative_url) == relative_url.lower(),
103+
cls.image_type == image_type,
104+
cls.is_active.is_(True),
105+
cls.is_deleted.is_(False),
106+
).first()
107+
108+
@classmethod
109+
@with_session
110+
def delete_image(cls, image_id, session=None):
111+
"""Delete the image."""
112+
image = cls.find_by_id(image_id)
113+
if not image:
114+
return None
115+
image.update(DELETE_DIC_PARAMS, commit=False)
116+
session.flush()
117+
return image

compliance-api/src/compliance_api/resources/inspection_requirement.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from http import HTTPStatus
44

5+
from flask import request
56
from flask_restx import Namespace, Resource
67

78
from compliance_api.auth import auth
9+
from compliance_api.exceptions import BadRequestError
810
from compliance_api.models.inspection import ImageTypeEnum
911
from compliance_api.schemas.inspection_requirement import (
1012
InspectionReqImageSchema, InspectionRequirementCreateSchema, InspectionRequirementSchema,
@@ -164,13 +166,32 @@ class InspectionReqPhotos(Resource):
164166
)
165167
@auth.require
166168
@API.response(code=200, description="Success", model=[inspesction_req_image_schema])
167-
def get(requirement_id):
169+
def get(inspection_id, requirement_id):
168170
"""Get all photos by requirement_id."""
169171
photos = InspectionRequirementService.get_all_images(
170-
requirement_id, ImageTypeEnum.PHOTO
172+
inspection_id, requirement_id, ImageTypeEnum.PHOTO
171173
)
172174
return InspectionReqImageSchema(many=True).dump(photos), HTTPStatus.OK
173175

176+
@staticmethod
177+
@auth.require
178+
@auth.has_one_of_roles([PermissionEnum.SUPERUSER])
179+
@ApiHelper.swagger_decorators(
180+
API, endpoint_description="Delete an inspection requirement photo"
181+
)
182+
@API.response(code=204, description="Success")
183+
@API.response(400, "Bad Request")
184+
@API.response(404, "Not Found")
185+
def delete(inspection_id, requirement_id):
186+
"""Delete complaint."""
187+
relative_url = request.args.get("relative_url", None)
188+
if not relative_url:
189+
raise BadRequestError("No 'relative_url' is passed as query parameter")
190+
InspectionRequirementService.delete_image(
191+
inspection_id, requirement_id, relative_url, ImageTypeEnum.PHOTO
192+
)
193+
return {}, HTTPStatus.NO_CONTENT
194+
174195

175196
@cors_preflight("GET, OPTIONS, DELETE")
176197
@API.route("/<int:requirement_id>/figures", methods=["OPTIONS", "GET", "DELETE"])
@@ -183,9 +204,28 @@ class InspectionReqFigures(Resource):
183204
)
184205
@auth.require
185206
@API.response(code=200, description="Success", model=[inspesction_req_image_schema])
186-
def get(requirement_id):
207+
def get(inspection_id, requirement_id):
187208
"""Get all figures by requirement_id."""
188209
figures = InspectionRequirementService.get_all_images(
189-
requirement_id, ImageTypeEnum.FIGURE
210+
inspection_id, requirement_id, ImageTypeEnum.FIGURE
190211
)
191212
return InspectionReqImageSchema(many=True).dump(figures), HTTPStatus.OK
213+
214+
@staticmethod
215+
@auth.require
216+
@auth.has_one_of_roles([PermissionEnum.SUPERUSER])
217+
@ApiHelper.swagger_decorators(
218+
API, endpoint_description="Delete an inspection requirement figure"
219+
)
220+
@API.response(code=204, description="Success")
221+
@API.response(400, "Bad Request")
222+
@API.response(404, "Not Found")
223+
def delete(inspection_id, requirement_id):
224+
"""Delete complaint."""
225+
relative_url = request.args.get("relative_url", None)
226+
if not relative_url:
227+
raise BadRequestError("No 'relative_url' is passed as query parameter")
228+
InspectionRequirementService.delete_image(
229+
inspection_id, requirement_id, relative_url, ImageTypeEnum.FIGURE
230+
)
231+
return {}, HTTPStatus.NO_CONTENT
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Constants associated with the document service."""
2+
3+
API_REQUEST_TIMEOUT = 120
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Class to manage document service."""
2+
3+
import requests
4+
from flask import current_app, g, json
5+
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
6+
7+
from compliance_api.exceptions import BusinessError
8+
from compliance_api.utils.enum import HttpMethod
9+
10+
from .constant import API_REQUEST_TIMEOUT
11+
12+
13+
class DocService:
14+
"""Doc service."""
15+
16+
@staticmethod
17+
def get_presigned_url(payload: dict):
18+
"""Get presigned url for the given action on the given file."""
19+
response = _request_doc_service(
20+
"storage-operations/presigned-urls", HttpMethod.POST, payload
21+
)
22+
if response.status_code != 200:
23+
raise BusinessError("Error contacting the document service")
24+
return response.json()
25+
26+
27+
@retry(
28+
retry=retry_if_exception_type(requests.exceptions.RequestException),
29+
stop=stop_after_attempt(3), # Retry up to 3 times
30+
wait=wait_fixed(2), # Wait 2 seconds between retries
31+
)
32+
def _request_doc_service(
33+
relative_url, http_method: HttpMethod = HttpMethod.GET, data=None
34+
):
35+
"""REST Api call to doc service."""
36+
token = getattr(g, "access_token", None)
37+
if not token:
38+
raise BusinessError("No access token found", 401)
39+
auth_base_url = current_app.config["DOC_SERVICE_URL"]
40+
headers = {
41+
"Content-Type": "application/json",
42+
"Authorization": f"Bearer {token}",
43+
}
44+
45+
url = f"{auth_base_url}/api/{relative_url}"
46+
47+
if http_method == HttpMethod.GET:
48+
response = requests.get(url, headers=headers, timeout=60)
49+
elif http_method == HttpMethod.POST:
50+
response = requests.post(
51+
url=url, headers=headers, data=json.dumps(data), timeout=API_REQUEST_TIMEOUT
52+
)
53+
else:
54+
raise ValueError("Invalid HTTP method")
55+
response.raise_for_status()
56+
return response
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Enums related to document service."""
2+
3+
from enum import Enum
4+
5+
6+
class ActionOnFileEnum(Enum):
7+
"""ActionOnFileEnum."""
8+
9+
PUT = "PUT"
10+
DELETE = "DELETE"

compliance-api/src/compliance_api/services/inspection_requirement.py

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""InspectionRequirementService."""
22

3+
import requests
34
from flask import g
45

56
from compliance_api.auth import auth
6-
from compliance_api.exceptions import BadRequestError, PermissionDeniedError, ResourceNotFoundError
7+
from compliance_api.exceptions import (
8+
BadRequestError, PermissionDeniedError, ResourceNotFoundError, UnprocessableEntityError)
79
from compliance_api.models import ImageTypeEnum
810
from compliance_api.models import Inspection as InspectionModel
911
from compliance_api.models import InspectionReqDetailDocument as InspectionReqDetailDocumentModel
@@ -12,6 +14,8 @@
1214
from compliance_api.models import InspectionRequirement as InspectionRequirementModel
1315
from compliance_api.models import InspectionRequirementImage as InspectionRequirementImageModel
1416
from compliance_api.models.db import session_scope
17+
from compliance_api.services.document_service.doc_service import DocService
18+
from compliance_api.services.document_service.doc_service_enum import ActionOnFileEnum
1519
from compliance_api.utils.enum import PermissionEnum
1620

1721

@@ -49,13 +53,13 @@ def create(cls, inspection_id, requirement_data):
4953
session,
5054
)
5155
# inserting photos
52-
_insert_images(
56+
_insert_or_update_images(
5357
requirement_id=created_requirement.id,
5458
images=requirement_data.get("photos", []),
5559
image_type=ImageTypeEnum.PHOTO,
5660
session=session,
5761
)
58-
_insert_images(
62+
_insert_or_update_images(
5963
requirement_id=created_requirement.id,
6064
images=requirement_data.get("figures", []),
6165
image_type=ImageTypeEnum.FIGURE,
@@ -85,13 +89,13 @@ def update(cls, inspection_id, requirement_id, requirement_data):
8589
requirement_data.get("enforcement_action_ids", []),
8690
session,
8791
)
88-
_update_images(
92+
_insert_or_update_images(
8993
requirement_id=requirement_id,
9094
images=requirement_data.get("photos", []),
9195
image_type=ImageTypeEnum.PHOTO,
9296
session=session,
9397
)
94-
_update_images(
98+
_insert_or_update_images(
9599
requirement_id=requirement_id,
96100
images=requirement_data.get("figures", []),
97101
image_type=ImageTypeEnum.FIGURE,
@@ -167,12 +171,40 @@ def insert_or_update_enforcements(
167171
)
168172

169173
@classmethod
170-
def get_all_images(cls, requirement_id, image_type: ImageTypeEnum):
174+
def get_all_images(cls, inspection_id, requirement_id, image_type: ImageTypeEnum):
171175
"""Get all photos."""
176+
_inspection_check(inspection_id)
177+
_requirement_check(requirement_id)
172178
return InspectionRequirementImageModel.find_all_images(
173179
requirement_id=requirement_id, image_type=image_type
174180
)
175181

182+
@classmethod
183+
def delete_image(cls, inspection_id, requirement_id, relative_url, image_type):
184+
"""Delete image."""
185+
_inspection_check(inspection_id)
186+
_requirement_check(requirement_id)
187+
image = InspectionRequirementImageModel.find_image_by_url(
188+
requirement_id, relative_url, image_type
189+
)
190+
if not image:
191+
raise UnprocessableEntityError(
192+
f"No {image_type.value} found for the given relative url"
193+
)
194+
# Get the presigned delete url for the file
195+
presigned_url_response = DocService.get_presigned_url(
196+
{
197+
"relative_url": image.relative_url,
198+
"action": ActionOnFileEnum.DELETE.value,
199+
}
200+
)
201+
presigned_delete_url = presigned_url_response.get("presigned_url")
202+
# Delete the actual file from cloud storage
203+
delete_response = requests.delete(presigned_delete_url, timeout=120)
204+
# Mark the deletion in the inspection_req_images table
205+
InspectionRequirementImageModel.delete_image(image.id)
206+
return delete_response
207+
176208

177209
def _create_image_obj(requirement_id, index, img: dict, image_type):
178210
"""Prepare the image object."""
@@ -188,26 +220,33 @@ def _create_image_obj(requirement_id, index, img: dict, image_type):
188220
}
189221

190222

191-
def _insert_images(
223+
def _insert_or_update_images(
192224
requirement_id, images: list[dict], image_type: ImageTypeEnum, session=None
193225
):
194-
"""Insert the images."""
195-
if images and len(images) > 0:
196-
images = [
197-
InspectionRequirementImageModel(
198-
**_create_image_obj(requirement_id, index, img, image_type)
199-
)
200-
for index, img in enumerate(images)
201-
]
202-
InspectionRequirementImageModel.bulk_insert(images, session=session)
226+
"""Update the images."""
227+
# Fetch existing images from the database
228+
existing_images = InspectionRequirementImageModel.find_all_images(
229+
requirement_id=requirement_id, image_type=image_type
230+
)
231+
existing_image_ids = {img.id for img in existing_images}
203232

233+
# Track incoming image IDs (existing ones)
234+
incoming_image_ids = {img["id"] for img in images if "id" in img}
204235

205-
def _update_images(
206-
requirement_id, images: list[dict], image_type: ImageTypeEnum, session=None
207-
):
208-
"""Update the images."""
236+
# DELETE: Remove images that exist in DB but are not in the new list
237+
images_to_delete = existing_image_ids - incoming_image_ids
238+
for image_id in images_to_delete:
239+
InspectionRequirementImageModel.delete_image(image_id, session=session)
240+
241+
# INSERT or UPDATE images while maintaining order
209242
for index, img in enumerate(images):
210-
if not img.get("id", None):
243+
img["sort_order"] = index + 1 # Maintain order
244+
245+
if "id" in img: # Update existing image
246+
InspectionRequirementImageModel.update_image(
247+
img["id"], img, session=session
248+
)
249+
else: # Insert new image
211250
image_obj = _create_image_obj(
212251
requirement_id=requirement_id,
213252
index=index,
@@ -217,11 +256,6 @@ def _update_images(
217256
InspectionRequirementImageModel.create_image(
218257
image_obj=image_obj, session=session
219258
)
220-
if img.get("id", None):
221-
img["sort_order"] = index + 1
222-
InspectionRequirementImageModel.update_image(
223-
img.get("id"), img, session=session
224-
)
225259

226260

227261
def _update_sort_order_subsequent(requirements, commit=False):

0 commit comments

Comments
 (0)