From fba1a60c3abf68cce0c770d2feaf6cfafee3880e Mon Sep 17 00:00:00 2001 From: Browning Date: Fri, 11 Aug 2023 15:34:46 +0100 Subject: [PATCH] Finalised unit tests for HTML report and create TemplateHTML class --- .../gtfs/report/report_utils.py | 128 +++++++++++++++ .../gtfs/report/utils.py | 125 --------------- src/transport_performance/gtfs/validation.py | 66 +++----- tests/data/gtfs/report/gtfs_report/styles.css | 148 ++++++++++++++++++ tests/gtfs/report/test_report_utils.py | 108 +++++++++++++ tests/gtfs/report/test_utils.py | 36 ----- 6 files changed, 409 insertions(+), 202 deletions(-) create mode 100644 src/transport_performance/gtfs/report/report_utils.py delete mode 100644 src/transport_performance/gtfs/report/utils.py create mode 100644 tests/data/gtfs/report/gtfs_report/styles.css create mode 100644 tests/gtfs/report/test_report_utils.py delete mode 100644 tests/gtfs/report/test_utils.py diff --git a/src/transport_performance/gtfs/report/report_utils.py b/src/transport_performance/gtfs/report/report_utils.py new file mode 100644 index 00000000..0a952c87 --- /dev/null +++ b/src/transport_performance/gtfs/report/report_utils.py @@ -0,0 +1,128 @@ +"""Utils to assist in the creation of a HTML report for GTFS.""" +from typing import Union +import pathlib +import shutil +import os + +from transport_performance.utils.defence import ( + _bool_defence, + _string_defence, + _is_path_like, +) + + +class TemplateHTML: + """A class for inserting HTML string into a docstring.""" + + def __init__(self, path: Union[str, pathlib.Path]) -> None: + """Initialise the TemplateHTML object. + + Parameters + ---------- + path : Union[str, pathlib.Path] + The file path of the html template + + Returns + ------- + None + + """ + _is_path_like(path, "path") + with open(path, "r", encoding="utf8") as f: + self.template = f.read() + return None + + def insert( + self, placeholder: str, value: str, replace_multiple: bool = False + ) -> None: + """Insert values into the html template. + + Parameters + ---------- + placeholder : str + The placeholder name in the template. + This is a string. In the template it + should be surrounded by sqsuare brackets. + value : str + The value to place in the placeholder + location. + replace_multiple : bool, optional + Whether or not to replace multiple + placeholders that share the same + placeholder value, + by default False + + Returns + ------- + None + + """ + _string_defence(placeholder, "placeholder") + _string_defence(value, "value") + _bool_defence(replace_multiple, "replace_multiple") + occurences = len(self.template.split(f"[{placeholder}]")) - 1 + if occurences > 1 and not replace_multiple: + raise ValueError( + "You have selected not to replace multiple" + "placeholders of the same value, however" + "placeholders occur more than once. \n" + "If you would like to allow this, set the" + "replace_multiple param to True" + ) + + self.template = self.template.replace(f"[{placeholder}]", value) + + def get_template(self) -> str: + """Get the template attribute of the TemplateHTML object. + + Returns + ------- + str + The template attribute + + """ + return self.template + + +def set_up_report_dir( + path: Union[str, pathlib.Path] = "outputs", overwrite: bool = False +) -> None: + """Set up the directory that will hold the report. + + Parameters + ---------- + path : Union[str, pathlib.Path], optional + The path to the directory, + by default "outputs" + overwrite : bool, optional + Whether or not to overwrite any current reports, + by default False + + Returns + ------- + None + + """ + # defences + if not os.path.exists(path): + raise FileNotFoundError( + "The specified path does not exist. " f"Path passed: {str(path)}" + ) + + if os.path.exists(f"{path}/gtfs_report") and not overwrite: + raise FileExistsError( + "Report already exists at path: " + f"[{path}]." + "Consider setting overwrite=True" + "if you'd like to overwrite this." + ) + + try: + os.mkdir(f"{path}/gtfs_report") + except FileExistsError: + pass + shutil.copy( + src="src/transport_performance/gtfs/report/css_styles/styles.css", + dst=f"{path}/gtfs_report", + ) + return None diff --git a/src/transport_performance/gtfs/report/utils.py b/src/transport_performance/gtfs/report/utils.py deleted file mode 100644 index 136bf3f3..00000000 --- a/src/transport_performance/gtfs/report/utils.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Utils to assist in the creation of a HTML report for GTFS.""" -from typing import Union -import pathlib -import shutil -import os - -from transport_performance.utils.defence import ( - _bool_defence, - _string_defence, - _is_path_like, -) - - -def read_html_template(location: Union[str, pathlib.Path]) -> str: - """Read in a html template file. - - Parameters - ---------- - location : Union[str, pathlib.Path] - The file path of the html template - - Returns - ------- - str - A string containing the HTML read in from - the template - - """ - _is_path_like(location, "location") - # Could add checking if location exists here? - # Possibly pointless as python will return an error - # regardless - with open(location, "r", encoding="utf8") as f: - template = f.read() - - return template - - -def insert_into_template( - template: str, placeholder: str, value: str, replace_multiple: bool = False -) -> None: - """Insert values into the html template. - - Parameters - ---------- - template : str - The template to insert values into - placeholder : str - The placeholder name in the template. - This is a string. In the template it - should be surrounded by sqsuare brackets. - value : str - The value to place in the placeholder - location. - replace_multiple : bool, optional - Whether or not to replace multiple - placeholders that share the same - placeholder value, - by default False - - Returns - ------- - None - - """ - _string_defence(template, "template") - _string_defence(placeholder, "placeholder") - _string_defence(value, "value") - _bool_defence(replace_multiple, "replace_multiple") - occurences = len(template.split("[{placeholder}]")) - 1 - if occurences > 1 and not replace_multiple: - raise ValueError( - "You have selected not to replace multiple" - "placeholders of the same value, however" - "placeholders occur more than once. \n" - "If you would like to allow this, set the" - "replace_multiple param to True" - ) - - template = template.replace(f"[{placeholder}]", value) - return template - - -def set_up_report_dir( - path: Union[str, pathlib.Path] = "outputs", overwrite: bool = False -) -> None: - """Set up the directory that will hold the report. - - Parameters - ---------- - path : Union[str, pathlib.Path], optional - The path to the directory, - by default "outputs" - overwrite : bool, optional - Whether or not to overwrite any current reports, - by default False - - Returns - ------- - None - - """ - # defences - if not os.path.exists(path): - raise FileNotFoundError( - "The specified path does not exist. " f"Path passed: {str(path)}" - ) - - if os.path.exists(f"{path}/gtfs_report") and not overwrite: - raise FileExistsError( - "Report already exists at path: " - f"[{path}]." - "Consider setting overwrite=True" - "if you'd like to overwrite this." - ) - - try: - os.mkdir(f"{path}/gtfs_report") - except FileExistsError: - pass - shutil.copy( - src="src/transport_performance/gtfs/report/css_styles/styles.css", - dst=f"{path}/gtfs_report", - ) - return None diff --git a/src/transport_performance/gtfs/validation.py b/src/transport_performance/gtfs/validation.py index 52fd3947..34fd6a19 100644 --- a/src/transport_performance/gtfs/validation.py +++ b/src/transport_performance/gtfs/validation.py @@ -28,9 +28,8 @@ _dict_defence, _string_and_nonetype_defence, ) -from transport_performance.gtfs.report.utils import ( - read_html_template, - insert_into_template, +from transport_performance.gtfs.report.report_utils import ( + TemplateHTML, set_up_report_dir, ) from transport_performance.gtfs.utils import convert_pandas_to_plotly @@ -1051,26 +1050,23 @@ def html_report( # feed evaluation self.clean_feed() validation_dataframe = self.is_valid() - eval_temp = read_html_template( - location=( + eval_temp = TemplateHTML( + path=( "src/transport_performance/gtfs/report/" "html_templates/evaluation_template.html" ) ) - eval_temp = insert_into_template( - eval_temp, + eval_temp.insert( "eval_placeholder_1", convert_pandas_to_plotly(validation_dataframe, True), ) - eval_temp = insert_into_template( - eval_temp, "eval_title_1", "GTFS Feed Evalulation" - ) - eval_temp = insert_into_template(eval_temp, "date", date) + eval_temp.insert("eval_title_1", "GTFS Feed Evalulation") + eval_temp.insert("date", date) with open( f"{report_dir}/gtfs_report/index.html", "w", encoding="utf8" ) as eval_f: - eval_f.writelines(eval_temp) + eval_f.writelines(eval_temp.get_template()) # stops self.viz_stops(out_pth=f"{report_dir}/gtfs_report/stop_locations.html") @@ -1079,29 +1075,23 @@ def html_report( geoms="hull", geom_crs=27700, ) - stops_temp = read_html_template( + stops_temp = TemplateHTML( ( "src/transport_performance/gtfs/report/" "html_templates/stops_template.html" ) ) - stops_temp = insert_into_template( - stops_temp, "stops_placeholder_1", "stop_locations.html" - ) - stops_temp = insert_into_template( - stops_temp, "stops_placeholder_2", "convex_hull.html" - ) - stops_temp = insert_into_template( - stops_temp, "stops_title_1", "Stops from GTFS data" + stops_temp.insert("stops_placeholder_1", "stop_locations.html") + stops_temp.insert("stops_placeholder_2", "convex_hull.html") + stops_temp.insert("stops_title_1", "Stops from GTFS data") + stops_temp.insert( + "stops_title_2", "Convex Hull Generated from GTFS Data" ) - stops_temp = insert_into_template( - stops_temp, "stops_title_2", "Convex Hull Generated from GTFS Data" - ) - stops_temp = insert_into_template(stops_temp, "date", date) + stops_temp.insert("date", date) with open( f"{report_dir}/gtfs_report/stops.html", "w", encoding="utf8" ) as stops_f: - stops_f.writelines(stops_temp) + stops_f.writelines(stops_temp.get_template()) # summaries self.summarise_routes() @@ -1122,36 +1112,30 @@ def html_report( xlabel="Trip Count", ylabel="Day", ) - summ_temp = read_html_template( - location=( + summ_temp = TemplateHTML( + path=( "src/transport_performance/gtfs/report/" "html_templates/summary_template.html" ) ) - summ_temp = insert_into_template( - summ_temp, "plotly_placeholder_1", route_html - ) - summ_temp = insert_into_template( - summ_temp, + summ_temp.insert("plotly_placeholder_1", route_html) + summ_temp.insert( "plotly_title_1", f"Route Summary by Day and Route Type ({summary_type})", ) - summ_temp = insert_into_template( - summ_temp, "plotly_placeholder_2", trip_html - ) - summ_temp = insert_into_template( - summ_temp, + summ_temp.insert("plotly_placeholder_2", trip_html) + summ_temp.insert( "plotly_title_2", f"Trip Summary by Day and Route Type ({summary_type})", ) - summ_temp = insert_into_template(summ_temp, "date", date) + summ_temp.insert("date", date) with open( f"{report_dir}/gtfs_report/summaries.html", "w", encoding="utf8" ) as summ_f: - summ_f.writelines(summ_temp) + summ_f.writelines(summ_temp.get_template()) print( - f"GTFS Report Created at {report_dir}" + f"GTFS Report Created at {report_dir}\n" f"View your report here: {report_dir}/gtfs_report" ) diff --git a/tests/data/gtfs/report/gtfs_report/styles.css b/tests/data/gtfs/report/gtfs_report/styles.css new file mode 100644 index 00000000..e3b11e97 --- /dev/null +++ b/tests/data/gtfs/report/gtfs_report/styles.css @@ -0,0 +1,148 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + min-height: 100vh; +} + +a { + text-decoration: none; + +} + +li { + list-style: none; +} + +h1, +h2 { + color: black; +} + +h3 { + color: #999; +} + +.btn { + background: #f05462; + color: white; + padding: 5px 10px; + text-align: center; +} + +.btn:hover { + color: #f05462; + background: white; + padding: 3px 8px; + border: 2px solid #f05462; +} + +.title { + display: flex; + align-items: center; + justify-content: space-around; + padding: 15px 10px; + border-bottom: 2px solid #999; +} + +table { + padding: 10px; +} + +th, +td { + text-align: left; + padding: 8px; +} + +.side-menu { + position: fixed; + background: #28A197; + width: 20vw; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.side-menu .side-menu-title { + height: 10vh; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold +} + +.side-menu li { + font-size: 24px; + padding: 10px 40px; + display: flex; + align-items: center; +} + +.side-menu li:hover { + background: white; + color: #28A197; +} + +.side-menu .option { + color: white; +} +.side-menu .option:hover{ + color: #28A197; + background-color: white; +} + +.container { + position: absolute; + right: 0; + width: 80vw; + height: 100vh; + background: #f1f1f1; +} + +.container .content { + position: relative; + background: #f1f1f1; + padding-left: 20px; + padding-right: 20px; +} + +.container .analysis-cont { + display: flex; + justify-content: space-around; + align-items: flex-start; + flex-wrap: wrap; + background-color: #f1f1f1; + padding: 20px; +} +.container .analysis .analysis-title { + font-weight: bold; + font-size: large; + margin-bottom: 10px; +} + + +.container .header { + position: fixed; + top: 0; + right: 0; + width: 80vw; + height: 10vh; + background: #801650; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); + z-index: 1; +} + +.container .header .header-title { + display: flex; + align-items: center; + color: #F46A25; + font-weight: bold; +} diff --git a/tests/gtfs/report/test_report_utils.py b/tests/gtfs/report/test_report_utils.py new file mode 100644 index 00000000..6002114b --- /dev/null +++ b/tests/gtfs/report/test_report_utils.py @@ -0,0 +1,108 @@ +"""Test scripts for the GTFS report utility functions.""" + +import os +import shutil +import re + +import pytest +from pyprojroot import here + +from transport_performance.gtfs.report.report_utils import ( + TemplateHTML, + set_up_report_dir, +) + + +@pytest.fixture(scope="function") +def template_fixture(): + """Fixture for test funcs expecting a valid feed object.""" + template = TemplateHTML( + path=here("tests/data/gtfs/report/html_template.html") + ) + return template + + +class TestTemplateHTML(object): + """Tests related to the TemplateHTML class.""" + + def test_init(self, template_fixture): + """Test initialising the TemplateHTML class.""" + expected_template = """ + + + +
[test_placeholder] Tester [test_placeholder]
+""" + assert ( + expected_template == template_fixture.get_template() + ), "Test template not as expected" + + def test_insert_defence(self, template_fixture): + """Test defences for .insert().""" + with pytest.raises( + ValueError, + match=( + "You have selected not to replace multiple" + "placeholders of the same value, however" + "placeholders occur more than once. \n" + "If you would like to allow this, set the" + "replace_multiple param to True" + ), + ): + template_fixture.insert("test_placeholder", "test_value") + + def test_insert_on_pass(self, template_fixture): + """Test functionality for .insert() when defences are passed.""" + expected_template = """ + + + +
test_value Tester test_value
+""" + template_fixture.insert( + placeholder="test_placeholder", + value="test_value", + replace_multiple=True, + ) + assert ( + expected_template == template_fixture.get_template() + ), "Test placeholder replacement not acting as expected" + + +class TestSetUpReportDir(object): + """Test setting up a dir for a report.""" + + def test_set_up_report_dir_defence(self): + """Test the defences for set_up_report_dir().""" + with pytest.raises( + FileNotFoundError, + match="The specified path does not exist. " + "Path passed: tests/tests/tests/tests", + ): + set_up_report_dir(path="tests/tests/tests/tests") + + with pytest.raises( + FileExistsError, + match=( + re.escape( + "Report already exists at path: " + "[tests/data/gtfs/report]." + "Consider setting overwrite=True" + "if you'd like to overwrite this." + ) + ), + ): + set_up_report_dir("tests/data/gtfs/report") + + def test_set_up_report_dir_on_pass(self): + """Test set_up_report_dir() when defences are passed.""" + set_up_report_dir("data/interim") + assert os.path.exists( + here("data/interim/gtfs_report") + ), "Failed to create report in data/interim" + shutil.rmtree(path=here("data/interim/gtfs_report")) + + set_up_report_dir("tests/data/gtfs/report", overwrite=True) + assert os.path.exists( + here("tests/data/gtfs/report/gtfs_report") + ), "Failed to replace report in tests/data/gtfs/report/" diff --git a/tests/gtfs/report/test_utils.py b/tests/gtfs/report/test_utils.py deleted file mode 100644 index 181dd3c0..00000000 --- a/tests/gtfs/report/test_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Test scripts for the GTFS report utility functions.""" - -# import pytest - -# from transport_performance.gtfs.report.utils import ( -# read_html_template, -# set_up_report_dir, -# insert_into_template, -# ) - - -class TestReadHtmlTemplate(object): - """Tests for read_html_template.""" - - def test_read_html_template(self): - """Test the functionality of reading a html template.""" - - -class TestInsertIntoTemplate(object): - """Test inserting strings into a template.""" - - def test_insert_into_template_defence(self): - """Test the defences for insert_into_template.""" - - def test_insert_into_template_on_pass(self): - """Test insert_into_template() when defences are passed.""" - - -class TestSetUpReportDir(object): - """Test setting up a dir for a report.""" - - def test_set_up_report_dir_defence(self): - """Test the defences for set_up_report_dir().""" - - def test_set_up_report_dir_on_pass(self): - """Test set_up_report_dir() when defences are passed."""