Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
"""Complaint Options."""

from enum import Enum
from ..option_base_model import OptionModel


class ComplaintSourceEnum(Enum):
"""Enumeration for Complaint Sources."""

PUBLIC = "Public"
FIRST_NATION = "First Nation"
FIRST_NATIONS_ALLIANCE = "First Nations Alliance"
AGENCY = "Agency"
OTHER = "Other"


class ComplaintSource(OptionModel):
"""ComplaintSource model."""

Expand Down
11 changes: 11 additions & 0 deletions compliance-api/src/compliance_api/models/report_enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Report type enum."""
import enum


class ReportTypeEnum(enum.Enum):
"""Report type enum."""

PROJECT_COMPLIANCE = "project-compliance"
CEB_SUMMARY = "ceb-summary"
CASE_FILE_MANAGEMENT = "case-file-management"
FIRST_NATION = "first-nation"
2 changes: 2 additions & 0 deletions compliance-api/src/compliance_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from .topic import API as TOPIC_API
from .violation_ticket import API as VIOLATION_TICKET_API
from .warning_letter import API as WARNING_LETTER_API
from .report import API as REPORT_API


__all__ = ("API_BLUEPRINT", "OPS_BLUEPRINT")
Expand Down Expand Up @@ -124,3 +125,4 @@
API.add_namespace(INSPECTION_REQUIREMENTS_API, path="inspection-requirements")
API.add_namespace(SENTENCE_TYPE_OPTION_API, path="sentence-type-options")
API.add_namespace(FILE_JOB_API, path="document-jobs")
API.add_namespace(REPORT_API, path="reports")
2 changes: 1 addition & 1 deletion compliance-api/src/compliance_api/resources/complaint.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def get():
@API.response(code=201, model=complaint_list_model, description="ComplaintCreated")
@API.response(400, "Bad Request")
def post():
"""Create an complaint."""
"""Create a complaint."""
current_app.logger.info(f"Creating Complaint with payload: {API.payload}")
complaint_data = ComplaintCreateSchema().load(API.payload)
created_complaint = ComplaintService.create(complaint_data)
Expand Down
67 changes: 67 additions & 0 deletions compliance-api/src/compliance_api/resources/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright © 2024 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API endpoints for managing report resources."""

from datetime import datetime
from io import BytesIO

from flask import current_app, request, send_file
from flask_restx import Namespace, Resource

from compliance_api.services.report.report import ReportService
from compliance_api.auth import auth
from compliance_api.utils.util import cors_preflight
from compliance_api.schemas.report import ReportGenerationSchema


from .apihelper import Api as ApiHelper


API = Namespace(
"reports",
description="Endpoints for Report Management",
)

report_generation_schema = ApiHelper.convert_ma_schema_to_restx_model(
API, ReportGenerationSchema(), "ReportGenerationSchema"
)


@cors_preflight("POST, OPTIONS")
@API.route("/export", methods=["POST", "OPTIONS"])
class Reports(Resource):
"""Resource for managing reports."""

@staticmethod
@ApiHelper.swagger_decorators(API, endpoint_description="Fetch report")
@API.expect(report_generation_schema)
@API.doc()
@API.response(code=200, description="Success - Excel file generated")
@auth.require
def post():
"""Fetch all reports."""
schema = ReportGenerationSchema()

report_data = schema.load(request.json or {})
report_type = report_data.get("report_type")

try:
data = ReportService.generate_report(report_data, report_type)
except ValueError as value_error:
current_app.logger.error(f"Error generating report: {value_error}")
return {"message": str(value_error)}, 400
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_name = f"{report_type}_{timestamp}.xlsx"

return send_file(BytesIO(data), as_attachment=True, download_name=file_name)
46 changes: 46 additions & 0 deletions compliance-api/src/compliance_api/schemas/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Schema for report generation parameters."""
from marshmallow import Schema, fields
from marshmallow_enum import EnumField
from compliance_api.models.report_enum import ReportTypeEnum


class ReportGenerationSchema(Schema):
"""Schema to filter the case files for export and pagination."""

report_type = EnumField(
ReportTypeEnum,
metadata={"description": "Type of report to generate"},
by_value=True,
required=True,
)

project_id = fields.Int(
required=False,
allow_none=True,
metadata={"description": "Project ID for report"},
)

officer_ids = fields.List(
fields.Int(),
required=False,
allow_none=True,
metadata={"description": "List of officer IDs for report"},
)

first_nation_id = fields.Int(
required=False,
allow_none=True,
metadata={"description": "First Nation Alliance ID for report"},
)

start_date = fields.DateTime(
required=False,
allow_none=True,
metadata={"description": "Start date for report"},
)

end_date = fields.DateTime(
required=False,
allow_none=True,
metadata={"description": "End date for report"},
)
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,39 @@ def get_first_nation_by_id(first_nation_id: int):
f"Unable to parse First Nation information for ID {first_nation_id}"
)

@staticmethod
def get_first_nations():
"""Return firstnations."""
try:
first_nation_response = _request_track_service("indigenous-nations")
if first_nation_response.status_code != 200:
current_app.logger.error(
f"EPIC.track returned status {first_nation_response.status_code} for GET first nations."
)
raise BadRequestError(
"Unable to retrieve First Nation information at this time"
)

return first_nation_response.json()
except (RetryError, requests.exceptions.RequestException) as e:
current_app.logger.error(
f"EPIC.track service unavailable for GET first nations: {str(e)}",
exc_info=True
)
raise BadRequestError(
"The First Nation information service is temporarily unavailable. Please try again later."
)
except (ResourceNotFoundError, BadRequestError):
raise
except (KeyError, ValueError, TypeError) as e:
current_app.logger.error(
f"Error parsing first nation data for GET first nations: {str(e)}",
exc_info=True
)
raise BadRequestError(
"Unable to parse First Nation information for GET first nations"
)


@retry(
retry=retry_if_exception_type(requests.exceptions.RequestException),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1351,51 +1351,6 @@ def _apply_requirement_source_number_sort(query, subq, sort_order):
)


def _get_enforcement_number_by_type(result):
"""Get the correct enforcement number based on the enforcement action type."""
enforcement_action_id = result.enforcement_action_id

# Map enforcement action ID to the corresponding number field attribute
enforcement_number_map = {
EnforcementActionOptionEnum.ORDER.value: "order_number",
EnforcementActionOptionEnum.WARNING_LETTER.value: "warning_letter_number",
EnforcementActionOptionEnum.VIOLATION_TICKET.value: "violation_ticket_number",
EnforcementActionOptionEnum.ADMINISTRATIVE_PENALTY_RECOMMENDATION.value: "admin_penalty_number",
EnforcementActionOptionEnum.CHARGE_RECOMMENDATION.value: "charge_rec_number",
EnforcementActionOptionEnum.RESTORATIVE_JUSTICE.value: "restorative_justice_number",
}

field_name = enforcement_number_map.get(enforcement_action_id)
if field_name:
return getattr(result, field_name, "") or ""
return ""


def _get_enforcement_status_by_type(result): # pylint: disable=too-many-return-statements
"""Get the correct enforcement status based on the enforcement action type."""
enforcement_action_id = result.enforcement_action_id

# Map enforcement action ID to the corresponding status field
if enforcement_action_id == EnforcementActionOptionEnum.ORDER.value:
return result.order_status
if enforcement_action_id == EnforcementActionOptionEnum.WARNING_LETTER.value:
return result.warning_letter_status
if enforcement_action_id == EnforcementActionOptionEnum.VIOLATION_TICKET.value:
return result.violation_ticket_status
if (
enforcement_action_id
== EnforcementActionOptionEnum.ADMINISTRATIVE_PENALTY_RECOMMENDATION.value
):
return result.admin_penalty_status
if (
enforcement_action_id == EnforcementActionOptionEnum.CHARGE_RECOMMENDATION.value
):
return result.charge_rec_status
if enforcement_action_id == EnforcementActionOptionEnum.RESTORATIVE_JUSTICE.value:
return result.restorative_justice_status
return None


def _get_enforcement_progress_by_type(result):
"""Get the correct enforcement progress based on the enforcement action type."""
enforcement_action_id = result.enforcement_action_id
Expand Down Expand Up @@ -1503,15 +1458,15 @@ def _process_inspection_requirement_query_results(query_results):
item["inspection_status"] = result.inspection_status

# Convert enforcement status to proper object format
raw_status = _get_enforcement_status_by_type(result)
item["status"] = _convert_enum_to_object(raw_status) if raw_status else None
raw_status = ServiceUtils.get_enforcement_status_by_type(result)
item["status"] = ServiceUtils.convert_enum_to_object(raw_status) if raw_status else None

# Convert enforcement progress to proper object format
raw_progress = _get_enforcement_progress_by_type(result)
item["progress"] = (
_convert_enum_to_object(raw_progress) if raw_progress else None
ServiceUtils.convert_enum_to_object(raw_progress) if raw_progress else None
)
item["enforcement_number"] = _get_enforcement_number_by_type(result)
item["enforcement_number"] = ServiceUtils.get_enforcement_number_by_type(result)

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


def _convert_enum_to_object(enum_value):
"""Convert enum value to proper object format for API response."""
if not enum_value:
return None

# If it's already an enum object, convert it to the expected format
if hasattr(enum_value, "name") and hasattr(enum_value, "value"):
return {
"id": enum_value.name,
"name": enum_value.value,
}

# If it's just a string, return it as is (shouldn't happen with our current logic)
return {
"id": str(enum_value),
"name": str(enum_value),
}


def _make_requirement_detail_object(requirements: list):
"""Make requirement detail object."""

Expand Down
16 changes: 16 additions & 0 deletions compliance-api/src/compliance_api/services/report/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Report Generator Base Class."""
from abc import ABC, abstractmethod


class BaseReportGenerator(ABC):
"""Base class for report generators."""

@abstractmethod
def __init__(self, report_data):
"""Initialize the report generator with the provided report data."""
self.report_data = report_data

@abstractmethod
def generate(self):
"""Generate the report."""
pass
Loading