Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: facility review fixes #2223

Merged
merged 17 commits into from
Sep 23, 2024
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
2 changes: 2 additions & 0 deletions bc_obps/registration/tests/utils/baker_recipes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from registration.models.event.transfer_event import TransferEvent
from registration.models.facility import Facility
from registration.models.multiple_operator import MultipleOperator
from registration.models.app_role import AppRole
from registration.models.opted_in_operation_detail import OptedInOperationDetail
Expand Down Expand Up @@ -121,3 +122,4 @@
other_operator=foreign_key(other_operator_for_transfer_event),
other_operator_contact=foreign_key(contact_for_transfer_event),
)
facility = Recipe(Facility, address=foreign_key(address))
1 change: 1 addition & 0 deletions bc_obps/reporting/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
from .build_form_schema import build_form_schema
from .operations import get_dashboard_operations_list
from .activity_data import get_initial_activity_data
from .facility_report import get_facility_report_form_data
75 changes: 75 additions & 0 deletions bc_obps/reporting/api/facility_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Literal, Tuple
from uuid import UUID
from django.http import HttpRequest
from registration.decorators import handle_http_errors
from reporting.constants import EMISSIONS_REPORT_TAGS
from reporting.schema.generic import Message
from service.facility_report_service import FacilityReportService
from service.error_service.custom_codes_4xx import custom_codes_4xx
from .router import router
from reporting.schema.facility_report import FacilityReportOut, FacilityReportIn
from registration.api.utils.current_user_utils import get_current_user_guid


@router.get(
"/report-version/{version_id}/facility-report/{facility_id}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌 👌

response={200: FacilityReportOut, 404: Message, 400: Message, 500: Message},
tags=EMISSIONS_REPORT_TAGS,
description="""Takes `version_id` (primary key of the ReportVersion model) and `facility_id` to return a single matching `facility_report` object.
Includes the associated activity IDs if found; otherwise, returns an error message if not found or in case of other issues.""",
)
@handle_http_errors()
def get_facility_report_form_data(
request: HttpRequest, version_id: int, facility_id: UUID
) -> Tuple[Literal[200], FacilityReportOut]:
facility_report = FacilityReportService.get_facility_report_by_version_and_id(version_id, facility_id)
return 200, FacilityReportOut(
id=facility_report.id,
report_version_id=facility_report.report_version.id,
facility_name=facility_report.facility_name,
facility_type=facility_report.facility_type,
facility_bcghgid=facility_report.facility_bcghgid,
activities=list(facility_report.activities.values_list('id', flat=True)),
products=list(facility_report.products.values_list('id', flat=True)),
)


@router.post(
"/report-version/{version_id}/facility-report/{facility_id}",
response={201: FacilityReportOut, custom_codes_4xx: Message},
tags=EMISSIONS_REPORT_TAGS,
description="""Updates the report facility details by version_id and facility_id. The request body should include
fields to be updated, such as facility name, type, BC GHG ID, activities, and products. Returns the updated report
facility object or an error message if the update fails.""",
)
@handle_http_errors()
def save_facility_report(
request: HttpRequest, version_id: int, facility_id: UUID, payload: FacilityReportIn
) -> Tuple[Literal[201], FacilityReportOut]:
"""
Save or update a report facility and its related activities.

Args:
request (HttpRequest): The HTTP request object.
version_id (int): The ID of the report version.
facility_id (int): The ID of the facility.
payload (FacilityReportIn): The input data for the report facility.

Returns:
Tuple: HTTP status code and the response data or an error message.
"""
# Fetch the existing facility report or create a new one
user_guid = get_current_user_guid(request)
facility_report = FacilityReportService.save_facility_report(version_id, facility_id, payload, user_guid)

# Prepare the response data
response_data = FacilityReportOut(
id=facility_id,
report_version_id=facility_report.report_version.id,
facility_name=facility_report.facility_name,
facility_type=facility_report.facility_type,
facility_bcghgid=facility_report.facility_bcghgid,
activities=list(facility_report.activities.values_list('id', flat=True)),
products=list(facility_report.products.values_list('id', flat=True)),
)
return 201, response_data
102 changes: 2 additions & 100 deletions bc_obps/reporting/api/reports.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Tuple, Dict, Union
from typing import Literal, Tuple
from django.http import HttpRequest
from registration.decorators import handle_http_errors
from reporting.constants import EMISSIONS_REPORT_TAGS
Expand All @@ -10,8 +10,7 @@
from reporting.schema.report_operation import ReportOperationOut, ReportOperationIn
from reporting.schema.reporting_year import ReportingYearOut
from .router import router
from ..models import FacilityReport, ReportingYear
from ..schema.facility_report import FacilityReportOut, FacilityReportIn
from ..models import ReportingYear


@router.post(
Expand Down Expand Up @@ -66,100 +65,3 @@ def save_report(
@handle_http_errors()
def get_reporting_year(request: HttpRequest) -> Tuple[Literal[200], ReportingYear]:
return 200, ReportingYearService.get_current_reporting_year()


@router.get(
"/report-version/{version_id}/facility-report/{facility_id}",
response={200: FacilityReportOut, 404: Message, 400: Message, 500: Message},
tags=EMISSIONS_REPORT_TAGS,
description="""Takes `version_id` (primary key of the ReportVersion model) and `facility_id` to return a single matching `facility_report` object.
Includes the associated activity IDs if found; otherwise, returns an error message if not found or in case of other issues.""",
)
@handle_http_errors()
def get_facility_report_by_version_and_id(
request: HttpRequest, version_id: int, facility_id: int
) -> Union[
Tuple[Literal[200], FacilityReportOut],
Tuple[Literal[404], Dict[str, str]],
Tuple[Literal[400], Dict[str, str]],
Tuple[Literal[500], Dict[str, str]],
]:
try:
facility_report = ReportService.get_facility_report_by_version_and_id(version_id, facility_id)

if facility_report:
activity_ids = ReportService.get_activity_ids_for_facility(facility_id) or []
response_data = FacilityReportOut(
id=facility_report.id,
report_version_id=facility_report.report_version.id,
facility_name=facility_report.facility_name,
facility_type=facility_report.facility_type,
facility_bcghgid=facility_report.facility_bcghgid,
activities=activity_ids,
products=[],
)
return 200, response_data

else:
return 404, {"message": "Facility not found"}

except ValueError as ve:
return 400, {"message": f"Invalid input: {str(ve)}"}

except Exception as e:
return 500, {"message": "An unexpected error occurred", "details": str(e)}


@router.post(
"/report-version/{version_id}/facility-report/{facility_id}",
response={201: FacilityReportOut, custom_codes_4xx: Message},
tags=EMISSIONS_REPORT_TAGS,
description="""Updates the report facility details by version_id and facility_id. The request body should include
fields to be updated, such as facility name, type, BC GHG ID, activities, and products. Returns the updated report
facility object or an error message if the update fails.""",
)
@handle_http_errors()
def save_facility_report(
request: HttpRequest, version_id: int, facility_id: int, payload: FacilityReportIn
) -> Union[
Tuple[Literal[201], FacilityReportOut],
Tuple[Literal[400], Dict[str, str]],
Tuple[Literal[404], Dict[str, str]],
Tuple[Literal[500], Dict[str, str]],
]:
"""
Save or update a report facility and its related activities.

Args:
request (HttpRequest): The HTTP request object.
version_id (int): The ID of the report version.
facility_id (int): The ID of the facility.
payload (FacilityReportIn): The input data for the report facility.

Returns:
Tuple: HTTP status code and the response data or an error message.
"""
try:
# Fetch the existing facility report or create a new one
facility_report = ReportService.save_facility_report(version_id, facility_id, payload)

# Prepare the response data
response_data = FacilityReportOut(
id=facility_id,
report_version_id=facility_report.report_version.id,
facility_name=facility_report.facility_name,
facility_type=facility_report.facility_type,
facility_bcghgid=facility_report.facility_bcghgid,
activities=list(facility_report.activities.values_list('id', flat=True)),
products=list(facility_report.products.values_list('id', flat=True)) or [],
)
return 201, response_data

except ValueError as ve:
return 400, {"message": f"Invalid input: {str(ve)}"}

except FacilityReport.DoesNotExist:
return 404, {"message": "Report facility not found"}

except Exception as e:
return 500, {"message": "An unexpected error occurred", "details": str(e)}
50 changes: 50 additions & 0 deletions bc_obps/reporting/tests/api/test_facility_report_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import json
import pytest
from django.test import Client
from reporting.models import FacilityReport
from registration.tests.utils.bakers import user_baker
from model_bakery import baker

client = Client()


@pytest.mark.django_db
class TestFacilityReportEndpoints:
# GET
def test_error_if_no_facility_report_exists(self):
response = client.get('/api/reporting/report-version/9999/facility-report/00000000-0000-0000-0000-000000000000')
assert response.status_code == 404
assert response.json()["message"] == "Not Found"

def test_error_if_no_invalid_facility_id(self):
response = client.get('/api/reporting/report-version/9999/facility-report/1')
assert response.status_code == 422
assert "Input should be a valid UUID" in response.json()["detail"][0]["msg"]

def test_returns_correct_data(self):
facility_report = baker.make_recipe('reporting.tests.utils.facility_report')
response = client.get(
f'/api/reporting/report-version/{facility_report.report_version_id}/facility-report/{facility_report.facility_id}'
)
assert response.status_code == 200
assert response.json()['facility_name'] == facility_report.facility_name

# POST
def test_saves_facility_data(self):
facility_report = baker.make_recipe('reporting.tests.utils.facility_report')
request_data = {
"facility_name": "CHANGED",
"facility_type": "Single Facility Operation",
"facility_bcghgid": "abc12345",
"activities": ["1", "2", "3"],
"products": [],
}
user = user_baker()
client.post(
f'/api/reporting/report-version/{facility_report.report_version_id}/facility-report/{facility_report.facility_id}',
data=json.dumps(request_data),
HTTP_AUTHORIZATION=json.dumps({"user_guid": f'{user.user_guid}'}),
content_type="application/json",
)
assert FacilityReport.objects.get(pk=facility_report.id).facility_name == "CHANGED"
assert FacilityReport.objects.get(pk=facility_report.id).activities.count() == 3
4 changes: 2 additions & 2 deletions bc_obps/reporting/tests/models/test_report_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from reporting.models.report_activity import ReportActivity
from reporting.tests.utils.bakers import report_version_baker
from reporting.tests.utils.constants import REPORT_DATA_MODELS_COMMON_FIELDS
from reporting.tests.utils.report_data_bakers import facility_report_baker
from model_bakery import baker


class ReportActivityModelTest(BaseTestCase):
Expand All @@ -17,7 +17,7 @@ def setUpTestData(cls):
json_data="{'test': 1}",
activity_base_schema=ActivityJsonSchema.objects.first(),
activity=ActivityJsonSchema.objects.first().activity,
facility_report=facility_report_baker(report_version=report_version),
facility_report=baker.make_recipe('reporting.tests.utils.facility_report'),
)
cls.field_data = [
*TIMESTAMP_COMMON_FIELDS,
Expand Down
20 changes: 20 additions & 0 deletions bc_obps/reporting/tests/utils/baker_recipes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from reporting.models.reporting_year import ReportingYear
from reporting.models.report import Report
from reporting.models.report_version import ReportVersion
from reporting.models.facility_report import FacilityReport
from registration.tests.utils.baker_recipes import operation, operator, facility
from model_bakery.recipe import Recipe, foreign_key


reporting_year = Recipe(ReportingYear)

report = Recipe(
Report,
operator=foreign_key(operator),
operation=foreign_key(operation),
reporting_year=foreign_key(reporting_year),
)

report_version = Recipe(ReportVersion, report=foreign_key(report))

facility_report = Recipe(FacilityReport, report_version=foreign_key(report_version), facility=foreign_key(facility))
49 changes: 49 additions & 0 deletions bc_obps/service/facility_report_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from uuid import UUID
from django.db import transaction
from typing import List

from registration.models import Activity
from reporting.models.facility_report import FacilityReport
from reporting.schema.facility_report import FacilityReportIn


class FacilityReportService:
@classmethod
def get_facility_report_by_version_and_id(cls, report_version_id: int, facility_id: UUID) -> FacilityReport:
return FacilityReport.objects.get(report_version_id=report_version_id, facility_id=facility_id)

@classmethod
def get_activity_ids_for_facility(cls, version_id: int, facility_id: UUID) -> List[int]:
facility_report = FacilityReport.objects.get(report_version_id=version_id, facility_id=facility_id)
return list(facility_report.activities.values_list('id', flat=True))

@classmethod
@transaction.atomic()
def save_facility_report(
cls, report_version_id: int, facility_id: UUID, data: FacilityReportIn, user_guid: UUID
) -> FacilityReport:
"""
Update a facility report and its related activities.

Args:
report_version_id (int): The ID of the report version.
facility_id (int): The ID of the facility.
data (FacilityReportIn): The input data for the facility report.

Returns:
FacilityReport: The updated or created FacilityReport instance.
"""
# Update FacilityReport instance
facility_report = FacilityReport.objects.get(report_version_id=report_version_id, facility_id=facility_id)
facility_report.facility_name = data.facility_name.strip()
facility_report.facility_type = data.facility_type.strip()

# Update ManyToMany fields (activities)
if data.activities:
facility_report.activities.set(Activity.objects.filter(id__in=data.activities))

# Save the updated FacilityReport instance
facility_report.save()
facility_report.set_create_or_update(user_guid)

return facility_report
Loading