mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
481 lines
20 KiB
Python
481 lines
20 KiB
Python
import pandas as pd
|
|
import pytest
|
|
from unittest.mock import Mock
|
|
from backend.Property import Property
|
|
from etl.epc_clean.EpcClean import EpcClean
|
|
from etl.epc.Record import EPCRecord
|
|
|
|
# Define some test data
|
|
mock_epc_response = {
|
|
"rows": [
|
|
{
|
|
"tenure": "rental (social)",
|
|
"lmk-key": 1,
|
|
"uprn": 1,
|
|
"number-habitable-rooms": 5,
|
|
"property-type": "House",
|
|
"built-form": "Detached",
|
|
"inspection-date": "2023-06-01",
|
|
'lodgement-datetime': '2023-06-01 20:29:01',
|
|
"some-other-key": "some-value",
|
|
"roof-description": "pitched, no insulation",
|
|
"walls-description": "Walls Description",
|
|
"windows-description": "Windows Description",
|
|
"mainheat-description": "Main Heating Description",
|
|
"hotwater-description": "Hot Water Description",
|
|
"transaction-type": "rental",
|
|
"lighting-description": "Good Lighting Efficiency",
|
|
"energy-consumption-current": "50",
|
|
"co2-emissions-current": "123",
|
|
"mechanical-ventilation": "natural",
|
|
'photo-supply': 0,
|
|
"solar-water-heating-flag": "N",
|
|
"wind-turbine-count": 0,
|
|
"extension-count": 0,
|
|
"heat-loss-corridor": "no corridor",
|
|
"unheated-corridor-length": 0,
|
|
"mains-gas-flag": "Y",
|
|
"floor-height": 2.5,
|
|
"total-floor-area": 100,
|
|
"construction-age-band": "England and Wales: 1967-1975",
|
|
"floor-description": "Floor Description",
|
|
"floor-level": "Ground"
|
|
},
|
|
{
|
|
"lmk-key": 2,
|
|
"uprn": 2,
|
|
"number-habitable-rooms": 5,
|
|
"property-type": "House",
|
|
"built-form": "Detached",
|
|
"inspection-date": "2023-05-01",
|
|
'lodgement-datetime': '2023-05-01 20:29:01',
|
|
"some-other-key": "some-other-value",
|
|
"roof-description": "Roof Description",
|
|
"walls-description": "Walls Description",
|
|
"windows-description": "Windows Description",
|
|
"mainheat-description": "Main Heating Description",
|
|
"hotwater-description": "Hot Water Description",
|
|
"transaction-type": "rental",
|
|
"lighting-description": "Good Lighting Efficiency",
|
|
"energy-consumption-current": "50",
|
|
"co2-emissions-current": "123",
|
|
"mechanical-ventilation": "natural",
|
|
'photo-supply': 0,
|
|
"solar-water-heating-flag": "N",
|
|
"wind-turbine-count": 0,
|
|
"extension-count": 0,
|
|
"heat-loss-corridor": "no corridor",
|
|
"unheated-corridor-length": 0,
|
|
"mains-gas-flag": "Y",
|
|
"floor-height": 2.5,
|
|
"total-floor-area": 100,
|
|
"construction-age-band": "England and Wales: 1967-1975",
|
|
"floor-description": "Floor Description",
|
|
"floor-level": "Ground"
|
|
}
|
|
]
|
|
}
|
|
|
|
mock_epc_response_dupe = {
|
|
'rows': [
|
|
{
|
|
"lmk-key": 1,
|
|
"uprn": 1,
|
|
"number-habitable-rooms": 5,
|
|
"property-type": "House",
|
|
'inspection-date': '2023-06-01',
|
|
'lodgement-datetime': '2023-06-01 20:29:01',
|
|
'some-other-key': 'some-value', 'roof-description': 'Roof Description',
|
|
'walls-description': 'Walls Description', 'windows-description': 'Windows Description',
|
|
'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description',
|
|
"transaction-type": "rental",
|
|
"lighting-description": "Good Lighting Efficiency",
|
|
"energy-consumption-current": "50",
|
|
"co2-emissions-current": "123",
|
|
"mechanical-ventilation": "natural",
|
|
'photo-supply': 0,
|
|
"solar-water-heating-flag": "N",
|
|
"wind-turbine-count": 0,
|
|
"extension-count": 0,
|
|
"heat-loss-corridor": "no corridor",
|
|
"unheated-corridor-length": 0,
|
|
"mains-gas-flag": "Y",
|
|
"floor-height": 2.5,
|
|
"total-floor-area": 100,
|
|
"construction-age-band": "England and Wales: 1967-1975",
|
|
"floor-description": "Floor Description",
|
|
"floor-level": "Ground"
|
|
},
|
|
{
|
|
"lmk-key": 2,
|
|
"uprn": 2,
|
|
"number-habitable-rooms": 5,
|
|
"property-type": "House",
|
|
'inspection-date': '2023-05-01',
|
|
'lodgement-datetime': '2023-05-01 20:29:01',
|
|
'some-other-key': 'some-other-value',
|
|
'roof-description': 'Roof Description', 'walls-description': 'Walls Description',
|
|
'windows-description': 'Windows Description', 'mainheat-description': 'Main Heating Description',
|
|
'hotwater-description': 'Hot Water Description',
|
|
"transaction-type": "rental",
|
|
"lighting-description": "Good Lighting Efficiency",
|
|
"energy-consumption-current": "50",
|
|
"co2-emissions-current": "123",
|
|
"mechanical-ventilation": "natural",
|
|
'photo-supply': 0,
|
|
"solar-water-heating-flag": "N",
|
|
"wind-turbine-count": 0,
|
|
"extension-count": 0,
|
|
"heat-loss-corridor": "no corridor",
|
|
"unheated-corridor-length": 0,
|
|
"mains-gas-flag": "Y",
|
|
"floor-height": 2.5,
|
|
"total-floor-area": 100,
|
|
"construction-age-band": "England and Wales: 1967-1975",
|
|
"floor-description": "Floor Description",
|
|
"floor-level": "Ground"
|
|
},
|
|
{
|
|
"lmk-key": 3,
|
|
"uprn": 3,
|
|
"number-habitable-rooms": 5,
|
|
"property-type": "House",
|
|
'inspection-date': '2023-06-01',
|
|
'lodgement-datetime': '2023-06-01 20:29:01',
|
|
'some-other-key': 'duplicate-date',
|
|
'roof-description': 'Roof Description',
|
|
'walls-description': 'Walls Description', 'windows-description': 'Windows Description',
|
|
'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description',
|
|
"transaction-type": "rental",
|
|
"lighting-description": "Good Lighting Efficiency",
|
|
"energy-consumption-current": "50",
|
|
"co2-emissions-current": "123",
|
|
"mechanical-ventilation": "natural",
|
|
'photo-supply': 0,
|
|
"solar-water-heating-flag": "N",
|
|
"wind-turbine-count": 0,
|
|
"extension-count": 0,
|
|
"heat-loss-corridor": "no corridor",
|
|
"unheated-corridor-length": 0,
|
|
"mains-gas-flag": "Y",
|
|
"floor-height": 2.5,
|
|
"total-floor-area": 100,
|
|
"construction-age-band": "England and Wales: 1967-1975",
|
|
"floor-description": "Floor Description",
|
|
"floor-level": "Ground"
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
class TestProperty:
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_photo_supply_lookup(self):
|
|
return pd.DataFrame(
|
|
[
|
|
dict(
|
|
tenure="rental (social)",
|
|
built_form="Detached",
|
|
property_type="House",
|
|
construction_age_band="England and Wales: 1967-1975",
|
|
is_flat=False,
|
|
is_pitched=True,
|
|
is_roof_room=False,
|
|
floor_area_decile=2,
|
|
photo_supply_median=40
|
|
)
|
|
]
|
|
)
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_floor_area_decile_thresholds(self):
|
|
return pd.DataFrame(
|
|
{"floor_area_decile_thresholds": [0, 10, 30, 50]}
|
|
)
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def property_instance(self, mock_cleaner):
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = mock_epc_response["rows"][0]
|
|
|
|
property_instance = Property(id=1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
property_instance.number_of_floors = 2
|
|
property_instance.number_of_rooms = 5
|
|
property_instance.floor_area = 100
|
|
property_instance.floor_height = 2.5
|
|
return property_instance
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def property_instance_dupe_data(self):
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = mock_epc_response_dupe["rows"][0]
|
|
property_instance_dupe_data = Property(id=2, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
return property_instance_dupe_data
|
|
|
|
# @pytest.fixture
|
|
# def mock_epc_client(self):
|
|
# mock_epc_client = Mock(spec=EpcClient(auth_token="mocked_auth_token"))
|
|
# mock_epc_client.domestic.search.return_value = mock_epc_response.copy()
|
|
# mock_epc_client.auth_token = "mocked_auth_token"
|
|
# return mock_epc_client
|
|
#
|
|
# @pytest.fixture
|
|
# def mock_epc_client_dupe_data(self):
|
|
# mock_epc_client_dupe_data = Mock(spec=EpcClient(auth_token="mocked_auth_token"))
|
|
# mock_epc_client_dupe_data.domestic.search.return_value = mock_epc_response_dupe.copy()
|
|
# mock_epc_client_dupe_data.auth_token = "mocked_auth_token"
|
|
# return mock_epc_client_dupe_data
|
|
|
|
@pytest.fixture
|
|
def mock_cleaner(self):
|
|
lighting_averages = [
|
|
{'lighting-description': 'good lighting efficiency', 'low-energy-lighting': 99.26666666666667},
|
|
{'lighting-description': 'excellent lighting efficiency', 'low-energy-lighting': 100.0},
|
|
{'lighting-description': 'below average lighting efficiency', 'low-energy-lighting': 0.0}
|
|
]
|
|
|
|
cleaner_spec = EpcClean(
|
|
data=[
|
|
{"roof-description": "Roof Description"},
|
|
{"walls-description": "Walls Description"},
|
|
{"windows-description": "Windows Description"},
|
|
{"mainheat-description": "Main Heating Description"},
|
|
{"hotwater-description": "Hot Water Description"},
|
|
{"lighting-description": "Good Lighting Efficiency"},
|
|
{"low-energy-lighting": 0},
|
|
{"floor-description": "Floor Description"}
|
|
],
|
|
lighting_averages=lighting_averages
|
|
)
|
|
|
|
mock_cleaner = Mock(spec=cleaner_spec)
|
|
|
|
walls_data = {
|
|
"original_description": "Walls Description",
|
|
"is_cavity_wall": True,
|
|
"is_solid_brick": False,
|
|
"is_timber_frame": False,
|
|
"is_system_built": False,
|
|
"is_park_home": False,
|
|
"is_cob": False,
|
|
"is_sandstone_or_limestone": False,
|
|
"is_granite_or_whinstone": False,
|
|
}
|
|
|
|
mock_cleaner.cleaned = {
|
|
"roof-description": [
|
|
{"original_description": "Roof Description"},
|
|
{"original_description": "pitched, no insulation", "is_pitched": True, "is_flat": False,
|
|
"is_roof_room": False}
|
|
],
|
|
"walls-description": [walls_data],
|
|
"windows-description": [{"original_description": "Windows Description"}],
|
|
"mainheat-description": [{"original_description": "Main Heating Description"}],
|
|
"hotwater-description": [{"original_description": "Hot Water Description"}],
|
|
"lighting-description": [{"original_description": "Good Lighting Efficiency"}],
|
|
"floor-description": [
|
|
{"original_description": "Floor Description", "is_suspended": True, "another_property_below": False}]
|
|
}
|
|
return mock_cleaner
|
|
|
|
def test_init(self):
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = {"uprn": 1}
|
|
inst1 = Property(0, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
|
|
assert inst1.data is not None
|
|
|
|
inst2 = Property(3, "AB12CD", "Test Address", epc_record=epc_record)
|
|
assert inst2.id == 3
|
|
|
|
inst3 = Property(4, "AB12CD", "Test Address", epc_record=epc_record)
|
|
assert inst3.data == {"uprn": 1}
|
|
|
|
def test_get_components(
|
|
self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds
|
|
):
|
|
property_instance.get_components(
|
|
mock_cleaner.cleaned,
|
|
photo_supply_lookup=mock_photo_supply_lookup,
|
|
floor_area_decile_thresholds=mock_floor_area_decile_thresholds
|
|
)
|
|
|
|
# Verify that the components are set correctly
|
|
assert property_instance.roof == {
|
|
'original_description': 'pitched, no insulation', 'is_pitched': True,
|
|
'is_flat': False, 'is_roof_room': False
|
|
}
|
|
|
|
assert property_instance.walls == {
|
|
"original_description": "Walls Description",
|
|
"is_cavity_wall": True,
|
|
"is_solid_brick": False,
|
|
"is_timber_frame": False,
|
|
"is_system_built": False,
|
|
"is_park_home": False,
|
|
"is_cob": False,
|
|
"is_sandstone_or_limestone": False,
|
|
"is_granite_or_whinstone": False,
|
|
}
|
|
assert property_instance.windows == {"original_description": "Windows Description"}
|
|
assert property_instance.main_heating == {"original_description": "Main Heating Description"}
|
|
assert property_instance.hotwater == {"original_description": "Hot Water Description"}
|
|
|
|
assert property_instance.wall_type == "cavity"
|
|
|
|
def test_get_components_without_cleaned_data(self, property_instance, mock_cleaner):
|
|
# Modify the mock EpcClean to not have cleaned data
|
|
mock_cleaner.cleaned = {}
|
|
|
|
# Verify that ValueError is raised when EpcClean doesn't contain cleaned data
|
|
with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"):
|
|
property_instance.get_components(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame())
|
|
|
|
def test_get_components_no_attributes(
|
|
self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds
|
|
):
|
|
# Modify the mock cleaner to have no attributes for a specific description
|
|
mock_cleaner.cleaned = {
|
|
"roof-description": []
|
|
}
|
|
property_instance.data["roof-description"] = "Pitched, no insulation"
|
|
property_instance.walls = {
|
|
"original_description": "Walls Description",
|
|
"is_cavity_wall": True,
|
|
"is_solid_brick": False,
|
|
"is_timber_frame": False,
|
|
"is_system_built": False,
|
|
"is_park_home": False,
|
|
"is_cob": False,
|
|
"is_sandstone_or_limestone": False,
|
|
"is_granite_or_whinstone": False,
|
|
}
|
|
|
|
property_instance.floor = {
|
|
"is_suspended": False,
|
|
"another_property_below": False,
|
|
"is_solid": True
|
|
}
|
|
|
|
# Assert backup cleaning has been applied
|
|
property_instance.get_components(
|
|
mock_cleaner.cleaned, mock_photo_supply_lookup, mock_floor_area_decile_thresholds
|
|
)
|
|
|
|
assert property_instance.roof["clean_description"] == "Pitched, no insulation"
|
|
assert property_instance.roof["is_pitched"]
|
|
|
|
def test_get_components_multiple_attributes(
|
|
self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds
|
|
):
|
|
# This shouldn't happen - it would mean a cleaning error
|
|
property_instance.data["roof-description"] = "Roof Description"
|
|
cleaned = {
|
|
"roof-description": [
|
|
{"original_description": "Roof Description"},
|
|
{"original_description": "Roof Description"}
|
|
]
|
|
}
|
|
|
|
# Verify that ValueError is raised when multiple attributes are found
|
|
with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"):
|
|
property_instance.get_components(cleaned, mock_photo_supply_lookup, mock_floor_area_decile_thresholds)
|
|
|
|
def test_set_spatial(self):
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = mock_epc_response["rows"][0]
|
|
prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
|
|
spatial1 = pd.DataFrame([{
|
|
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
|
'conservation_status': True, 'is_listed_building': False, 'is_heritage_building': True
|
|
}])
|
|
|
|
prop.set_spatial(spatial1)
|
|
|
|
assert prop.in_conservation_area
|
|
assert not prop.is_listed
|
|
assert prop.is_heritage
|
|
assert prop.restricted_measures
|
|
|
|
prop2 = Property(1, "AB12CD", "Test Address", epc_record=epc_record)
|
|
|
|
spatial2 = pd.DataFrame([{
|
|
'X_COORDINATE': 411143.0, 'Y_COORDINATE': 281701.0, 'LATITUDE': 52.4331896, 'LONGITUDE': -1.8375238,
|
|
'conservation_status': None, 'is_listed_building': False, 'is_heritage_building': False
|
|
}])
|
|
|
|
prop2.set_spatial(spatial2)
|
|
|
|
assert prop2.in_conservation_area is None
|
|
assert not prop2.is_listed
|
|
assert not prop2.is_heritage
|
|
assert not prop2.restricted_measures
|
|
|
|
def test_set_floor_level(self):
|
|
# In this case, we have a flat which looks looks it's on the first floor, but it's actually on the ground
|
|
# floor, so we should set floor_level to 0
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = {'floor-level': '01', 'property-type': 'Flat'}
|
|
prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
prop.floor = {
|
|
'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation',
|
|
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': True,
|
|
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True,
|
|
'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
|
'floor_insulation_thickness': 'none'
|
|
}
|
|
|
|
prop.set_floor_level()
|
|
|
|
assert prop.floor_level == 0
|
|
|
|
# This property is labelled as being on the ground floor but actually has another property below
|
|
# so we set floor level to 1
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = {'floor-level': 'Ground', 'property-type': 'Flat'}
|
|
prop2 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
prop2.floor = {
|
|
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
|
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False,
|
|
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False,
|
|
'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
|
'floor_insulation_thickness': 'none'
|
|
}
|
|
|
|
prop2.set_floor_level()
|
|
|
|
assert prop2.floor_level == 1
|
|
|
|
# this property is correctly labelled as being on the 2nd floor
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = {'floor-level': '02', 'property-type': 'Flat'}
|
|
prop3 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
prop3.floor = {
|
|
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
|
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False,
|
|
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False,
|
|
'another_property_below': True, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
|
'floor_insulation_thickness': 'none'
|
|
}
|
|
|
|
prop3.set_floor_level()
|
|
|
|
assert prop3.floor_level == 2
|
|
|
|
# Example of a house
|
|
epc_record = EPCRecord()
|
|
epc_record.prepared_epc = {'floor-level': '', 'property-type': 'House'}
|
|
prop4 = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record)
|
|
prop4.floor = {
|
|
'original_description': '(Another dwelling below)', 'clean_description': 'Solid, no insulation',
|
|
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_assumed': False,
|
|
'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False,
|
|
'another_property_below': False, 'insulation_thickness': 'none', 'floor_thermal_transmittance': None,
|
|
'floor_insulation_thickness': 'none'
|
|
}
|
|
|
|
prop4.set_floor_level()
|
|
|
|
assert prop4.floor_level is None
|