From 24e22fa56a176b3d324db0445c19c3b1232b201a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 1 Dec 2023 14:50:46 +0000 Subject: [PATCH] Added pushing of spatial data to backend --- backend/Property.py | 23 ++++++--- .../app/db/functions/property_functions.py | 51 +++++++++++++++++-- backend/app/db/models/portfolio.py | 13 +++++ backend/app/plan/router.py | 5 +- 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index ddfb9445..a9d7645d 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -45,7 +45,7 @@ class Property(Definitions): windows = None lighting = None - coordinates = None + spatial = None def __init__(self, id, postcode, address1, epc_client=None, data=None): self.id = id @@ -129,13 +129,6 @@ class Property(Definitions): else: self.uprn = int(self.data["uprn"]) - def set_coordinates(self, coordinates): - """ - This method sets the coordinates of the property, given the open uprn data - :param coordinates: dictionary - """ - self.coordinates = {key.lower(): value for key, value in coordinates.items()} - def set_energy(self): """ Extracts and formats data about the home's energy and co2 consumption @@ -364,6 +357,9 @@ class Property(Definitions): def set_spatial(self, spatial: pd.DataFrame): """ Sets whether the property is in a conservation area given the output of the ConservationAreaClient + + Will store a dictionary, spatial, which is used to populate the property spatial table in the database + :param spatial: Dataframe, containing the spatial data for the property """ self.in_conservation_area = spatial["conservation_status"].values[0] @@ -373,6 +369,17 @@ class Property(Definitions): if self.in_conservation_area is True | self.is_listed is True | self.is_heritage is True: self.restricted_measures = True + spatial_dict = spatial.to_dict("records")[0] + self.spatial = { + "x_coordinate": spatial_dict["X_COORDINATE"], + "y_coordinate": spatial_dict["Y_COORDINATE"], + "latitude": spatial_dict["LATITUDE"], + "longitude": spatial_dict["LONGITUDE"], + "conservation_status": spatial_dict["conservation_status"], + "is_listed_building": spatial_dict["is_listed_building"], + "is_heritage_building": spatial_dict["is_heritage_building"], + } + def set_year_built(self): """ Estimates when the property was built based on as much available data as possible. diff --git a/backend/app/db/functions/property_functions.py b/backend/app/db/functions/property_functions.py index ecad3ab7..93dc0c49 100644 --- a/backend/app/db/functions/property_functions.py +++ b/backend/app/db/functions/property_functions.py @@ -3,13 +3,15 @@ ### import datetime import pytz +from sqlalchemy.orm import Session from backend.app.db.models.portfolio import ( - PropertyModel, PropertyCreationStatus, PortfolioStatus, PropertyTargetsModel, PropertyDetailsEpcModel + PropertyModel, PropertyCreationStatus, PortfolioStatus, PropertyTargetsModel, PropertyDetailsEpcModel, + PropertyDetailsSpatial ) from sqlalchemy.orm.exc import NoResultFound -def create_property(session, portfolio_id: int, address: str, postcode: str) -> (int, bool): +def create_property(session: Session, portfolio_id: int, address: str, postcode: str) -> (int, bool): """ This function will create a record for the property in the database if it does not exist. If it does exist, it will just update the updated_at field. @@ -55,7 +57,9 @@ def create_property(session, portfolio_id: int, address: str, postcode: str) -> return new_property.id, True -def create_property_targets(session, property_id: int, portfolio_id: int, epc_target=None, heat_demand_target=None): +def create_property_targets( + session: Session, property_id: int, portfolio_id: int, epc_target=None, heat_demand_target=None +): """ This function will create a record for the property targets in the database if it does not exist. :param session: The database session @@ -78,7 +82,9 @@ def create_property_targets(session, property_id: int, portfolio_id: int, epc_ta return True -def update_property_data(session, property_id: int, portfolio_id: int, property_data: dict): +def update_property_data( + session: Session, property_id: int, portfolio_id: int, property_data: dict +): now = datetime.datetime.now(pytz.utc) try: @@ -103,7 +109,9 @@ def update_property_data(session, property_id: int, portfolio_id: int, property_ return True -def create_property_details_epc(session, property_details_epc: dict): +def create_property_details_epc( + session: Session, property_details_epc: dict +): """ This function will create or update a record for the property details EPC in the database. :param session: The database session @@ -128,3 +136,36 @@ def create_property_details_epc(session, property_details_epc: dict): session.flush() return True + + +def update_or_create_property_spatial_details(session: Session, uprn: int, property_details_spatial: dict): + """ + Update an existing property details record or create a new one based on the UPRN. + + :param session: The SQLAlchemy session for database interaction. + :param uprn: The unique property reference number (UPRN) of the property. + :param property_details_spatial: A dictionary containing the spatial property details to store or update. + :return: True if the operation is successful, otherwise raises an exception. + """ + + try: + # Attempt to fetch the existing property details + existing_property_details = session.query(PropertyDetailsSpatial).filter_by( + uprn=uprn + ).one() + + # Update the fields with the data in property_details + for key, value in property_details_spatial.items(): + setattr(existing_property_details, key, value) + + # Merge the updated property details back into the session and flush + session.merge(existing_property_details) + session.flush() + + except NoResultFound: + # Create a new record if not found + new_property_details = PropertyDetailsSpatial(uprn=uprn, **property_details_spatial) + session.add(new_property_details) + session.flush() + + return True diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index ab047477..6f865381 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -155,6 +155,19 @@ class PropertyDetailsEpcModel(Base): adjusted_energy_consumption = Column(Float) +class PropertyDetailsSpatial(Base): + __tablename__ = "property_details_spatial" + id = Column(Integer, primary_key=True, autoincrement=True) + uprn = Column(Integer, nullable=False) + x_coordinate = Column(Float) + y_coordinate = Column(Float) + latitude = Column(Float) + longitude = Column(Float) + conservation_status = Column(Boolean) + is_listed_building = Column(Boolean) + is_heritage_building = Column(Boolean) + + class PropertyDetailsMeter(Base): __tablename__ = 'property_details_meter' id = Column(Integer, primary_key=True, autoincrement=True) diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 1b84ed72..2277a6b4 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -13,7 +13,8 @@ from backend.app.db.connection import db_engine from backend.app.db.functions.materials_functions import get_materials from backend.app.db.functions.portfolio_functions import aggregate_portfolio_recommendations from backend.app.db.functions.property_functions import ( - create_property, create_property_details_epc, create_property_targets, update_property_data + create_property, create_property_details_epc, create_property_targets, update_property_data, + update_or_create_property_spatial_details ) from backend.app.db.functions.recommendations_functions import ( create_plan, create_plan_recommendations, upload_recommendations @@ -507,6 +508,8 @@ async def trigger_plan(body: PlanTriggerRequest): ) create_property_details_epc(session, property_details_epc) + update_or_create_property_spatial_details(session, p.uprn, p.spatial) + # TODO: TEMP if p.data["uprn"] == "": print("Get rid of me!")