From bd1f15a8f9b69d1aecd7ffef9d719e797a6bae3b Mon Sep 17 00:00:00 2001 From: Jinal Soni Date: Thu, 5 Dec 2019 13:44:10 -0500 Subject: [PATCH 01/23] BIS report - in progress --- bpeng/bis/__init__.py | 0 bpeng/bis/field_format.py | 16 +++++ bpeng/bis/report.py | 10 +++ bpeng/bis/template.py | 139 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+) create mode 100644 bpeng/bis/__init__.py create mode 100644 bpeng/bis/field_format.py create mode 100644 bpeng/bis/report.py create mode 100644 bpeng/bis/template.py diff --git a/bpeng/bis/__init__.py b/bpeng/bis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bpeng/bis/field_format.py b/bpeng/bis/field_format.py new file mode 100644 index 0000000..9fcbd38 --- /dev/null +++ b/bpeng/bis/field_format.py @@ -0,0 +1,16 @@ +from datetime import date + +def format_usd(val): + """ + Format an integer as a string representation of US Dollars. + """ + return '${:,}'.format(val) + +def format_bool(val): + if b: + return 'Yes' + else: + return 'No' + +def format_date(val): + return val.strftime("%m/%d/%Y") diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py new file mode 100644 index 0000000..18463b1 --- /dev/null +++ b/bpeng/bis/report.py @@ -0,0 +1,10 @@ +from pptx import Presentation +import os +from datetime import date +import uuid +from pptx.enum.text import ( # pylint: disable=no-name-in-module + MSO_ANCHOR, + PP_ALIGN +) +from .template import * +from .field_format import * diff --git a/bpeng/bis/template.py b/bpeng/bis/template.py new file mode 100644 index 0000000..f9bb674 --- /dev/null +++ b/bpeng/bis/template.py @@ -0,0 +1,139 @@ +from pptx import Presentation +from pptx.dml.color import RGBColor +from pptx.enum.text import ( # pylint: disable=no-name-in-module + MSO_ANCHOR, + PP_ALIGN +) +from pptx.util import Inches, Pt + +from .field_format import * + +class TemplateInstantiator(): + """ + Creates a report from a template + """ + def __init__(self, presentation_path): + self.presentation = Presentation(presentation_path) + self.slideInstantiator = [TemplateSlideInstantiator() for slide in self.presentation.slides] + + def templateSlideNumber(self, index): + return self.slideInstantiator[index] + + def export(self): + i=0 + for slide in self.presentation.slides: + slide = self.slideInstantiator[i].render(slide) + i=i+1 + + return self.presentation + +class TemplateSlideInstantiator(): + def __init__(self): + self.substitutions = [] + self.extra_functions = [] + + def add_substitutions(self, subs): + for sub in subs: + self.substitutions.append(sub) + + def add_function(self, fun): + self.extra_functions.append(fun) + + def render(self, slide): + for sub in self.substitutions: + sub.render(slide) + + for function in self.extra_functions: + function(slide) + + return slide + +class Substitution(): + """ + Represents a single text-based substitution of a value from a database field + into a template. + """ + def __init__(self, name_in_ppt, value, external_value=None, + font='Avenir', font_size=21, color=(0,0,0), no_data_color=(255,0,0), formatter=None, + bold=False, align=PP_ALIGN.LEFT, underline=False): + """ + We use 3 underscores to denote a field in the powerpoint template. + These fields are filled in using substitutions. + + name_in_ppt - the name (without the added triple underscores) in PPT + template that we are looking to replace + value - the value to replace it with + external_value - if user is external, we can optionally replace the + PPT template name with a different value + Font size - in Pt. + Color - an RGB tuple + formatter - a lambda function, or a function from field_format.py, + to format the string for better presentation form + """ + self.name_in_ppt = '___{}___'.format(name_in_ppt) + self.value = str(value) + self.font = font + self.font_size = font_size + self.bold = bold + self.underline = underline + self.alignment = align + + self.color = color + if len(self.value) == 0: + self.color = no_data_color + self.value = 'No Information Provided By User' + + if formatter is None: + self.formatter = lambda x: x + else: + self.formatter = formatter + + def ___repr___(self): + return "[" + self.name_in_ppt + ", " + self.formatter(self.value) + "]" + + def ___str___(self): + return self.___repr___() + + def toString(self): + return self.___repr___() + + def render(self, slide): + if slide is None: + print("slide is None") + return + + for shape in slide.shapes: + if shape is None: + continue + if shape.has_text_frame and shape.text.find(self.name_in_ppt) != -1: + new_text = shape.text + new_text = new_text.replace( + self.name_in_ppt, # underscores from constructor + self.formatter(self.value) + ) + shape.text = new_text + + for p in shape.text_frame.paragraphs: + p.font.name = self.font + p.font.size = Pt(self.font_size) + p.font.bold = self.bold + p.font.color.rgb = RGBColor(self.color[0], self.color[1], self.color[2]) + p.font.underline = self.underline + p.alignment = self.alignment + + if shape.has_table: + for row in shape.table.rows: + for cell in row.cells: + if cell.text is not None and cell.text.find(self.name_in_ppt) != -1: + cell.text = cell.text.replace( + self.name_in_ppt, # underscores from constructor + self.formatter(self.value) + ) + + for p in cell.text_frame.paragraphs: + p.font.name = self.font + p.font.size = Pt(self.font_size) + p.font.bold = self.bold + p.font.color.rgb = RGBColor(self.color[0], self.color[1], self.color[2]) + p.font.underline = self.underline + p.alignment = self.alignment -- GitLab From 39fba590f53d1dca1e4274e979a9f126f0272f99 Mon Sep 17 00:00:00 2001 From: Jinal Soni Date: Thu, 5 Dec 2019 17:04:20 -0500 Subject: [PATCH 02/23] report generate file - not tested --- bpeng/bis/report.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 18463b1..b09f887 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -8,3 +8,49 @@ from pptx.enum.text import ( # pylint: disable=no-name-in-module ) from .template import * from .field_format import * + +def generate_bis_report(template_path, building_info): + """ + Generate an BIS Report from dicts + + building_info - dict containing these keys: + - address (string) + + Returns a python-pptx presentation object + """ + report_generator = TemplateInstantiator(template_path) + addr = building_info['address'] + + # Provide template with address, date for title page + addr_sub = Substitution( + 'ADDRESS', + addr, + font_size=18, + font = 'Avenir', + align=PP_ALIGN.LEFT, + bold=False, + color=(173,216,216), + ) + + date_sub = Substitution( + 'DATE', + format_date(date.today()), + font_size=9, + color=(173,216,216), + ) + + title_slide = report_generator.templateSlideNumber(0) + title_slide.add_substitutions([ + date_sub, + addr_sub + ]) + + # Save PPT to a temporary file, and pass a reference to BlocLink + tmp_dir = '/tmp/bpeng/{}'.format(uuid.uuid4()) + escaped_addr = addr.replace(' ', '_') # TODO: replace special characters + save_to = '{}/BIS_{}.pptx'.format(tmp_dir, escaped_addr) + if not os.path.exists(tmp_dir): + os.makedirs(tmp_dir) + + report_generator.export().save(save_to) + return save_to -- GitLab From 4c40624e168c825d4e608e4d6d550eb465e27ac9 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 07:54:41 -0800 Subject: [PATCH 03/23] Update README to include necessary report templates. --- README.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0468448..658ce42 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,30 @@ # BPEngine -### Requirements +## Requirements + python: 3.6.4 pip: 9.0.3 -### Installation +## Installation + `pip install git+ssh://git@github.com/Blocp/bpengine.git` -- For reporting, install python-pptx -`pip install git+ssh://git@github.com/Blocp/python-pptx.git` +## Developer Setup -### Developer Setup -``` +```bash mkvirtualenv bpeng pip install -r requirements-dev.txt ``` -#### Run Tests and Coverage +### Download Report Templates + +Download report templates and save in dev-templates directory to use when generation offline + +- PNS_NonNYC_Template.pptx +- PNA_Report_Template.pptx + +### Run Tests and Coverage + - Copy `pytest.default.ini` --> `pytest.ini` - `pytest bpeng tests` -- GitLab From 4f6cb69f47aa7c3d2df2a0af286d93b84518c89a Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 07:56:31 -0800 Subject: [PATCH 04/23] Add comments to understand template processing. --- bpeng/bis/template.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bpeng/bis/template.py b/bpeng/bis/template.py index f9bb674..b2abf27 100644 --- a/bpeng/bis/template.py +++ b/bpeng/bis/template.py @@ -10,7 +10,7 @@ from .field_format import * class TemplateInstantiator(): """ - Creates a report from a template + Creates a PNA report from a template for non NYC buildings """ def __init__(self, presentation_path): self.presentation = Presentation(presentation_path) @@ -20,10 +20,10 @@ class TemplateInstantiator(): return self.slideInstantiator[index] def export(self): - i=0 + index = 0 for slide in self.presentation.slides: - slide = self.slideInstantiator[i].render(slide) - i=i+1 + slide = self.slideInstantiator[index].render(slide) + index = index+1 return self.presentation @@ -41,9 +41,11 @@ class TemplateSlideInstantiator(): def render(self, slide): for sub in self.substitutions: + # Render substitution in slide sub.render(slide) for function in self.extra_functions: + # Apply Function to slide function(slide) return slide -- GitLab From f2f87071635358b3eb502211a1cd22164be171e8 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 07:58:17 -0800 Subject: [PATCH 05/23] Add function to duplicate slide when we have more than 3 recommendations. --- bpeng/bis/report.py | 57 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index b09f887..e333bd3 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -1,7 +1,9 @@ -from pptx import Presentation import os +import copy from datetime import date import uuid +import six # Add to requirements +from pptx import Presentation from pptx.enum.text import ( # pylint: disable=no-name-in-module MSO_ANCHOR, PP_ALIGN @@ -9,16 +11,42 @@ from pptx.enum.text import ( # pylint: disable=no-name-in-module from .template import * from .field_format import * -def generate_bis_report(template_path, building_info): +def duplicate_slide(pres,index): + template = pres.slides[index] + try: + blank_slide_layout = pres.slide_layouts[12] + except: + blank_slide_layout = pres.slide_layouts[len(pres.slide_layouts)] + + copied_slide = pres.slides.add_slide(blank_slide_layout) + + for shp in template.shapes: + el = shp.element + newel = copy.deepcopy(el) + copied_slide.shapes._spTree.insert_element_before(newel, 'p:extLst') + + for _, value in six.iteritems(template.part.rels): + # Make sure we don't copy a notesSlide relation as that won't exist + if "notesSlide" not in value.reltype: + copied_slide.part.rels.add_relationship(value.reltype, + value._target, + value.rId) + + return copied_slide + +def generate_pns_report(template_path, building_info, recommendations): """ - Generate an BIS Report from dicts + Generate an PNS Report from dicts building_info - dict containing these keys: - address (string) + + recommendations - list of recommendation objects Returns a python-pptx presentation object """ - report_generator = TemplateInstantiator(template_path) + report_template = TemplateInstantiator(template_path) + report = TemplateInstantiator(template_path) addr = building_info['address'] # Provide template with address, date for title page @@ -39,12 +67,29 @@ def generate_bis_report(template_path, building_info): color=(173,216,216), ) - title_slide = report_generator.templateSlideNumber(0) + title_slide = report.templateSlideNumber(0) title_slide.add_substitutions([ date_sub, addr_sub ]) + # Add Recommendations to Recommedations Slide Template + recommendations_inserted = 0 + total_recs = len(recommendations) + + while recommendations_inserted < total_recs: + recommentation_slide = report.templateSlideNumber(2) + recommentation_slide.add_substitutions([ + date_sub, + addr_sub + ]) + # Insert max 3 recommendations per slide + recs_in_page = 0 + while recs_in_page < 3 and recommendations_inserted < total_recs: + recs_in_page += 1 + print(recommendations[recommendations_inserted]) + + # Save PPT to a temporary file, and pass a reference to BlocLink tmp_dir = '/tmp/bpeng/{}'.format(uuid.uuid4()) escaped_addr = addr.replace(' ', '_') # TODO: replace special characters @@ -52,5 +97,5 @@ def generate_bis_report(template_path, building_info): if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) - report_generator.export().save(save_to) + report_template.export().save(save_to) return save_to -- GitLab From 2a0ce195c8481c581e6df7ba00adb683090225be Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 08:34:39 -0800 Subject: [PATCH 06/23] Clean up copy indentation. --- bpeng/bis/report.py | 51 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index e333bd3..348ee0c 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -12,27 +12,27 @@ from .template import * from .field_format import * def duplicate_slide(pres,index): - template = pres.slides[index] - try: - blank_slide_layout = pres.slide_layouts[12] - except: - blank_slide_layout = pres.slide_layouts[len(pres.slide_layouts)] + template = pres.slides[index] + try: + blank_slide_layout = pres.slide_layouts[12] + except: + blank_slide_layout = pres.slide_layouts[len(pres.slide_layouts)] - copied_slide = pres.slides.add_slide(blank_slide_layout) + copied_slide = pres.slides.add_slide(blank_slide_layout) - for shp in template.shapes: - el = shp.element - newel = copy.deepcopy(el) - copied_slide.shapes._spTree.insert_element_before(newel, 'p:extLst') + for shp in template.shapes: + el = shp.element + newel = copy.deepcopy(el) + copied_slide.shapes._spTree.insert_element_before(newel, 'p:extLst') - for _, value in six.iteritems(template.part.rels): - # Make sure we don't copy a notesSlide relation as that won't exist - if "notesSlide" not in value.reltype: - copied_slide.part.rels.add_relationship(value.reltype, - value._target, - value.rId) + for _, value in six.iteritems(template.part.rels): + # Make sure we don't copy a notesSlide relation as that won't exist + if "notesSlide" not in value.reltype: + copied_slide.part.rels.add_relationship(value.reltype, + value._target, + value.rId) - return copied_slide + return copied_slide def generate_pns_report(template_path, building_info, recommendations): """ @@ -40,19 +40,17 @@ def generate_pns_report(template_path, building_info, recommendations): building_info - dict containing these keys: - address (string) - - recommendations - list of recommendation objects + recommendations - list of recommendation objects Returns a python-pptx presentation object """ - report_template = TemplateInstantiator(template_path) + # report_template = TemplateInstantiator(template_path) report = TemplateInstantiator(template_path) - addr = building_info['address'] - + address = building_info['address'] # Provide template with address, date for title page addr_sub = Substitution( 'ADDRESS', - addr, + address, font_size=18, font = 'Avenir', align=PP_ALIGN.LEFT, @@ -86,16 +84,17 @@ def generate_pns_report(template_path, building_info, recommendations): # Insert max 3 recommendations per slide recs_in_page = 0 while recs_in_page < 3 and recommendations_inserted < total_recs: - recs_in_page += 1 print(recommendations[recommendations_inserted]) + recs_in_page += 1 + recommendations_inserted += 1 # Save PPT to a temporary file, and pass a reference to BlocLink tmp_dir = '/tmp/bpeng/{}'.format(uuid.uuid4()) - escaped_addr = addr.replace(' ', '_') # TODO: replace special characters + escaped_addr = address.replace(' ', '_') # TODO: replace special characters save_to = '{}/BIS_{}.pptx'.format(tmp_dir, escaped_addr) if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) - report_template.export().save(save_to) + report.export().save(save_to) return save_to -- GitLab From f91f3e19414a277df501f4eb6a728163a20eda62 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 09:45:07 -0800 Subject: [PATCH 07/23] Add place holders for substitutions in the slides --- bpeng/bis/report.py | 17 +++++++++++++++++ tests/bis/__init__.py | 0 tests/bis/test_report_generation.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/bis/__init__.py create mode 100644 tests/bis/test_report_generation.py diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 348ee0c..38742f5 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -76,6 +76,23 @@ def generate_pns_report(template_path, building_info, recommendations): total_recs = len(recommendations) while recommendations_inserted < total_recs: + rec_1_sub = Substitution( + 'ADDRESS', + address, + font_size=18, + font = 'Avenir', + align=PP_ALIGN.LEFT, + ) + rec_2_sub = Substitution( + 'DATE', + format_date(date.today()), + font_size=9, + ) + rec_3_sub = Substitution( + 'DATE', + format_date(date.today()), + font_size=9, + ) recommentation_slide = report.templateSlideNumber(2) recommentation_slide.add_substitutions([ date_sub, diff --git a/tests/bis/__init__.py b/tests/bis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py new file mode 100644 index 0000000..9ec5d13 --- /dev/null +++ b/tests/bis/test_report_generation.py @@ -0,0 +1,14 @@ +from bpeng.bis.report import * + +def test_report_generation(): + building_info = {'address':'489 Hart Street Brooklyn, NY 11221'} + recommendation_list = [ + {'', ''}, + {'', ''}, + ] + + report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + + print(report) + +test_report_generation() -- GitLab From de03492cc96c71ae391b5cb0b2cee9e7832fd6ca Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 09:45:55 -0800 Subject: [PATCH 08/23] Add list of recommendations for testing report generation. --- tests/bis/test_report_generation.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 9ec5d13..736650b 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -3,8 +3,11 @@ from bpeng.bis.report import * def test_report_generation(): building_info = {'address':'489 Hart Street Brooklyn, NY 11221'} recommendation_list = [ - {'', ''}, - {'', ''}, + {'Heat Pump Water Heater', 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'Smart DHW Recirculation Pumps', 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + {'Cool Roof', 'Install a cool roof by adding reflective paint, sheet coverings, tiles, or shingles so your building absorbs less heat from the sunlight and stay cooler in the summer.'}, + {'Smart Thermostat', "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, + {'LED Lighting', 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, ] report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) -- GitLab From d57749be07b10de7cb3ae5180546d3795ada71fd Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 09:59:31 -0800 Subject: [PATCH 09/23] Properly format the recommendations object in the test. --- tests/bis/test_report_generation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 736650b..4d388a9 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -3,11 +3,11 @@ from bpeng.bis.report import * def test_report_generation(): building_info = {'address':'489 Hart Street Brooklyn, NY 11221'} recommendation_list = [ - {'Heat Pump Water Heater', 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, - {'Smart DHW Recirculation Pumps', 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, - {'Cool Roof', 'Install a cool roof by adding reflective paint, sheet coverings, tiles, or shingles so your building absorbs less heat from the sunlight and stay cooler in the summer.'}, - {'Smart Thermostat', "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, - {'LED Lighting', 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, + {'title':'Heat Pump Water Heater', 'description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'title':'Smart DHW Recirculation Pumps', 'description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + {'title':'Cool Roof', 'description': 'Install a cool roof by adding reflective paint, sheet coverings, tiles, or shingles so your building absorbs less heat from the sunlight and stay cooler in the summer.'}, + {'title':'Smart Thermostat', 'description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, + {'title':'LED Lighting', 'description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, ] report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) -- GitLab From 6e60db0e7561e1f2053729437792358160062201 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 10:07:01 -0800 Subject: [PATCH 10/23] Update object to use same parameters as Django model. --- tests/bis/test_report_generation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 4d388a9..379b934 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -3,11 +3,11 @@ from bpeng.bis.report import * def test_report_generation(): building_info = {'address':'489 Hart Street Brooklyn, NY 11221'} recommendation_list = [ - {'title':'Heat Pump Water Heater', 'description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, - {'title':'Smart DHW Recirculation Pumps', 'description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, - {'title':'Cool Roof', 'description': 'Install a cool roof by adding reflective paint, sheet coverings, tiles, or shingles so your building absorbs less heat from the sunlight and stay cooler in the summer.'}, - {'title':'Smart Thermostat', 'description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, - {'title':'LED Lighting', 'description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, + {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + {'ecm_name':'Cool Roof', 'short_description': 'Install a cool roof by adding reflective paint, sheet coverings, tiles, or shingles so your building absorbs less heat from the sunlight and stay cooler in the summer.'}, + {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, + {'ecm_name':'LED Lighting', 'short_description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, ] report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) -- GitLab From d59154dfa1ec4902832977f1410be3beb03ca6de Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 11:00:32 -0800 Subject: [PATCH 11/23] Insert recommendations into slide and simplify import in BIS test case. --- bpeng/bis/report.py | 46 ++++++++++++++--------------- tests/bis/test_report_generation.py | 2 +- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 38742f5..a594179 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -76,35 +76,33 @@ def generate_pns_report(template_path, building_info, recommendations): total_recs = len(recommendations) while recommendations_inserted < total_recs: - rec_1_sub = Substitution( - 'ADDRESS', - address, - font_size=18, - font = 'Avenir', - align=PP_ALIGN.LEFT, - ) - rec_2_sub = Substitution( - 'DATE', - format_date(date.today()), - font_size=9, - ) - rec_3_sub = Substitution( - 'DATE', - format_date(date.today()), - font_size=9, - ) - recommentation_slide = report.templateSlideNumber(2) - recommentation_slide.add_substitutions([ - date_sub, - addr_sub - ]) # Insert max 3 recommendations per slide recs_in_page = 0 + subs = [] + recommentation_slide = report.templateSlideNumber(2) while recs_in_page < 3 and recommendations_inserted < total_recs: - print(recommendations[recommendations_inserted]) + rec_title = Substitution( + 'TITLE'+str(recs_in_page), + recommendations[recommendations_inserted]['ecm_name'], + font_size=11, + font='Avenir', + align=PP_ALIGN.LEFT, + color=(15, 45, 65) + ) + subs.append(rec_title) + rec_description = Substitution( + 'DESC'+str(recs_in_page), + recommendations[recommendations_inserted]['short_description'], + font_size=10, + font='Avenir', + align=PP_ALIGN.LEFT, + color=(15, 45, 65) + ) + subs.append(rec_description) recs_in_page += 1 recommendations_inserted += 1 - + # Add substitutions to 1 slide at a time + recommentation_slide.add_substitutions(subs) # Save PPT to a temporary file, and pass a reference to BlocLink tmp_dir = '/tmp/bpeng/{}'.format(uuid.uuid4()) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 379b934..3b0c940 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -1,4 +1,4 @@ -from bpeng.bis.report import * +from bpeng.bis.report import generate_pns_report def test_report_generation(): building_info = {'address':'489 Hart Street Brooklyn, NY 11221'} -- GitLab From 687b35530efbef56b6864148241fe88052e73d99 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Tue, 31 Dec 2019 11:04:36 -0800 Subject: [PATCH 12/23] Use Oakland Address in test generation. --- bpeng/bis/report.py | 2 +- tests/bis/test_report_generation.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index a594179..a285007 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -40,7 +40,7 @@ def generate_pns_report(template_path, building_info, recommendations): building_info - dict containing these keys: - address (string) - recommendations - list of recommendation objects + recommendations - list of recommendation dicts Returns a python-pptx presentation object """ diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 3b0c940..81d8aab 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -1,11 +1,10 @@ from bpeng.bis.report import generate_pns_report def test_report_generation(): - building_info = {'address':'489 Hart Street Brooklyn, NY 11221'} + building_info = {'address':'2148 Broadway, Oakland, CA 94612'} recommendation_list = [ {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, - {'ecm_name':'Cool Roof', 'short_description': 'Install a cool roof by adding reflective paint, sheet coverings, tiles, or shingles so your building absorbs less heat from the sunlight and stay cooler in the summer.'}, {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, {'ecm_name':'LED Lighting', 'short_description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, ] -- GitLab From 1e4b0d41afa03bb99d5a557184e231751ad7e52d Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Thu, 2 Jan 2020 14:30:32 -0800 Subject: [PATCH 13/23] Try to create copy of a slide. --- bpeng/bis/report.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index a285007..3d5adf9 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -1,5 +1,6 @@ import os import copy +import math from datetime import date import uuid import six # Add to requirements @@ -11,7 +12,7 @@ from pptx.enum.text import ( # pylint: disable=no-name-in-module from .template import * from .field_format import * -def duplicate_slide(pres,index): +def duplicate_slide(pres, index): template = pres.slides[index] try: blank_slide_layout = pres.slide_layouts[12] @@ -44,6 +45,8 @@ def generate_pns_report(template_path, building_info, recommendations): Returns a python-pptx presentation object """ + recommendations_slide_index = 2 + recs_per_slide = 3 # report_template = TemplateInstantiator(template_path) report = TemplateInstantiator(template_path) address = building_info['address'] @@ -55,14 +58,14 @@ def generate_pns_report(template_path, building_info, recommendations): font = 'Avenir', align=PP_ALIGN.LEFT, bold=False, - color=(173,216,216), + color=(173, 216, 216), ) date_sub = Substitution( 'DATE', format_date(date.today()), font_size=9, - color=(173,216,216), + color=(173, 216, 216), ) title_slide = report.templateSlideNumber(0) @@ -74,13 +77,17 @@ def generate_pns_report(template_path, building_info, recommendations): # Add Recommendations to Recommedations Slide Template recommendations_inserted = 0 total_recs = len(recommendations) + num_recommendation_slides = math.ceil(float(total_recs)/recs_per_slide) + if num_recommendation_slides > 1: + # if more than one slide generate additional slides + rec_slide_copy = duplicate_slide(report.presentation, 2) while recommendations_inserted < total_recs: # Insert max 3 recommendations per slide recs_in_page = 0 subs = [] - recommentation_slide = report.templateSlideNumber(2) - while recs_in_page < 3 and recommendations_inserted < total_recs: + recommentation_slide = report.templateSlideNumber(recommendations_slide_index) + while recs_in_page < recs_per_slide and recommendations_inserted < total_recs: rec_title = Substitution( 'TITLE'+str(recs_in_page), recommendations[recommendations_inserted]['ecm_name'], -- GitLab From 6fac26d774713796832b70200f6aaf2c4475ae37 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 09:55:21 -0800 Subject: [PATCH 14/23] Add assertion to test. --- tests/bis/test_report_generation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 81d8aab..affd110 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -12,5 +12,4 @@ def test_report_generation(): report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) print(report) - -test_report_generation() + assert 0 == 1 -- GitLab From b0330dab1f3a91b99ad64e3de45867acc0f03c5a Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 10:00:09 -0800 Subject: [PATCH 15/23] Move slide template instantiation to it's own method to use independently. --- bpeng/bis/template.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bpeng/bis/template.py b/bpeng/bis/template.py index b2abf27..f59dcc0 100644 --- a/bpeng/bis/template.py +++ b/bpeng/bis/template.py @@ -14,11 +14,14 @@ class TemplateInstantiator(): """ def __init__(self, presentation_path): self.presentation = Presentation(presentation_path) - self.slideInstantiator = [TemplateSlideInstantiator() for slide in self.presentation.slides] + self.instantiateSlideTemplates() def templateSlideNumber(self, index): return self.slideInstantiator[index] + def instantiateSlideTemplates(self): + self.slideInstantiator = [TemplateSlideInstantiator() for slide in self.presentation.slides] + def export(self): index = 0 for slide in self.presentation.slides: @@ -28,6 +31,9 @@ class TemplateInstantiator(): return self.presentation class TemplateSlideInstantiator(): + """ + Instantiates slides - TODO: Figure out where did we get this code from. + """ def __init__(self): self.substitutions = [] self.extra_functions = [] @@ -56,7 +62,7 @@ class Substitution(): into a template. """ def __init__(self, name_in_ppt, value, external_value=None, - font='Avenir', font_size=21, color=(0,0,0), no_data_color=(255,0,0), formatter=None, + font='Avenir', font_size=21, color=(0, 0, 0), no_data_color=(255, 0, 0), formatter=None, bold=False, align=PP_ALIGN.LEFT, underline=False): """ We use 3 underscores to denote a field in the powerpoint template. -- GitLab From 15412830ae08cacab0e0cd04660c9794805b7995 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 11:22:40 -0800 Subject: [PATCH 16/23] Add duplicate slide at the end. --- bpeng/bis/report.py | 58 +++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 3d5adf9..6d0bcc5 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -12,28 +12,37 @@ from pptx.enum.text import ( # pylint: disable=no-name-in-module from .template import * from .field_format import * +def _get_blank_slide_layout(pres): + layout_items_count = [len(layout.placeholders) for layout in pres.slide_layouts] + min_items = min(layout_items_count) + blank_layout_id = layout_items_count.index(min_items) + return pres.slide_layouts[blank_layout_id] + def duplicate_slide(pres, index): - template = pres.slides[index] - try: - blank_slide_layout = pres.slide_layouts[12] - except: - blank_slide_layout = pres.slide_layouts[len(pres.slide_layouts)] + """Duplicate the slide with the given index in pres. + + Adds slide to the end of the presentation""" + source = pres.slides[index] - copied_slide = pres.slides.add_slide(blank_slide_layout) + blank_slide_layout = _get_blank_slide_layout(pres) + dest = pres.slides.add_slide(blank_slide_layout) + + # Remove shapes in blank slide layout + for shp in dest.shapes: + el = shp.element + el.getparent().remove(el) - for shp in template.shapes: + for shp in source.shapes: el = shp.element newel = copy.deepcopy(el) - copied_slide.shapes._spTree.insert_element_before(newel, 'p:extLst') + dest.shapes._spTree.insert_element_before(newel, 'p:extLst') - for _, value in six.iteritems(template.part.rels): + for key, value in six.iteritems(source.part.rels): # Make sure we don't copy a notesSlide relation as that won't exist - if "notesSlide" not in value.reltype: - copied_slide.part.rels.add_relationship(value.reltype, - value._target, - value.rId) + if not "notesSlide" in value.reltype: + dest.part.rels.add_relationship(value.reltype, value._target, value.rId) - return copied_slide + return dest def generate_pns_report(template_path, building_info, recommendations): """ @@ -47,10 +56,17 @@ def generate_pns_report(template_path, building_info, recommendations): """ recommendations_slide_index = 2 recs_per_slide = 3 + total_recs = len(recommendations) + num_recommendation_slides = math.ceil(float(total_recs)/recs_per_slide) # report_template = TemplateInstantiator(template_path) report = TemplateInstantiator(template_path) - address = building_info['address'] + if num_recommendation_slides > 1: + # if more than one slide generate additional slides + for x in range(0, num_recommendation_slides - 1): + duplicate_slide(report.presentation, 2) + report.instantiateSlideTemplates() # Provide template with address, date for title page + address = building_info['address'] addr_sub = Substitution( 'ADDRESS', address, @@ -76,11 +92,6 @@ def generate_pns_report(template_path, building_info, recommendations): # Add Recommendations to Recommedations Slide Template recommendations_inserted = 0 - total_recs = len(recommendations) - num_recommendation_slides = math.ceil(float(total_recs)/recs_per_slide) - if num_recommendation_slides > 1: - # if more than one slide generate additional slides - rec_slide_copy = duplicate_slide(report.presentation, 2) while recommendations_inserted < total_recs: # Insert max 3 recommendations per slide @@ -94,7 +105,8 @@ def generate_pns_report(template_path, building_info, recommendations): font_size=11, font='Avenir', align=PP_ALIGN.LEFT, - color=(15, 45, 65) + color=(15, 45, 65), + bold=True ) subs.append(rec_title) rec_description = Substitution( @@ -118,5 +130,9 @@ def generate_pns_report(template_path, building_info, recommendations): if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) + # Trying to figure out why pptx gets broken after inserting new slide + for slide in report.presentation.slides: + print(slide.name) + print(slide.slide_id) report.export().save(save_to) return save_to -- GitLab From 0c5651065fcc060977ce7e01295b559182864d28 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 12:24:20 -0800 Subject: [PATCH 17/23] Duplicated slides from secondary presentation. --- bpeng/bis/report.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 6d0bcc5..5fc492e 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -18,11 +18,12 @@ def _get_blank_slide_layout(pres): blank_layout_id = layout_items_count.index(min_items) return pres.slide_layouts[blank_layout_id] -def duplicate_slide(pres, index): - """Duplicate the slide with the given index in pres. +def duplicate_slide(pres, source_pres, index): + """Duplicate the slide with the given index in source pres to pres. - Adds slide to the end of the presentation""" - source = pres.slides[index] + Adds slide to the end of the presentation from other presentation""" + + source = source_pres.slides[index] blank_slide_layout = _get_blank_slide_layout(pres) dest = pres.slides.add_slide(blank_slide_layout) @@ -58,12 +59,16 @@ def generate_pns_report(template_path, building_info, recommendations): recs_per_slide = 3 total_recs = len(recommendations) num_recommendation_slides = math.ceil(float(total_recs)/recs_per_slide) - # report_template = TemplateInstantiator(template_path) + report_template = TemplateInstantiator('./dev-templates/PNS_NonNYC_Contact_Us.pptx') report = TemplateInstantiator(template_path) if num_recommendation_slides > 1: - # if more than one slide generate additional slides + # if more than one slide generate additional recommendation slides for x in range(0, num_recommendation_slides - 1): - duplicate_slide(report.presentation, 2) + duplicate_slide(report.presentation, report_template.presentation, 0) + + # Add Contact Us Slide + duplicate_slide(report.presentation, report_template.presentation, 1) + # Prepare slides for substitutions report.instantiateSlideTemplates() # Provide template with address, date for title page address = building_info['address'] -- GitLab From d0c97804bd4b50f39796454b87288b094b8a9059 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 12:25:10 -0800 Subject: [PATCH 18/23] Add tests for generating reports with different amount of recommendations --- tests/bis/test_report_generation.py | 47 ++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index affd110..d00df63 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -1,6 +1,29 @@ from bpeng.bis.report import generate_pns_report -def test_report_generation(): +def test_report_generation_1_rec(): + building_info = {'address':'2148 Broadway, Oakland, CA 94612'} + recommendation_list = [ + {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + ] + + report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + + print(report) + assert 0 == 1 + +def test_report_generation_2_recs(): + building_info = {'address':'2148 Broadway, Oakland, CA 94612'} + recommendation_list = [ + {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + ] + + report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + + print(report) + assert 0 == 1 + +def test_report_generation_4_recs(): building_info = {'address':'2148 Broadway, Oakland, CA 94612'} recommendation_list = [ {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, @@ -13,3 +36,25 @@ def test_report_generation(): print(report) assert 0 == 1 + + +def test_report_generation_11_recs(): + building_info = {'address':'2148 Broadway, Oakland, CA 94612'} + recommendation_list = [ + {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, + {'ecm_name':'LED Lighting', 'short_description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, + {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, + {'ecm_name':'LED Lighting', 'short_description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, + {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, + {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, + {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, + ] + + report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + + print(report) + assert 0 == 1 -- GitLab From 82337dd4dc2b0c4350b18672bf0f2ae46895940e Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 15:54:12 -0800 Subject: [PATCH 19/23] Add tests for reports with different amount of recommendations. Up to 12 recommendations fit. Use one of 4 templates when generating report allowing for max 12 recommendations. Update README with locations of template files. --- README.md | 6 ++- bpeng/bis/report.py | 80 +++++++++++++++++------------ tests/bis/test_report_generation.py | 8 +-- 3 files changed, 55 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 658ce42..070476f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,11 @@ pip install -r requirements-dev.txt Download report templates and save in dev-templates directory to use when generation offline -- PNS_NonNYC_Template.pptx +- PNS_NonNYC_Templates to `bpeng/bis/templates` directory + - PNS_NonNYC_Template_1.pptx + - PNS_NonNYC_Template_2.pptx + - PNS_NonNYC_Template_3.pptx + - PNS_NonNYC_Template_4.pptx - PNA_Report_Template.pptx ### Run Tests and Coverage diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 5fc492e..027f46b 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -18,12 +18,12 @@ def _get_blank_slide_layout(pres): blank_layout_id = layout_items_count.index(min_items) return pres.slide_layouts[blank_layout_id] -def duplicate_slide(pres, source_pres, index): +def duplicate_slide(pres, index): """Duplicate the slide with the given index in source pres to pres. Adds slide to the end of the presentation from other presentation""" - source = source_pres.slides[index] + source = pres.slides[index] blank_slide_layout = _get_blank_slide_layout(pres) dest = pres.slides.add_slide(blank_slide_layout) @@ -45,7 +45,7 @@ def duplicate_slide(pres, source_pres, index): return dest -def generate_pns_report(template_path, building_info, recommendations): +def generate_pns_report(building_info, recommendations): """ Generate an PNS Report from dicts @@ -59,17 +59,8 @@ def generate_pns_report(template_path, building_info, recommendations): recs_per_slide = 3 total_recs = len(recommendations) num_recommendation_slides = math.ceil(float(total_recs)/recs_per_slide) - report_template = TemplateInstantiator('./dev-templates/PNS_NonNYC_Contact_Us.pptx') - report = TemplateInstantiator(template_path) - if num_recommendation_slides > 1: - # if more than one slide generate additional recommendation slides - for x in range(0, num_recommendation_slides - 1): - duplicate_slide(report.presentation, report_template.presentation, 0) - - # Add Contact Us Slide - duplicate_slide(report.presentation, report_template.presentation, 1) - # Prepare slides for substitutions - report.instantiateSlideTemplates() + # Select Template Based on the number of recommendations with a max of 12 recommendations + report = TemplateInstantiator('./bpeng/bis/templates/PNS_NonNYC_Template_' + str(num_recommendation_slides) + '.pptx') # Provide template with address, date for title page address = building_info['address'] addr_sub = Substitution( @@ -103,30 +94,51 @@ def generate_pns_report(template_path, building_info, recommendations): recs_in_page = 0 subs = [] recommentation_slide = report.templateSlideNumber(recommendations_slide_index) - while recs_in_page < recs_per_slide and recommendations_inserted < total_recs: - rec_title = Substitution( - 'TITLE'+str(recs_in_page), - recommendations[recommendations_inserted]['ecm_name'], - font_size=11, - font='Avenir', - align=PP_ALIGN.LEFT, - color=(15, 45, 65), - bold=True - ) - subs.append(rec_title) - rec_description = Substitution( - 'DESC'+str(recs_in_page), - recommendations[recommendations_inserted]['short_description'], - font_size=10, - font='Avenir', - align=PP_ALIGN.LEFT, - color=(15, 45, 65) - ) + while recs_in_page < recs_per_slide: + if recommendations_inserted < total_recs: + rec_title = Substitution( + 'TITLE'+str(recs_in_page), + recommendations[recommendations_inserted]['ecm_name'], + font_size=11, + font='Avenir', + align=PP_ALIGN.LEFT, + color=(15, 45, 65), + bold=True + ) + subs.append(rec_title) + rec_description = Substitution( + 'DESC'+str(recs_in_page), + recommendations[recommendations_inserted]['short_description'], + font_size=10, + font='Avenir', + align=PP_ALIGN.LEFT, + color=(15, 45, 65) + ) + else: + rec_title = Substitution( + 'TITLE'+str(recs_in_page), + " ", # Not using empty string because it displays text when empty string is passed + font_size=11, + font='Avenir', + align=PP_ALIGN.LEFT, + color=(15, 45, 65), + bold=True + ) + subs.append(rec_title) + rec_description = Substitution( + 'DESC'+str(recs_in_page), + " ", # Not using empty string because it displays text when empty string is passed + font_size=10, + font='Avenir', + align=PP_ALIGN.LEFT, + color=(15, 45, 65) + ) subs.append(rec_description) recs_in_page += 1 recommendations_inserted += 1 - # Add substitutions to 1 slide at a time + # Add substitutions a slide at a time recommentation_slide.add_substitutions(subs) + recommendations_slide_index += 1 # Save PPT to a temporary file, and pass a reference to BlocLink tmp_dir = '/tmp/bpeng/{}'.format(uuid.uuid4()) diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index d00df63..9515e5a 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -6,7 +6,7 @@ def test_report_generation_1_rec(): {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, ] - report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + report = generate_pns_report(building_info, recommendation_list) print(report) assert 0 == 1 @@ -18,7 +18,7 @@ def test_report_generation_2_recs(): {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, ] - report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + report = generate_pns_report(building_info, recommendation_list) print(report) assert 0 == 1 @@ -32,7 +32,7 @@ def test_report_generation_4_recs(): {'ecm_name':'LED Lighting', 'short_description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, ] - report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + report = generate_pns_report(building_info, recommendation_list) print(report) assert 0 == 1 @@ -54,7 +54,7 @@ def test_report_generation_11_recs(): {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, ] - report = generate_pns_report('./dev-templates/PNS_NonNYC_Template.pptx', building_info, recommendation_list) + report = generate_pns_report(building_info, recommendation_list) print(report) assert 0 == 1 -- GitLab From 98b7850692729bffba6d94b70581996feb0697fa Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Fri, 3 Jan 2020 16:15:32 -0800 Subject: [PATCH 20/23] Use environment variables for Oakland template paths. --- bpeng/bis/report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 027f46b..10888be 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -60,7 +60,8 @@ def generate_pns_report(building_info, recommendations): total_recs = len(recommendations) num_recommendation_slides = math.ceil(float(total_recs)/recs_per_slide) # Select Template Based on the number of recommendations with a max of 12 recommendations - report = TemplateInstantiator('./bpeng/bis/templates/PNS_NonNYC_Template_' + str(num_recommendation_slides) + '.pptx') + template_path = os.environ['NON_NYC_PPTX_TEMPLATE_' + str(num_recommendation_slides)] + report = TemplateInstantiator(template_path) # Provide template with address, date for title page address = building_info['address'] addr_sub = Substitution( -- GitLab From f7ec21f87ededae5b4f6b1d61d23b3f5bc8bd35b Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 6 Jan 2020 09:59:59 -0800 Subject: [PATCH 21/23] Removing format bool function that is not used and not correct. --- bpeng/bis/field_format.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bpeng/bis/field_format.py b/bpeng/bis/field_format.py index 9fcbd38..f1c5cc0 100644 --- a/bpeng/bis/field_format.py +++ b/bpeng/bis/field_format.py @@ -6,11 +6,5 @@ def format_usd(val): """ return '${:,}'.format(val) -def format_bool(val): - if b: - return 'Yes' - else: - return 'No' - def format_date(val): return val.strftime("%m/%d/%Y") -- GitLab From f93c074ed2e8b0c2d38c543e3ef0d7e907ddcfff Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 6 Jan 2020 10:01:14 -0800 Subject: [PATCH 22/23] Remove loop used during development to test. --- bpeng/bis/report.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bpeng/bis/report.py b/bpeng/bis/report.py index 10888be..369ecaf 100644 --- a/bpeng/bis/report.py +++ b/bpeng/bis/report.py @@ -147,10 +147,5 @@ def generate_pns_report(building_info, recommendations): save_to = '{}/BIS_{}.pptx'.format(tmp_dir, escaped_addr) if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) - - # Trying to figure out why pptx gets broken after inserting new slide - for slide in report.presentation.slides: - print(slide.name) - print(slide.slide_id) report.export().save(save_to) return save_to -- GitLab From 7afdd4e4511838b4b6c422ca49178f5339f78f51 Mon Sep 17 00:00:00 2001 From: Jose Contreras Date: Mon, 6 Jan 2020 10:21:08 -0800 Subject: [PATCH 23/23] Add TODOs to report generation testing and template rendering. --- bpeng/bis/template.py | 3 ++- tests/bis/test_report_generation.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bpeng/bis/template.py b/bpeng/bis/template.py index f59dcc0..508941c 100644 --- a/bpeng/bis/template.py +++ b/bpeng/bis/template.py @@ -8,6 +8,7 @@ from pptx.util import Inches, Pt from .field_format import * +# TODO: Review this and merge with python file in PNA directory `../pna/template.py` class TemplateInstantiator(): """ Creates a PNA report from a template for non NYC buildings @@ -26,7 +27,7 @@ class TemplateInstantiator(): index = 0 for slide in self.presentation.slides: slide = self.slideInstantiator[index].render(slide) - index = index+1 + index += 1 return self.presentation diff --git a/tests/bis/test_report_generation.py b/tests/bis/test_report_generation.py index 9515e5a..ec6767c 100644 --- a/tests/bis/test_report_generation.py +++ b/tests/bis/test_report_generation.py @@ -4,7 +4,7 @@ def test_report_generation_1_rec(): building_info = {'address':'2148 Broadway, Oakland, CA 94612'} recommendation_list = [ {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, - ] + ] # TODO: Load data from json and create fixtures report = generate_pns_report(building_info, recommendation_list) @@ -16,7 +16,7 @@ def test_report_generation_2_recs(): recommendation_list = [ {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, - ] + ] # TODO: Load data from json and create fixtures report = generate_pns_report(building_info, recommendation_list) @@ -30,7 +30,7 @@ def test_report_generation_4_recs(): {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, {'ecm_name':'LED Lighting', 'short_description': 'Replace any existing incandescent /fluorescent/halogen light bulbs with LEDs. LEDs use up to 70 percent less energy than incandescent bulbs and last longer than conventional lighting.'}, - ] + ] # TODO: Load data from json and create fixtures report = generate_pns_report(building_info, recommendation_list) @@ -52,7 +52,7 @@ def test_report_generation_11_recs(): {'ecm_name':'Heat Pump Water Heater', 'short_description': 'Install a Heat Pump Water Heater (HPWH) in replacement of an outdated electric water heater. HPWHs are more than three times more efficient than electric resistance water heaters. They are an easy retrofit, simple to operate, and they save energy and money every day.'}, {'ecm_name':'Smart DHW Recirculation Pumps', 'short_description': 'Install smart hot water recirculation pumps to precisely control the availability of domestic hot water at showerheads and sink taps. Using a smart hot water recirculation pump provides fully automated and temperature-controlled operation, reduces electric consumption (as a an existing circulation pump retrofit) and can prevent thousands of gallons of water from flowing down the drain as it warms.'}, {'ecm_name':'Smart Thermostat', 'short_description': "Install a smart thermostat to control heating and cooling system either centrally or room by room. Smart thermostats provide accurate temperature control for conditioned spaces, remote access to setpoints and ambiant temperatures measurements, cultivate people's energy-friendly habits, and save energy and money on utility bills."}, - ] + ] # TODO: Load data from json and create fixtures report = generate_pns_report(building_info, recommendation_list) -- GitLab