diff --git a/hub_api/city_commands.py b/hub_api/city_commands.py deleted file mode 100644 index 6dfb522..0000000 --- a/hub_api/city_commands.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -HeatPump Service -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2023 Project Author Koa Wells kekoa.wells@concordia.ca -""" - -import json - -from flask import Response, request -from flask_restful import Resource -from hub.persistence.models import UserRoles - -from hub_api.helpers.auth import role_required -from hub_api.helpers.session_helper import refresh_session - -# Admin user commands -""" -Save class -""" -class Save(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value]) - def put(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - - -""" -Update class -""" -class Update(Resource): - def __init__(self): - print() - - @role_required([UserRoles.Admin.value]) - def put(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers -""" -Delete class -""" -class Delete(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value]) - def delete(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - -# Standard user commands -""" -ListCities class -""" -class List(Resource): - def __init__(self): - pass - - @role_required([UserRoles.Admin.value]) - def put(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - -""" -SearchCity class -""" -class Search(Resource): - def __init__(self): - pass - - def get(self): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - diff --git a/hub_api/city_info.py b/hub_api/city_info.py index e0f3f3d..2249931 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -15,71 +15,147 @@ from pathlib import Path import os from hub_api.config import Config +headers = {'Content-Type': 'application/json'} + class CityInfo(Resource, Config): def __init__(self): super().__init__() - @role_required([UserRoles.Admin.value]) def get(self, city_id): + """ + API call to search database for city by city_id + :return: json, status code, headers + """ city = self.get_city(city_id) + if city is not None: + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404, headers=headers) - # TODO: this is only for dompark project and need to be removed in future versions. - floor_area = 0 - wall_construction = 'unknown' - floor_construction = 'unknown' - roof_construction = 'unknown' - window_type = 'unknown' - building_dic = {} - for building in city.buildings: + @role_required([UserRoles.Admin.value]) + def put(self, city_id): + """ + API call to update city in database with new city_file by city_id + :return: json, status code, headers + """ + allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} + try: + city_file = request.files['city_file'] + ext = city_file.filename.rsplit('.', 1)[1].lower() - usages = [] # This is only valid for dompark project as all the building have the same usage - if building.lower_corner[2] == 0: - floor_area += building.floor_area - for internal_zone in building.internal_zones: - for usage_zone in internal_zone.usage_zones: - usages.append({'percentage': usage_zone.percentage, 'usage': usage_zone.usage}) - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.parent_surface.type == 'Ground': - floor_construction = thermal_boundary.construction_name - elif thermal_boundary.parent_surface.type == 'Wall': - wall_construction = thermal_boundary.construction_name - for thermal_opening in thermal_boundary.thermal_openings: - if thermal_opening.construction_name is not None: - window_type = thermal_opening.construction_name - break - else: - roof_construction = thermal_boundary.construction_name - name = building.human_readable_name - year_of_construction = str(building.year_of_construction) - building_dic = { - 'name': str(name), - 'floor_area': str(floor_area), - 'year_of_construction': str(year_of_construction), - 'usages': usages, - 'wall_construction': wall_construction, - 'floor_construction': floor_construction, - 'roof_construction': roof_construction, - 'window_type': window_type, - 'default_archetype': 'industry ASHRAE_2004:4A non_standard_dompark' - } + if ext in allowed_ext: + city_file_type = ext + if ext == 'gml': + city_file_type = 'citygml' + elif ext == '3dm': + city_file_type = 'rhino' + + file_path = ( + Path(__file__).parent.parent / 'data/uploaded_city/{}'.format(city_file.filename)).resolve() + city_file.save(file_path) + city = GeometryFactory(city_file_type, file_path).city + + saved_city = self.import_db_factory.update_city(city_id, city) + if os.path.exists(file_path): + os.remove(file_path) + if saved_city is None: + saved_city = self.export_db_factory.get_city(city_id) + return Response(headers=headers, response=json.dumps({ + 'id': saved_city.id, 'name': saved_city.name, 'srs_name': saved_city.srs_name, + 'time_zone': saved_city.time_zone, 'version': saved_city.city_version, + 'country': saved_city.country_code, + 'lat': saved_city.latitude, 'lon': saved_city.longitude, + 'lower_corner': saved_city.lower_corner, + 'upper_corner': saved_city.upper_corner, 'created': saved_city.created, + 'updated': saved_city.updated, + 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, + 'role': saved_city.user.role.value} + }, default=str), status=201) + return Response(response=json.dumps(saved_city), status=200) + else: + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while updating city'}), status=400) + + @role_required([UserRoles.Admin.value]) + def delete(self, city_id): + """ + API call to delete city from database by city_id + :return: json, status code, headers + """ + try: + return Response(headers=headers, response=json.dumps( + self.import_db_factory.delete_city(city_id=city_id)), status=201) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while deleting city'}), status=400) + +class CitySearch(Resource, Config): + """ + CitySearch class for searching for cities via an API call + """ + def __init__(self): + super().__init__() + + def get(self, search_term, query): + """ + API call to search city depending on search_term and specified query + :return: json, status code, headers + """ + if search_term == "city_name": + city = self.get_city_by_name(query) + + if city is not None: + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'City not found'}), status=404, headers=headers) + + elif search_term == "user_id": + cities = self.get_city_by_user(query) + + if cities is not None: + for city in cities: + #TODO - iterate through cities packaging them into a list and return the packaged JSON + return Response(response=json.dumps({ + 'id': city.id, 'name': city.name, 'srs_name': city.srs_name, + 'time_zone': city.time_zone, 'version': city.city_version, 'country': city.country_code, + 'lat': city.latitude, 'lon': city.longitude, 'lower_corner': city.lower_corner, + 'upper_corner': city.upper_corner, 'created': city.created, 'updated': city.updated, + 'user': {'id': city.user.id, 'name': city.user.name, 'email': city.user.email, + 'role': city.user.role.value} + }, default=str), status=200, headers=headers) + return Response(response=json.dumps({'err_msg': 'No cities found by user'}), status=404, headers=headers) + + elif search_term == "list_all": + #TODO - implement API call to query all define cities in the database + pass - buildings = [building_dic] - response = {'city_name': city.name, - 'climate_reference_city': str(city.climate_reference_city), - 'buildings': buildings - } - return Response(json.dumps(response), status=200) class City(Resource, Config): - def __init__(self): - super().__init__() @role_required([UserRoles.Admin.value]) def post(self): + """ + API call to create city in database with newcity_file by city_id + :return: json, status code, headers + """ allowed_ext = {'gml', '3dm', 'xml', 'obj', 'rhino'} try: city_file = request.files['city_file'] @@ -108,9 +184,10 @@ class City(Resource, Config): 'user': {'id': saved_city.user.id, 'name': saved_city.user.name, 'email': saved_city.user.email, 'role': saved_city.user.role.value} }, default=str), status=201) - return Response(response=json.dumps(saved_city), status=200) + return Response(response=json.dumps(saved_city), status=200, headers=headers) else: - return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400) + return Response(response=json.dumps({'err_msg': 'Unknown city file type'}), status=400, headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), status=400) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating city'}), + status=400, headers=headers) \ No newline at end of file diff --git a/hub_api/config.py b/hub_api/config.py index 48a8758..efa2985 100644 --- a/hub_api/config.py +++ b/hub_api/config.py @@ -3,28 +3,41 @@ Config SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2023 Project Peter Yefi peteryefi@gmail.com """ +import os from hub.exports.db_factory import DBFactory as CityExportFactory from hub.imports.db_factory import DBFactory -import os -import pickle +from hub.imports.user_factory import UserFactory +from hub.exports.user_factory import UserFactory as ExUserFactory class Config: def __init__(self): - self.export_db_factory = CityExportFactory(db_name='hub_prod', app_env='PROD', + db_name = None + app_env = None + if os.getenv("FLASK_DEBUG") == 'production': + db_name = 'hub_prod' + app_env = 'PROD' + elif os.getenv("FLASK_DEBUG") == 'testing': + db_name = 'hub_test' + app_env = 'TEST' + + self.export_db_factory = CityExportFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) - self.import_db_factory = DBFactory(db_name='hub_prod', app_env='PROD', + self.import_db_factory = DBFactory(db_name=db_name, app_env=app_env, dotenv_path="{}/.env".format(os.path.expanduser('~'))) + self.user_factory = UserFactory(db_name=db_name, app_env=app_env, + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + self.ex_user_factory = ExUserFactory(db_name=db_name, app_env=app_env, + dotenv_path="{}/.env".format(os.path.expanduser('~'))) def get_city(self, city_id): - city_obj = self.export_db_factory.get_city(city_id) - city = pickle.loads(city_obj.city) - for building in city.buildings: - building.heated = True - building.cooled = True - building.attic_heated = 0 - building.basement_heated = 0 - for surface in building.surfaces: - surface.swr = 0.2 - return city + return self.export_db_factory.get_city(city_id) + + def get_city_by_name(self, city_name): + return self.export_db_factory.get_city_by_name(city_name) + + def get_city_by_user(self, user_id): + return self.export_db_factory.get_city_by_user(user_id) + + diff --git a/hub_api/docs/openapi-specs.yml b/hub_api/docs/openapi-specs.yml index 6555d14..021ec9b 100644 --- a/hub_api/docs/openapi-specs.yml +++ b/hub_api/docs/openapi-specs.yml @@ -3,7 +3,7 @@ info: description: NextGen Cities Institute API termsOfService: http://swagger.io/terms/ contact: - email: peteryefi@gmail.com + email: nextgen-cities@gmail.com version: 1.4 externalDocs: description: Find out more about Swagger @@ -100,13 +100,69 @@ paths: $ref: '#/components/schemas/ApiResponse' security: - BearerAuth: [] - /v1.4/heat-pump/{city_id}: - post: + /v1.4/city/{city_id}: + get: tags: - - heatpump - summary: Create a heat pump simulation - operationId: createHeatpump - description: heatpump simulation with existing catalog data + - city + summary: Get a city + operationId: getCity + description: Retrieve a city with a given city ID + parameters: + - in: header + name: appId + schema: + type: string + required: true + description: the ID of the application access this API + - in: path + name: city_id + schema: + type: integer + required: true + description: Numeric ID of the city to get + responses: + '200': + description: City retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/City' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '404': + description: City not found + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '403': + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + put: + tags: + - city + summary: Update a city + operationId: updateCity + description: Create a new city with a file upload parameters: - in: header name: appId @@ -119,21 +175,30 @@ paths: schema: type: integer required: true - description: Numeric ID of the city to get + description: Numeric ID of the city to update requestBody: content: - application/json: + multipart/form-data: schema: type: object - $ref: '#/components/schemas/HeatPump' + properties: + city_file: + type: string + format: binary required: true responses: '201': - description: Heatpump simulation created successfully + description: City updated successfully content: application/json: schema: - $ref: '#/components/schemas/HeatPumpRes' + $ref: '#/components/schemas/City' + '200': + description: City not updated + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' '400': description: Bad Request content: @@ -141,7 +206,7 @@ paths: schema: $ref: '#/components/schemas/ApiResponse' '404': - description: Not found + description: City not found content: application/json: schema: @@ -166,13 +231,12 @@ paths: $ref: '#/components/schemas/ApiResponse' security: - BearerAuth: [ ] - /v1.4/city/{city_id}: - get: + delete: tags: - city - summary: Get a city - operationId: getCity - description: Retrieve a city with a given city ID + summary: Delete a city + operationId: deleteCity + description: Delete city with specified city_id parameters: - in: header name: appId @@ -185,14 +249,20 @@ paths: schema: type: integer required: true - description: Numeric ID of the city to get + description: Numeric ID of the city to delete responses: - '200': - description: City created successfully + '201': + description: City deleted successfully content: application/json: schema: $ref: '#/components/schemas/City' + '200': + description: City not deleted + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' '400': description: Bad Request content: @@ -364,233 +434,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ApiResponse' - /v1.4/energy-demand/{city_id}: - get: - tags: - - energy-demand - summary: Get energy demand - description: Retrieve energy demand data - operationId: getEnergyDemand - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - responses: - '200': - description: Successfully retrieved energy demand data - content: - application/json: - schema: - $ref: '#/components/schemas/EnergyDemand' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction/{city_id}: - put: - tags: - - construction - summary: updates a city with building construction - description: updates a city with building construction - operationId: updateConstruction - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - - in: path - name: city_id - schema: - type: integer - required: true - description: Numeric ID of the city to get - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/Construction' - application/xml: - schema: - $ref: '#/components/schemas/Construction' - responses: - '201': - description: city updated with building construction successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Construction' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction-catalog/entries: - post: - tags: - - construction-catalog - summary: creates building construction catalog entries - description: create the construction catalog entries - operationId: catalogEntries - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - application/xml: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - responses: - '201': - description: Construction catalog entries created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntries' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] - /v1.4/construction-catalog/entry: - post: - tags: - - construction-catalog - summary: creates building construction catalog - description: create the construction catalog of a building - operationId: catalogEntry - parameters: - - in: header - name: appId - schema: - type: string - required: true - description: the Id of the application access this API - requestBody: - description: creates building construction catalog - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - application/xml: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - responses: - '201': - description: Construction catalog created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ConstructionCatalogEntry' - '400': - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '403': - description: Forbidden - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '401': - description: Unauthorized - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - security: - - BearerAuth: [ ] components: schemas: Uptime: @@ -648,211 +491,6 @@ components: updated: type: string example: 2023-01-15 18:40:54.64877 - HeatPump: - type: object - properties: - StartYear: - type: integer - format: int64 - example: 10 - EndYear: - type: integer - format: int64 - example: 10 - MaximumHPEnergyInput: - type: float - example: 9.8382 - HoursOfStorageAtMaxDemand: - type: integer - format: int64 - example: 9 - BuildingSuppTemp: - type: integer - format: int64 - example: 40 - TemperatureDifference: - type: float - example: 9.8382 - FuelLHV: - type: float - example: 9.8382 - FuelPrice: - type: float - example: 9.8382 - FuelEF: - type: integer - format: int64 - example: 40 - FuelDensity: - type: float - example: 9.8382 - HPSupTemp: - type: float - example: 9.8382 - HeatPumpType: - type: string - example: Water to Water HP - enum: - - Water to Water HP - - Air Source HP - HeatPumpModel: - type: string - example: Water to Water HP - enum: - - ClimateMaster 156 kW - - ClimateMaster 256 kW - - ClimateMaster 335 kW - - 012 - - 015 - - 018 - - 023 - - 030 - - 033 - - 037 - - 044 - - 047 - - 057 - - 070 - - 087 - - 097 - - 102 - - 120 - - 130 - - 140 - SimType: - type: int - example: 1 - format: int64 - EnergyDemand: - type: array - items: - type: float - example: [ 610.610, 754.746, 288.338 ] - required: - - SimType - - EnergyDemand - - HeatPumpModel - - HeatPumpType - - FuelEF - - FuelDensity - - FuelPrice - - FuelLHV - - BuildingSuppTemp - - TemperatureDifference - - HoursOfStorageAtMaxDemand - - MaximumHPEnergyInput - - StartYear - - EndYear - ConstructionCatalogEntry: - type: object - properties: - name: - type: string - example: 189.1-2009 Nonres 4B Ext Wall Mass - required: - - name - ConstructionCatalogEntries: - type: object - properties: - category: - type: string - example: archetypes - required: - - category - Construction: - type: object - properties: - building_names: - type: array - items: - type: string - example: ["Dompark"] - extra_loses_due_to_thermal_bridges: - type: float - example: 0.15 - indirect_heated_ratio: - type: float - example: 0.15 - infiltration_rate_for_ventilation_system_off: - type: float - example: 0.5 - infiltration_rate_for_ventilation_system_on: - type: float - example: 0.0 - constructions: - type: array - items: - type: object - properties: - name: - type: string - example: 189.1-2009 Res 4B Ext Wall Steel-Framed - type: - type: string - example: Wall - layers: - type: array - items: - type: object - $ref: '#components/schemas/Layers' - required: - - building_names - - extra_loses_due_to_thermal_bridges - - indirect_heated_ratio - - infiltration_rate_for_ventilation_system_off - - infiltration_rate_for_ventilation_system_on - - constructions - Layers: - type: object - properties: - name: - type: string - example: Layer 1 - thickness: - type: float - example: 0.0 - material: - type: object - $ref: '#/components/schemas/Material' - Material: - type: object - properties: - id: - type: integer - format: int64 - example: 12 - name: - type: string - example: MAT-SHEAT - solar_absorptance: - type: float - example: 0.7 - thermal_absorptance: - type: float - example: 0.9 - visible_absorptance: - type: float - example: 0.7 - no_mass: - type: string - example: "True" - thermal_resistance: - type: float - example: 0.36256 - conductivity: - type: string - density: - type: string - specific_heat: - type: string - required: - - id - - name - - solar_absorptance - - thermal_absorptance - - visible_absorptance - - no_mass - - thermal_resistance User: type: object properties: @@ -880,48 +518,6 @@ components: - email - password - role - HeatPumpRes: - type: object - properties: - hourly_electricity_demand: - type: array - items: - type: object - daily_electricity_demand: - type: array - items: - type: object - monthly_electricity_demand: - type: array - items: - type: object - daily_fossil_consumption: - type: array - items: - type: object - monthly_fossil_consumption: - type: array - items: - type: object - EnergyDemand: - type: object - properties: - heating_demand: - type: array - items: - type: object - cooling_demand: - type: array - items: - type: object - lighting_demand: - type: array - items: - type: object - appliances_demand: - type: array - items: - type: object Login: type: object properties: @@ -951,9 +547,14 @@ components: format: int32 message: type: string + Uptime: + type: object + properties: + uptime: + type: string requestBodies: User: - description: Pet object that needs to be added to the store + description: User object that is to be created content: application/json: schema: diff --git a/hub_api/geometry.py b/hub_api/geometry.py deleted file mode 100644 index 9fdd68d..0000000 --- a/hub_api/geometry.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Geometry -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -from flask import make_response, send_file -from flask_restful import Resource -from pathlib import Path -from hub_api.helpers.auth import role_required -from persistence.models import UserRoles - - -class Geometry(Resource): - def __init__(self): - data_path = (Path(__file__).parent.parent / 'data').resolve() - self._gtlf_path = (Path(data_path / 'DomparkBuilding.gltf')).resolve() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def get(self): - response = make_response(send_file(self._gtlf_path, - as_attachment=True, - mimetype='model/gltf+json, model/gltf-binary')) - return response diff --git a/hub_api/greenery.py b/hub_api/greenery.py deleted file mode 100644 index 96dc84e..0000000 --- a/hub_api/greenery.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Greenery -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -import json - -from flask import Response, request -from flask_restful import Resource -from hub_api.config import Config -from city_model_structure.greenery.vegetation import Vegetation -from city_model_structure.greenery.soil import Soil -from city_model_structure.greenery.plant import Plant -import helpers.constants as cte -from persistence.models import UserRoles -from hub_api.helpers.auth import role_required - - -class Greenery(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value]) - def put(self, city_id): - - city = self.get_city(city_id) - try: - greenery_percentage = request.json['greenery_percentage'] - if greenery_percentage == 0: - response = {'result': 'succeed'} - return Response(json.dumps(response), status=200) - - building_names = request.json['building_names'] - vegetation_requested = request.json['vegetation'] - vegetation_name = vegetation_requested['vegetation_name'] - soil_thickness = vegetation_requested['soil_thickness'] - soil_name = vegetation_requested['soil_name'] - roughness = vegetation_requested['soil_roughness'] - dry_conductivity = vegetation_requested['dry_soil_conductivity'] - dry_density = vegetation_requested['dry_soil_density'] - dry_specific_heat = vegetation_requested['dry_soil_specific_heat'] - thermal_absorptance = vegetation_requested['soil_thermal_absorptance'] - solar_absorptance = vegetation_requested['soil_solar_absorptance'] - visible_absorptance = vegetation_requested['soil_visible_absorptance'] - saturation_volumetric_moisture_content = vegetation_requested['soil_saturation_volumetric_moisture_content'] - residual_volumetric_moisture_content = vegetation_requested['soil_residual_volumetric_moisture_content'] - soil = Soil(soil_name, roughness, dry_conductivity, dry_density, dry_specific_heat, thermal_absorptance, - solar_absorptance, visible_absorptance, saturation_volumetric_moisture_content, - residual_volumetric_moisture_content) - soil.initial_volumetric_moisture_content = '0.1' - plant_percentages = vegetation_requested['plant_percentages'] - plants = [] - for plant_percentage in plant_percentages: - plant_name = plant_percentage['plant_name'] - height = plant_percentage['plant_height'] - leaf_area_index = plant_percentage['plant_leaf_area_index'] - leaf_reflectivity = plant_percentage['plant_leaf_reflectivity'] - leaf_emissivity = plant_percentage['plant_leaf_emissivity'] - minimal_stomatal_resistance = plant_percentage['plant_minimal_stomatal_resistance'] - co2_sequestration = plant_percentage['plant_co2_sequestration'] - grows_on_soils = plant_percentage['plant_grows_on'] - plant = Plant(plant_name, height, leaf_area_index, leaf_reflectivity, leaf_emissivity, - minimal_stomatal_resistance, co2_sequestration, grows_on_soils) - plant.percentage = plant_percentage['plant_percentage'] - plants.append(plant) - vegetation = Vegetation(vegetation_name, soil, soil_thickness, plants) - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - for thermal_zone in internal_zone.thermal_zones: - for thermal_boundary in thermal_zone.thermal_boundaries: - if thermal_boundary.type == cte.ROOF: - thermal_boundary.vegetation = vegetation - - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - - return Response(json.dumps(response)) diff --git a/hub_api/heat_pump.py b/hub_api/heat_pump.py deleted file mode 100644 index 54aa214..0000000 --- a/hub_api/heat_pump.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -HeatPump Service -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com -""" -import json - -from flask import request, Response -from hub_api.config import Config -from flask_restful import Resource -from hub_api.helpers.auth import role_required -from utils import HeatPumpSimulator -from utils import validate_hp_model -from persistence.models import UserRoles -from utils import expand_energy_demand -from hub_logger import logger - - -class HeatPump(Config, Resource): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def post(self, city_id): - payload = request.get_json() - city = self.get_city(city_id) - if validate_hp_model(payload['HeatPumpType'], payload['HeatPumpModel']): - # expand energy demand values - expand_energy_demand(payload['EnergyDemand']) - try: - # Run simulation and return output file here - hp_simulator = HeatPumpSimulator(city, payload) - results = hp_simulator.run_hp_simulation() - return Response(json.dumps(results), status=200) - except Exception as err: - logger.error(err) - return Response(json.dumps({'error_message': 'Sorry an error occurred while running HP Simulation'})) - else: - return Response(json.dumps({'error_message': 'Wrong heat pump type/model combination'}), status=400) diff --git a/hub_api/uptime.py b/hub_api/uptime.py index 9fabd35..a10c67b 100644 --- a/hub_api/uptime.py +++ b/hub_api/uptime.py @@ -12,6 +12,8 @@ from flask_restful import Resource import hub_api.helpers.session_helper as sh +headers = {'Content-Type': 'application/json'} + class Uptime(Resource): def __init__(self): pass @@ -19,4 +21,4 @@ class Uptime(Resource): @staticmethod def get(): uptime = {"uptime": f"{datetime.datetime.now() - sh.begin_time}"} - return Response(json.dumps(uptime)) + return Response(response=json.dumps(uptime), headers=headers) diff --git a/hub_api/usage.py b/hub_api/usage.py deleted file mode 100644 index 8d127f7..0000000 --- a/hub_api/usage.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Usage -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -Code contributors: Peter Yefi peteryefi@gmail.com -""" - -import json -from flask import Response, request -from flask_restful import Resource - -from city_model_structure.attributes.schedule import Schedule -from city_model_structure.building_demand.appliances import Appliances -from city_model_structure.building_demand.lighting import Lighting -from city_model_structure.building_demand.occupancy import Occupancy -from city_model_structure.building_demand.thermal_control import ThermalControl -from city_model_structure.building_demand.usage_zone import UsageZone -import helpers.constants as cte -from hub_api.helpers.auth import role_required -from hub_api.config import Config -from persistence.models import UserRoles - - -class Usage(Resource, Config): - def __init__(self): - super().__init__() - - @role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value]) - def put(self, city_id): - - city = self.get_city(city_id) - catalog = request.json['usage_catalog'] - - usage_name = None - try: - building_names = request.json['building_names'] - usages = request.json['usages'] - seconds_in_hour = cte.MINUTES_TO_SECONDS * cte.HOUR_TO_MINUTES - for building_name in building_names: - for building in city.buildings: - if building.human_readable_name != building_name: - continue - for internal_zone in building.internal_zones: - internal_zone.usage_zones = [] - for usage in usages: - usage_entry = catalog.get_entry(usage['name']) - occupancy_schedules = [] - for schedule in usage_entry.occupancy.schedules: - occupancy_schedule = Schedule() - occupancy_schedule.type = schedule.type - occupancy_schedule.values = schedule.values - occupancy_schedule.data_type = schedule.data_type - occupancy_schedule.time_step = schedule.time_step - occupancy_schedule.time_range = schedule.time_range - occupancy_schedule.day_types = schedule.day_types - occupancy_schedules.append(occupancy_schedule) - occupancy = Occupancy() - occupancy.occupancy_density = usage_entry.occupancy.occupancy_density - occupancy.sensible_convective_internal_gain = usage_entry.occupancy.sensible_convective_internal_gain - occupancy.sensible_radiative_internal_gain = usage_entry.occupancy.sensible_radiative_internal_gain - occupancy.latent_internal_gain = usage_entry.occupancy.latent_internal_gain - occupancy.occupancy_schedules = occupancy_schedules - - lighting_schedules = [] - for schedule in usage_entry.lighting.schedules: - lighting_schedule = Schedule() - lighting_schedule.type = schedule.type - lighting_schedule.values = schedule.values - lighting_schedule.data_type = schedule.data_type - lighting_schedule.time_step = schedule.time_step - lighting_schedule.time_range = schedule.time_range - lighting_schedule.day_types = schedule.day_types - lighting_schedules.append(lighting_schedule) - lighting = Lighting() - lighting.density = usage_entry.lighting.density - lighting.convective_fraction = usage_entry.lighting.convective_fraction - lighting.radiative_fraction = usage_entry.lighting.radiative_fraction - lighting.latent_fraction = usage_entry.lighting.latent_fraction - lighting.schedules = lighting_schedules - appliances_schedules = [] - for schedule in usage_entry.appliances.schedules: - appliances_schedule = Schedule() - appliances_schedule.type = schedule.type - appliances_schedule.values = schedule.values - appliances_schedule.data_type = schedule.data_type - appliances_schedule.time_step = schedule.time_step - appliances_schedule.time_range = schedule.time_range - appliances_schedule.day_types = schedule.day_types - appliances_schedules.append(appliances_schedule) - appliances = Appliances() - appliances.density = usage_entry.appliances.density - appliances.convective_fraction = usage_entry.appliances.convective_fraction - appliances.radiative_fraction = usage_entry.appliances.radiative_fraction - appliances.latent_fraction = usage_entry.appliances.latent_fraction - appliances.schedules = appliances_schedules - hvac_schedules = [] - for schedule in usage_entry.thermal_control.hvac_availability_schedules: - hvac_schedule = Schedule() - hvac_schedule.type = schedule.type - hvac_schedule.values = schedule.values - hvac_schedule.data_type = schedule.data_type - hvac_schedule.time_step = schedule.time_step - hvac_schedule.time_range = schedule.time_range - hvac_schedule.day_types = schedule.day_types - hvac_schedules.append(hvac_schedule) - heating_schedules = [] - for schedule in usage_entry.thermal_control.heating_set_point_schedules: - heating_schedule = Schedule() - heating_schedule.type = schedule.type - heating_schedule.values = schedule.values - heating_schedule.data_type = schedule.data_type - heating_schedule.time_step = schedule.time_step - heating_schedule.time_range = schedule.time_range - heating_schedule.day_types = schedule.day_types - heating_schedules.append(heating_schedule) - cooling_schedules = [] - for schedule in usage_entry.thermal_control.cooling_set_point_schedules: - cooling_schedule = Schedule() - cooling_schedule.type = schedule.type - cooling_schedule.values = schedule.values - cooling_schedule.data_type = schedule.data_type - cooling_schedule.time_step = schedule.time_step - cooling_schedule.time_range = schedule.time_range - cooling_schedule.day_types = schedule.day_types - cooling_schedules.append(cooling_schedule) - thermal_control = ThermalControl() - thermal_control.mean_heating_set_point = usage_entry.thermal_control.mean_heating_set_point - thermal_control.heating_set_back = usage_entry.thermal_control.heating_set_back - thermal_control.mean_cooling_set_point = usage_entry.thermal_control.mean_cooling_set_point - thermal_control.hvac_availability_schedules = hvac_schedules - thermal_control.heating_set_point_schedules = heating_schedules - thermal_control.cooling_set_point_schedules = cooling_schedules - usage_zone = UsageZone() - usage_zone.usage = usage_entry.usage - usage_zone.percentage = usage['percentage'] - usage_zone.hours_day = usage_entry.hours_day - usage_zone.days_year = usage_entry.days_year - usage_zone.mechanical_air_change = usage_entry.mechanical_air_change - if usage_entry.mechanical_air_change is None: - usage_zone.mechanical_air_change = ((usage_entry.ventilation_rate * internal_zone.area) / - internal_zone.volume) * seconds_in_hour - usage_zone.occupancy = occupancy - usage_zone.lighting = lighting - usage_zone.appliances = appliances - usage_zone.thermal_control = thermal_control - internal_zone.usage_zones.append(usage_zone) - response = {'result': 'succeed'} - except KeyError as ex: - response = {'error': f'Mandatory parameter {ex} is missing'} - return Response(json.dumps(response), status=400) - except IndexError: - response = {'error': f'Name "{usage_name}" unknown'} - return Response(json.dumps(response), status=400) - return Response(json.dumps(response)) diff --git a/hub_api/usage_catalog.py b/hub_api/usage_catalog.py deleted file mode 100644 index 026df07..0000000 --- a/hub_api/usage_catalog.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Construction catalog -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca -""" - -import json - -from flask import request, Response -from flask_restful import Resource - -from hub_api.helpers.session_helper import refresh_session - - -class ToJson: - @staticmethod - def usage_to_json(usage): - dictionary = { - 'usage': usage.usage, - 'hours_day': usage.hours_day, - 'days_year': usage.days_year, - 'mechanical_air_change': usage.mechanical_air_change if usage.mechanical_air_change is not None else '', - 'ventilation_rate': usage.ventilation_rate if usage.ventilation_rate is not None else '', - 'occupancy': ToJson.occupancy_to_json(usage.occupancy), - 'lighting': ToJson.lighting_to_json(usage.lighting), - 'appliances': ToJson.appliances_to_json(usage.appliances), - 'thermal_control': ToJson.thermal_control_to_json(usage.thermal_control) - } - return dictionary - - @staticmethod - def occupancy_to_json(occupancy): - dictionary = { - 'occupancy_density': occupancy.occupancy_density, - 'sensible_convective_internal_gain': occupancy.sensible_convective_internal_gain, - 'sensible_radiative_internal_gain': occupancy.sensible_radiative_internal_gain, - 'latent_internal_gain': occupancy.latent_internal_gain, - 'schedules': [] - } - for schedule in occupancy.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def lighting_to_json(lighting): - dictionary = { - 'density': lighting.density, - 'convective_fraction': lighting.convective_fraction, - 'radiative_fraction': lighting.radiative_fraction, - 'latent_fraction': lighting.latent_fraction, - 'schedules': [] - } - for schedule in lighting.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def appliances_to_json(appliances): - dictionary = { - 'density': appliances.density, - 'convective_fraction': appliances.convective_fraction, - 'radiative_fraction': appliances.radiative_fraction, - 'latent_fraction': appliances.latent_fraction, - 'schedules': [] - } - for schedule in appliances.schedules: - dictionary['schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def thermal_control_to_json(thermal_control): - dictionary = { - 'mean_heating_set_point': thermal_control.mean_heating_set_point, - 'heating_set_back': thermal_control.heating_set_back, - 'mean_cooling_set_point': thermal_control.mean_cooling_set_point, - 'hvac_availability_schedules': [], - 'heating_set_point_schedules': [], - 'cooling_set_point_schedules': [], - } - for schedule in thermal_control.hvac_availability_schedules: - dictionary['hvac_availability_schedules'].append(ToJson.schedule_to_json(schedule)) - for schedule in thermal_control.heating_set_point_schedules: - dictionary['heating_set_point_schedules'].append(ToJson.schedule_to_json(schedule)) - for schedule in thermal_control.cooling_set_point_schedules: - dictionary['cooling_set_point_schedules'].append(ToJson.schedule_to_json(schedule)) - return dictionary - - @staticmethod - def schedule_to_json(schedule): - schedule_dictionary = { - 'type': schedule.type, - 'data_type': schedule.data_type, - 'time_step': schedule.time_step, - 'time_range': schedule.time_range, - 'day_types': schedule.day_types, - 'values': schedule.values, - } - return schedule_dictionary - - -class UsageCatalogEntry(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - - name = None - if request.data == b'' or request.json['name'] is None: - response = {'error': 'Mandatory parameter "name" is missing'} - return Response(json.dumps(response), headers=headers, status=400) - try: - name = request.json['name'] - entry = catalog.get_entry(name) - output = {'usages': [ToJson.usage_to_json(entry)]} - return Response(json.dumps(output), headers=headers) - except IndexError: - response = {'error': f'Name "{name}" unknown'} - return Response(json.dumps(response), headers=headers, status=400) - - -class UsageCatalogEntries(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - output = {'usages': []} - content = catalog.entries() - for usage in content.usages: - output['usages'].append(ToJson.usage_to_json(usage)) - return Response(json.dumps(output), headers=headers) - - -class UsageCatalogNames(Resource): - def __init__(self): - pass - - @staticmethod - def post(): - session = refresh_session(request) - if session is None: - return Response(json.dumps({'error': 'invalid session'}), status=401) - headers = session.headers - catalog = session.usage_catalog - return Response(json.dumps(catalog.names()), headers=headers) diff --git a/hub_api/user.py b/hub_api/user.py index d7e0936..3f5d01f 100644 --- a/hub_api/user.py +++ b/hub_api/user.py @@ -6,18 +6,17 @@ Copyright © 2023 Project Author Peter Yefi peteryefi@gmail.com import json from flask import Response, request from flask_restful import Resource -from hub.imports.user_factory import UserFactory -from hub.exports.user_factory import UserFactory as ExUserFactory -import os from hub.hub_logger import logger from hub_api.helpers.auth import generate_auth_token, role_required from hub.persistence.models import UserRoles +from hub_api.config import Config + +headers = {'Content-Type': 'application/json'} -class User(Resource): +class User(Resource, Config): def __init__(self): - self.user_factory = UserFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + super().__init__() @role_required([UserRoles.Admin.value]) def post(self): @@ -26,12 +25,14 @@ class User(Resource): user = self.user_factory.create_user(name=payload["name"], email=payload["email"], password=payload["password"], role=payload["role"]) if type(user) is dict: - return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps(user), status=400, headers=headers) return Response(response=json.dumps({'user': {'id': user.id, 'name': user.name, 'email': user.email, - 'password': user.password, 'role': user.role.value}}), status=201) + 'password': user.password, 'role': user.role.value}}), status=201, + headers=headers) except Exception as err: logger.error(err) - return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400, + headers=headers) @role_required([UserRoles.Admin.value]) def put(self): @@ -40,25 +41,24 @@ class User(Resource): res = self.user_factory.update_user(user_id=payload['id'], name=payload['name'], password=payload['password'], role=payload['role'], email=payload['email']) if res: - return Response(response=json.dumps(res), status=400) - return Response(response=json.dumps({'success': 'user updated successfully'}), status=200) + return Response(response=json.dumps(res), status=400, headers=headers) + return Response(response=json.dumps({'success': 'user updated successfully'}), status=200, headers=headers) except Exception as err: logger.error(err) return Response(response=json.dumps({'err_msg': 'Sorry, an error occurred while updating user'}), - status=400) + status=400, headers=headers) -class UserLogin(Resource): +class UserLogin(Resource, Config): def __init__(self): - self.user_factory = ExUserFactory(db_name='hub_prod', app_env='PROD', - dotenv_path="{}/.env".format(os.path.expanduser('~'))) + super().__init__() def post(self): try: payload = request.get_json() - user = self.user_factory.login_user(email=payload["email"], password=payload["password"]) + user = self.ex_user_factory.login_user(email=payload["email"], password=payload["password"]) if type(user) is dict: - return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps(user), status=400, headers=headers) user = user[0] user_dict = { 'user': { @@ -70,7 +70,7 @@ class UserLogin(Resource): } } user_dict['token'] = generate_auth_token(user_dict) - return Response(response=json.dumps(user_dict), status=200) + return Response(response=json.dumps(user_dict), status=200, headers=headers) except Exception as err: logger.error(err) return Response(response=json.dumps({'err_msg': 'An error occurred while authenticating user'}), status=400) diff --git a/requirements.txt b/requirements.txt index 521ea31..5cfd237 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,4 +24,5 @@ pyecore==0.12.2 jwt==1.3.1 flagger==3.1.0 flasgger -cerc-hub \ No newline at end of file +cerc-hub +psycopg2 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/base_test.py b/tests/base_test.py new file mode 100644 index 0000000..627aef1 --- /dev/null +++ b/tests/base_test.py @@ -0,0 +1,58 @@ +import os +from unittest import TestCase +from bootstrap import app +from hub.persistence.base_repo import BaseRepo +from sqlalchemy import create_engine +from hub.persistence.models import City +from hub.persistence.models import User +from sqlalchemy.exc import ProgrammingError + + +# function to ensure tests run in order shown in fle +def arrange(): + order = {} + + def ordered(f): + order[f.__name__] = len(order) + return f + + def compare(a, b): + return [1, -1][order[a] < order[b]] + + return ordered, compare + + +class BaseTest(TestCase): + """ + Tests for payment resource + """ + + @classmethod + def setUpClass(cls): + os.environ['FLASK_DEBUG'] = 'testing' + cls.app = app + cls.client = cls.app.test_client() + + # Create test database + repo = BaseRepo(db_name='test_db', app_env='TEST', dotenv_path="{}/.env".format(os.path.expanduser('~'))) + eng = create_engine(f'postgresql://{repo.config.get_db_user()}@/{repo.config.get_db_user()}') + + try: + # delete test database if it exists + conn = eng.connect() + conn.execute('commit') + conn.execute('DROP DATABASE test_db') + conn.close() + except ProgrammingError as err: + print(f'Database does not exist. Nothing to delete') + + cnn = eng.connect() + cnn.execute('commit') + cnn.execute("CREATE DATABASE test_db") + cnn.close() + User.__table__.create(bind=repo.engine, checkfirst=True) + City.__table__.create(bind=repo.engine, checkfirst=True) + + @classmethod + def tearDownClass(cls) -> None: + pass diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..0993b24 --- /dev/null +++ b/tests/test_user.py @@ -0,0 +1,124 @@ +import json +from .base_test import BaseTest, arrange +import unittest +from hub_api.helpers.auth import generate_auth_token + +ordered, compare = arrange() +unittest.defaultTestLoader.sortTestMethodsUsing = compare + + +class UserTest(BaseTest): + """ + Tests for User API endpoints + """ + + @classmethod + def setUpClass(cls): + # Call setUp from parent + super().setUpClass() + cls.user_dict = {"user": { + "name": "Test User", + "email": "testuser@gmail.com", + "password": "TestUser@12345", + "role": "Admin", + }} + cls.token = generate_auth_token(cls.user_dict) + + @ordered + def test_create_user_by_non_admin(self): + # When + res = self.client.post('/v1.4/user', data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('Invalid payload', res.json['messages']) + self.assertEqual(400, res.status_code) + + + @ordered + def test_create_user_by_admin(self): + # When + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + user = res.json + self.assertEqual(201, res.status_code) + self.assertEqual(type(user['user']), dict) + self.assertEqual(user['user']['email'], self.user_dict['user']['email']) + self.assertEqual(user['user']['role'], self.user_dict['user']['role']) + + @ordered + def test_create_user_with_existing_email(self): + # When + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('user with testuser@gmail.com email already exists', res.json['message']) + self.assertEqual(400, res.status_code) + + @ordered + def test_create_user_with_weak_password(self): + # When + self.user_dict['user']['password'] = '1234' + self.user_dict['user']['email'] = 'new@gmail.com' + res = self.client.post('/v1.4/user', + headers={ + 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + str(self.token) + }, + data=json.dumps(self.user_dict['user'])) + # Then + self.assertEqual('Sorry an error occurred while creating user', res.json['err_msg']) + self.assertEqual(400, res.status_code) + + + @ordered + def test_login_user_with_wrong_credential(self): + # When + res = self.client.post('/v1.4/user/login', + headers={ + 'Content-Type': 'application/json' + }, + data=json.dumps({'email': 'wrong@gmail.com', 'password': 'wrong'})) + + # Then + message = res.json + self.assertEqual('user not found', message['message']) + self.assertEqual(400, res.status_code) + + @ordered + def test_login_user_with_correct_credential(self): + # When + self.user_dict['user']['password'] = 'TestUser@12345' + self.user_dict['user']['email'] = 'testuser@gmail.com' + login_data = { + "email": self.user_dict['user']['email'], + "password": self.user_dict['user']['password'] + } + res = self.client.post('/v1.4/user/login', headers={'Content-Type': 'application/json'}, + data=json.dumps(login_data)) + + # Then + user = res.json + self.assertEqual(user['user']['email'], self.user_dict['user']['email']) + self.assertEqual(user['user']['role'], self.user_dict['user']['role']) + self.assertIsNotNone(user['token']) + self.assertEqual(200, res.status_code) + + @classmethod + def tearDownClass(cls) -> None: + # Call tearDown from parent + super().tearDownClass() + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(unittest.TestLoader( + ).loadTestsFromTestCase(UserTest)) + + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite())