Skip to content

Commit 863793a

Browse files
tom0827tolkamps1
andauthored
[COMP-744]: System Reports (#754)
* [COMP-751][COMP-752]: System Reports (#716) * COMP-752 System Reports * COMP-752 fetch projects from track using special history * [Comp-750] Project Compliance report (#738) * [Comp-753] First nations report (#744) * adding case file number column ad hiding report frontend (#753) --------- Co-authored-by: Shaelyn Tolkamp <46355612+tolkamps1@users.noreply.github.com>
1 parent fb925a5 commit 863793a

File tree

26 files changed

+3717
-165
lines changed

26 files changed

+3717
-165
lines changed

compliance-api/src/compliance_api/models/complaint/complaint_option.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
"""Complaint Options."""
22

3+
from enum import Enum
34
from ..option_base_model import OptionModel
45

56

7+
class ComplaintSourceEnum(Enum):
8+
"""Enumeration for Complaint Sources."""
9+
10+
PUBLIC = "Public"
11+
FIRST_NATION = "First Nation"
12+
FIRST_NATIONS_ALLIANCE = "First Nations Alliance"
13+
AGENCY = "Agency"
14+
OTHER = "Other"
15+
16+
617
class ComplaintSource(OptionModel):
718
"""ComplaintSource model."""
819

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Report type enum."""
2+
import enum
3+
4+
5+
class ReportTypeEnum(enum.Enum):
6+
"""Report type enum."""
7+
8+
PROJECT_COMPLIANCE = "project-compliance"
9+
CEB_SUMMARY = "ceb-summary"
10+
CASE_FILE_MANAGEMENT = "case-file-management"
11+
FIRST_NATION = "first-nation"

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .topic import API as TOPIC_API
5656
from .violation_ticket import API as VIOLATION_TICKET_API
5757
from .warning_letter import API as WARNING_LETTER_API
58+
from .report import API as REPORT_API
5859

5960

6061
__all__ = ("API_BLUEPRINT", "OPS_BLUEPRINT")
@@ -124,3 +125,4 @@
124125
API.add_namespace(INSPECTION_REQUIREMENTS_API, path="inspection-requirements")
125126
API.add_namespace(SENTENCE_TYPE_OPTION_API, path="sentence-type-options")
126127
API.add_namespace(FILE_JOB_API, path="document-jobs")
128+
API.add_namespace(REPORT_API, path="reports")

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def get():
170170
@API.response(code=201, model=complaint_list_model, description="ComplaintCreated")
171171
@API.response(400, "Bad Request")
172172
def post():
173-
"""Create an complaint."""
173+
"""Create a complaint."""
174174
current_app.logger.info(f"Creating Complaint with payload: {API.payload}")
175175
complaint_data = ComplaintCreateSchema().load(API.payload)
176176
created_complaint = ComplaintService.create(complaint_data)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright © 2024 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the 'License');
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an 'AS IS' BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""API endpoints for managing report resources."""
15+
16+
from datetime import datetime
17+
from io import BytesIO
18+
19+
from flask import current_app, request, send_file
20+
from flask_restx import Namespace, Resource
21+
22+
from compliance_api.services.report.report import ReportService
23+
from compliance_api.auth import auth
24+
from compliance_api.utils.util import cors_preflight
25+
from compliance_api.schemas.report import ReportGenerationSchema
26+
27+
28+
from .apihelper import Api as ApiHelper
29+
30+
31+
API = Namespace(
32+
"reports",
33+
description="Endpoints for Report Management",
34+
)
35+
36+
report_generation_schema = ApiHelper.convert_ma_schema_to_restx_model(
37+
API, ReportGenerationSchema(), "ReportGenerationSchema"
38+
)
39+
40+
41+
@cors_preflight("POST, OPTIONS")
42+
@API.route("/export", methods=["POST", "OPTIONS"])
43+
class Reports(Resource):
44+
"""Resource for managing reports."""
45+
46+
@staticmethod
47+
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch report")
48+
@API.expect(report_generation_schema)
49+
@API.doc()
50+
@API.response(code=200, description="Success - Excel file generated")
51+
@auth.require
52+
def post():
53+
"""Fetch all reports."""
54+
schema = ReportGenerationSchema()
55+
56+
report_data = schema.load(request.json or {})
57+
report_type = report_data.get("report_type")
58+
59+
try:
60+
data = ReportService.generate_report(report_data, report_type)
61+
except ValueError as value_error:
62+
current_app.logger.error(f"Error generating report: {value_error}")
63+
return {"message": str(value_error)}, 400
64+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
65+
file_name = f"{report_type}_{timestamp}.xlsx"
66+
67+
return send_file(BytesIO(data), as_attachment=True, download_name=file_name)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Schema for report generation parameters."""
2+
from marshmallow import Schema, fields
3+
from marshmallow_enum import EnumField
4+
from compliance_api.models.report_enum import ReportTypeEnum
5+
6+
7+
class ReportGenerationSchema(Schema):
8+
"""Schema to filter the case files for export and pagination."""
9+
10+
report_type = EnumField(
11+
ReportTypeEnum,
12+
metadata={"description": "Type of report to generate"},
13+
by_value=True,
14+
required=True,
15+
)
16+
17+
project_id = fields.Int(
18+
required=False,
19+
allow_none=True,
20+
metadata={"description": "Project ID for report"},
21+
)
22+
23+
officer_ids = fields.List(
24+
fields.Int(),
25+
required=False,
26+
allow_none=True,
27+
metadata={"description": "List of officer IDs for report"},
28+
)
29+
30+
first_nation_id = fields.Int(
31+
required=False,
32+
allow_none=True,
33+
metadata={"description": "First Nation Alliance ID for report"},
34+
)
35+
36+
start_date = fields.DateTime(
37+
required=False,
38+
allow_none=True,
39+
metadata={"description": "Start date for report"},
40+
)
41+
42+
end_date = fields.DateTime(
43+
required=False,
44+
allow_none=True,
45+
metadata={"description": "End date for report"},
46+
)

compliance-api/src/compliance_api/services/epic_track_service/track_service.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,39 @@ def get_first_nation_by_id(first_nation_id: int):
132132
f"Unable to parse First Nation information for ID {first_nation_id}"
133133
)
134134

135+
@staticmethod
136+
def get_first_nations():
137+
"""Return firstnations."""
138+
try:
139+
first_nation_response = _request_track_service("indigenous-nations")
140+
if first_nation_response.status_code != 200:
141+
current_app.logger.error(
142+
f"EPIC.track returned status {first_nation_response.status_code} for GET first nations."
143+
)
144+
raise BadRequestError(
145+
"Unable to retrieve First Nation information at this time"
146+
)
147+
148+
return first_nation_response.json()
149+
except (RetryError, requests.exceptions.RequestException) as e:
150+
current_app.logger.error(
151+
f"EPIC.track service unavailable for GET first nations: {str(e)}",
152+
exc_info=True
153+
)
154+
raise BadRequestError(
155+
"The First Nation information service is temporarily unavailable. Please try again later."
156+
)
157+
except (ResourceNotFoundError, BadRequestError):
158+
raise
159+
except (KeyError, ValueError, TypeError) as e:
160+
current_app.logger.error(
161+
f"Error parsing first nation data for GET first nations: {str(e)}",
162+
exc_info=True
163+
)
164+
raise BadRequestError(
165+
"Unable to parse First Nation information for GET first nations"
166+
)
167+
135168

136169
@retry(
137170
retry=retry_if_exception_type(requests.exceptions.RequestException),

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

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,51 +1351,6 @@ def _apply_requirement_source_number_sort(query, subq, sort_order):
13511351
)
13521352

13531353

1354-
def _get_enforcement_number_by_type(result):
1355-
"""Get the correct enforcement number based on the enforcement action type."""
1356-
enforcement_action_id = result.enforcement_action_id
1357-
1358-
# Map enforcement action ID to the corresponding number field attribute
1359-
enforcement_number_map = {
1360-
EnforcementActionOptionEnum.ORDER.value: "order_number",
1361-
EnforcementActionOptionEnum.WARNING_LETTER.value: "warning_letter_number",
1362-
EnforcementActionOptionEnum.VIOLATION_TICKET.value: "violation_ticket_number",
1363-
EnforcementActionOptionEnum.ADMINISTRATIVE_PENALTY_RECOMMENDATION.value: "admin_penalty_number",
1364-
EnforcementActionOptionEnum.CHARGE_RECOMMENDATION.value: "charge_rec_number",
1365-
EnforcementActionOptionEnum.RESTORATIVE_JUSTICE.value: "restorative_justice_number",
1366-
}
1367-
1368-
field_name = enforcement_number_map.get(enforcement_action_id)
1369-
if field_name:
1370-
return getattr(result, field_name, "") or ""
1371-
return ""
1372-
1373-
1374-
def _get_enforcement_status_by_type(result): # pylint: disable=too-many-return-statements
1375-
"""Get the correct enforcement status based on the enforcement action type."""
1376-
enforcement_action_id = result.enforcement_action_id
1377-
1378-
# Map enforcement action ID to the corresponding status field
1379-
if enforcement_action_id == EnforcementActionOptionEnum.ORDER.value:
1380-
return result.order_status
1381-
if enforcement_action_id == EnforcementActionOptionEnum.WARNING_LETTER.value:
1382-
return result.warning_letter_status
1383-
if enforcement_action_id == EnforcementActionOptionEnum.VIOLATION_TICKET.value:
1384-
return result.violation_ticket_status
1385-
if (
1386-
enforcement_action_id
1387-
== EnforcementActionOptionEnum.ADMINISTRATIVE_PENALTY_RECOMMENDATION.value
1388-
):
1389-
return result.admin_penalty_status
1390-
if (
1391-
enforcement_action_id == EnforcementActionOptionEnum.CHARGE_RECOMMENDATION.value
1392-
):
1393-
return result.charge_rec_status
1394-
if enforcement_action_id == EnforcementActionOptionEnum.RESTORATIVE_JUSTICE.value:
1395-
return result.restorative_justice_status
1396-
return None
1397-
1398-
13991354
def _get_enforcement_progress_by_type(result):
14001355
"""Get the correct enforcement progress based on the enforcement action type."""
14011356
enforcement_action_id = result.enforcement_action_id
@@ -1503,15 +1458,15 @@ def _process_inspection_requirement_query_results(query_results):
15031458
item["inspection_status"] = result.inspection_status
15041459

15051460
# Convert enforcement status to proper object format
1506-
raw_status = _get_enforcement_status_by_type(result)
1507-
item["status"] = _convert_enum_to_object(raw_status) if raw_status else None
1461+
raw_status = ServiceUtils.get_enforcement_status_by_type(result)
1462+
item["status"] = ServiceUtils.convert_enum_to_object(raw_status) if raw_status else None
15081463

15091464
# Convert enforcement progress to proper object format
15101465
raw_progress = _get_enforcement_progress_by_type(result)
15111466
item["progress"] = (
1512-
_convert_enum_to_object(raw_progress) if raw_progress else None
1467+
ServiceUtils.convert_enum_to_object(raw_progress) if raw_progress else None
15131468
)
1514-
item["enforcement_number"] = _get_enforcement_number_by_type(result)
1469+
item["enforcement_number"] = ServiceUtils.get_enforcement_number_by_type(result)
15151470

15161471
# Add all requirement source names
15171472
item["requirement_sources"] = getattr(result, "requirement_sources", None)
@@ -1520,25 +1475,6 @@ def _process_inspection_requirement_query_results(query_results):
15201475
return processed_requirements
15211476

15221477

1523-
def _convert_enum_to_object(enum_value):
1524-
"""Convert enum value to proper object format for API response."""
1525-
if not enum_value:
1526-
return None
1527-
1528-
# If it's already an enum object, convert it to the expected format
1529-
if hasattr(enum_value, "name") and hasattr(enum_value, "value"):
1530-
return {
1531-
"id": enum_value.name,
1532-
"name": enum_value.value,
1533-
}
1534-
1535-
# If it's just a string, return it as is (shouldn't happen with our current logic)
1536-
return {
1537-
"id": str(enum_value),
1538-
"name": str(enum_value),
1539-
}
1540-
1541-
15421478
def _make_requirement_detail_object(requirements: list):
15431479
"""Make requirement detail object."""
15441480

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Report Generator Base Class."""
2+
from abc import ABC, abstractmethod
3+
4+
5+
class BaseReportGenerator(ABC):
6+
"""Base class for report generators."""
7+
8+
@abstractmethod
9+
def __init__(self, report_data):
10+
"""Initialize the report generator with the provided report data."""
11+
self.report_data = report_data
12+
13+
@abstractmethod
14+
def generate(self):
15+
"""Generate the report."""
16+
pass

0 commit comments

Comments
 (0)