diff --git a/Procfile b/Procfile index 1a74262..a5dd072 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: python helix/main.py +web: gunicorn -c gunicorn_config.py --pythonpath helix main:app diff --git a/README.md b/README.md index bea73d8..3df90cf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![CircleCI](https://circleci.com/gh/SunPower/Helix_Roof_Calculator.svg?style=svg&circle-token=aacf2ae59dae99075992ed10d1e27f119e223e75)](https://circleci.com/gh/SunPower/Helix_Roof_Calculator) + + ## Helix Calculator - [Staging](https://sp-helix-staging.herokuapp.com) @@ -31,6 +34,22 @@ | google-chrome-stable/stable,now 62.0.3202.94-1 amd64 | | nodejs/unknown,now 6.12.0-1nodesource1 amd64 | + +##### Environment Variables + +Set the environment variables for your specific environment. Use the `.env` file below as reference: + +``` +AWS_S3_BUCKET="..." +AWS_ACCESS_KEY_ID="..." +AWS_SECRET_ACCESS_KEY="..." + +SF_BASE_URL="https://test.salesforce.com" +SFDC_ACCESS_KEY_ID="..." +SFDC_SECRET_ACCESS_KEY="..." +``` + + ##### Set up for Develop and Testing - Run the docker container for the 1st time @@ -47,7 +66,7 @@ - Log in the docker container -```docker exec -t -i helix /bin/bash``` +```docker exec -ti helix bash``` - Run this commands inside the docker conatiner @@ -367,3 +386,9 @@ docker exec -it helixroofcalculator_helix_1 invoke db_migrate ``` Debugging is possible directly from PyCharm, all code changes are transparent between host and container. + + +## Database schema + + +![Database Schema](documentation/db_schema.png) diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 46d87b6..0000000 --- a/circle.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: ivannnn/heroku_cedar14:2.0 - environment: - PGUSER: pivotal - PGPASSWORD: password - PASSWORD: password - environment: - PATH: /usr/local/rvm/gems/ruby-2.3.4/bin:/usr/local/rvm/gems/ruby-2.3.4@global/bin:/usr/local/rvm/rubies/ruby-2.3.4/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/rvm/bin - steps: - - checkout - - run: - name: Set up and Run tests - command: | - source /etc/profile.d/rvm.sh - rvm use 2.3.4 - /etc/init.d/postgresql start - python3.4 -m venv env - source env/bin/activate - pip install invoke - invoke install - npm install - invoke db_migrate - invoke test_ci - - deploy: - name: staging - command: | - invoke update_heroku_version staging - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" - git push -f git@heroku.com:sp-helix-staging.git $CIRCLE_SHA1:refs/heads/master - heroku run --app sp-helix-staging invoke db_migrate - rake ci:deliver - name: preprod - command: | - invoke update_heroku_version preprod - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" - git push -f git@heroku.com:sp-helix-preprod.git $CIRCLE_SHA1:refs/heads/master - heroku run --app sp-helix-preprod invoke db_migrate - rake ci:deliver - name: production - commands: | - invoke update_heroku_version production - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" - git push -f git@heroku.com:sp-helix-production.git $CIRCLE_SHA1:refs/heads/master - heroku run --app sp-helix-production invoke db_migrate - rake ci:deliver - notify: - webhooks: - - url: http://pulse.pivotallabs.com/projects/03ba990f-b8f5-4508-b4c1-19038b2cb791/status diff --git a/documentation/classes.calculators-diagram.png b/documentation/classes.calculators-diagram.png new file mode 100644 index 0000000..1198ce7 Binary files /dev/null and b/documentation/classes.calculators-diagram.png differ diff --git a/documentation/classes.constants-diagram.png b/documentation/classes.constants-diagram.png new file mode 100644 index 0000000..4c85aae Binary files /dev/null and b/documentation/classes.constants-diagram.png differ diff --git a/documentation/classes.forms-diagram.png b/documentation/classes.forms-diagram.png new file mode 100644 index 0000000..0c4d5ab Binary files /dev/null and b/documentation/classes.forms-diagram.png differ diff --git a/documentation/classes.models-diagram.png b/documentation/classes.models-diagram.png new file mode 100644 index 0000000..89808ae Binary files /dev/null and b/documentation/classes.models-diagram.png differ diff --git a/documentation/classes.validators-diagram.png b/documentation/classes.validators-diagram.png new file mode 100644 index 0000000..9cce803 Binary files /dev/null and b/documentation/classes.validators-diagram.png differ diff --git a/documentation/db_schema.png b/documentation/db_schema.png new file mode 100644 index 0000000..249f5b3 Binary files /dev/null and b/documentation/db_schema.png differ diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000..63eded5 Binary files /dev/null and b/dump.rdb differ diff --git a/gunicorn_config.py b/gunicorn_config.py new file mode 100644 index 0000000..3027a05 --- /dev/null +++ b/gunicorn_config.py @@ -0,0 +1,13 @@ +"""gunicorn WSGI server configuration.""" +import multiprocessing +import os + + +bind = '0.0.0.0:' + os.getenv('PORT', '5000') +max_requests = 1000 +worker_class = 'meinheld.gmeinheld.MeinheldWorker' +workers = multiprocessing.cpu_count() +timeout = os.getenv('TIMEOUT', 120) +graceful_timeout = os.getenv('TIMEOUT', 120) +keepalive = 5 +daemon = False diff --git a/helix/Services/dxf_helper.py b/helix/Services/dxf_helper.py index 4b5e021..ed8df8a 100644 --- a/helix/Services/dxf_helper.py +++ b/helix/Services/dxf_helper.py @@ -234,24 +234,43 @@ class DXFHelper(object): node_store.add_node(node) + # Optimization: avoid creating thousands of replicated objects + graph_directions_cache = {} + for x in (0, 1, -1): + for y in (0, 1, -1): + if x == y == 0: + continue + graph_directions_cache[(x, y)] = GraphDirection((x, y)) + for node in nodes: if len(node.neighboring_nodes()) == 8: continue + + rotation = math.radians(node.coordinate.rotation) + # Optimization: Avoid replicate calculations thousands of times + x_spacing_cos_rotation = (node.x_spacing * math.cos(rotation)) + y_spacing_sin_rotation = (node.y_spacing * math.sin(rotation)) + x_spacing_sin_rotation = (node.x_spacing * math.sin(rotation)) + y_spacing_cos_rotation = (node.y_spacing * math.cos(rotation)) + for x in (0, 1, -1): for y in (0, 1, -1): if x == y == 0: continue - rotation = math.radians(node.coordinate.rotation) - x_spacing = (x * node.x_spacing * math.cos(rotation)) - (y * node.y_spacing * math.sin(rotation)) - y_spacing = (x * node.x_spacing * math.sin(rotation)) + (y * node.y_spacing * math.cos(rotation)) + x_spacing = (x * x_spacing_cos_rotation) - (y * y_spacing_sin_rotation) + y_spacing = (x * x_spacing_sin_rotation) + (y * y_spacing_cos_rotation) coordinate = Coordinate(node.coordinate.x + x_spacing, node.coordinate.y + y_spacing, node.coordinate.rotation) if coordinate.x < 0 or coordinate.y < 0: continue - direction = GraphDirection((x, y)) + + # Optimization for `direction = GraphDirection((x, y))` + direction = graph_directions_cache[(x, y)] if node.has_existing_neighbor(direction): continue + # FIXME: This is the bottleneck of the loop + # Calling this ~10000 times needs ~20 seconds neighbor = node_store.find_coordinate(coordinate) if neighbor: node.add_neighbor(neighbor, direction) @@ -417,7 +436,7 @@ class DXFHelper(object): def __compute_segment_direction(p1, p2): """ Computes direction of a segment. Points taken from building outline are assumed to be in counterclockwise order. - + :param p1: first point :param p2: second point :return: tuple representing orientation ('north', 'south', 'east', 'west') and variation in degrees from ideally @@ -438,7 +457,7 @@ class DXFHelper(object): def compute_corner_directions(vertex, prev, next, angle_correction): """ Determines if point is located in north/east corner - + :param vertex: point located in the corner :param prev: point previous to vertex, assuming counterclockwise order :param next: point next to vertex, assuming counterclockwise order @@ -510,7 +529,7 @@ class DXFHelper(object): @staticmethod def __generate_wind_zone__(buildings, scaling_factor, points_callback, panel_orientation): """ - Important: polygons representing buildings are expected to have points in counterclockwise order + Important: polygons representing buildings are expected to have points in counterclockwise order """ wind_zones = [] diff --git a/helix/Services/dxf_service.py b/helix/Services/dxf_service.py index 6c3c1a0..fbb6322 100644 --- a/helix/Services/dxf_service.py +++ b/helix/Services/dxf_service.py @@ -2,9 +2,6 @@ import io import dxfgrabber -from helix.constants.file_validation_error import FileValidationMessage -from helix.models.dxf.dxf_error import OldDxfFormatException - class DXFService(object): """ @@ -51,6 +48,7 @@ class DXFService(object): panels = dxf_helper.generate_panels(modules, translated_modules) + # FIXME: Building a graph with many entities is very slow node_graph = dxf_helper.build_node_graph(panels, module_constants.panel_spacing) subarrays = dxf_helper.detect_subarrays(node_graph, panels) for subarray in subarrays: diff --git a/helix/Services/s3_helper.py b/helix/Services/s3_helper.py new file mode 100644 index 0000000..26a8bc1 --- /dev/null +++ b/helix/Services/s3_helper.py @@ -0,0 +1,26 @@ +import boto3 +import os +import uuid + + +def s3_upload(bytes_or_file_like, filename=None, file_extension=None): + ''' + @bytes_or_file_like: bytes(), open('filepath') or io.StringIO('') + ''' + if filename is None: + filename = uuid.uuid4().hex + if file_extension: + filename += file_extension + + s3 = boto3.resource('s3', + aws_access_key_id=os.getenv('AWS_ACCESS_KEY_ID'), + aws_secret_access_key=os.getenv('AWS_SECRET_ACCESS_KEY')) + + # Default: test environment + bucket_name = os.getenv('AWS_S3_BUCKET', 'sunpower-test-dgplatform-spectrum') + + # Assuming bucket already exists + s3.Bucket(bucket_name).put_object(Key=filename, Body=bytes_or_file_like.read(), ACL='public-read') + file_url = 'https://s3.amazonaws.com/{}/{}'.format(bucket_name, filename) + print('Uploaded filename {} to S3: {}'.format(filename, file_url)) + return file_url diff --git a/helix/api/api.py b/helix/api/api.py index cbe4b2e..edc94df 100644 --- a/helix/api/api.py +++ b/helix/api/api.py @@ -5,7 +5,6 @@ from helix.presenters.panel_presenter import ProjectPresenter from helix.session_manager import SessionManager from helix.constants import redis_constant, sql_constant from helix.seismic_validator_user_values import SeismicValidatorUserValues -from helix.validators.file_validator import FileValidator from helix.validators.seismic_anchor_validator import SeismicAnchorValidator api = Blueprint('api', __name__, template_folder='templates') diff --git a/helix/calculators/bom_calculator.py b/helix/calculators/bom_calculator.py index 7102180..80be944 100644 --- a/helix/calculators/bom_calculator.py +++ b/helix/calculators/bom_calculator.py @@ -50,7 +50,7 @@ class BomCalculator(object): row_count = sum(subarray.row_count for subarray in self.subarrays) column_count = sum(subarray.column_count for subarray in self.subarrays) parts_list = MechanicalBomCalculator(self.values, self.panels, self.subarrays).mechanical_bom() - ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module)).compute_ebom() + ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module), self.subarrays).compute_ebom() add_parts_to_list(parts_list, ebom_parts_list) diff --git a/helix/calculators/calculator.py b/helix/calculators/calculator.py index dc722fe..666a1a6 100644 --- a/helix/calculators/calculator.py +++ b/helix/calculators/calculator.py @@ -1,4 +1,3 @@ -from math import ceil, floor import copy from helix.Repositories.graph_repository import GraphRepository from helix.calculators.ballast_calculator import BallastCalculator diff --git a/helix/calculators/ebom_calculator.py b/helix/calculators/ebom_calculator.py index e9113af..747a669 100644 --- a/helix/calculators/ebom_calculator.py +++ b/helix/calculators/ebom_calculator.py @@ -6,15 +6,15 @@ from helix.constants.ebom_parts import * from helix.constants.parts import wire_clip_large, cable_support, cable_support_lid, channel_nut, sunshade from helix.constants.system_type import SystemType from helix.constants.inverter_brand import InverterBrand -from helix.forms.ebom_form import InverterBrandForm class EbomCalculator(object): - def __init__(self, user_values, row_count, column_count, modules_count = None): + def __init__(self, user_values, row_count, column_count, modules_count = None, panels = None): self.values = user_values self.row_count = row_count self.column_count = column_count self.modules_count = modules_count + self.panels = panels def resolve_power_monitor_type(self): module_type = self.values.module_type() @@ -30,6 +30,14 @@ class EbomCalculator(object): else: return monitor_controller_240_v + def resolve_is_delta(self): + if len(self.values.inverter_brands()) > 0: + return self.values.inverter_brands()[0]['inverter_brand_id'] == InverterBrand.DELTA.value + if len(self.values.standalone_inverters()) > 0: + return self.values.standalone_inverters()[0]['model'].get_type == InverterBrand.DELTA.label + # can't determine without data + return False + def compute_ebom(self): part_list = {} @@ -38,25 +46,14 @@ class EbomCalculator(object): monitors = self.values.power_monitors() module_type = self.values.module_type() system_type = self.values.system_type() - - is_delta=None - try: - is_delta=(self.values.inverter_brands()[0]['inverter_brand_id']==InverterBrand.DELTA.value) - except IndexError : - #Some tests are calculating bom without providing inverter brand so inverter_brands is empty - #for those tests, inverter brand is irrelevant - is_delta=False - + + is_delta = self.resolve_is_delta() + inverter_count = 0 total_ac_run_length = 0 panel_board_counts = [0, 0] proper_monitor_controller = self.resolve_power_monitor_type() - try: - is_delta = (self.values.inverter_brands()[0]['inverter_brand_id']==InverterBrand.DELTA.value) - except IndexError: - is_delta = False - for power_station in power_stations: power_station_count = power_station['power_station_quantity'] total_ac_run_length += power_station['ac_run_length'] @@ -94,12 +91,17 @@ class EbomCalculator(object): if (is_delta): add_parts_to_list(part_list, {ethernet_plug: 2},1) - add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count) + if is_delta: + clips_amount = inverter_count * self.row_count * 1.15 + clips_rounded = int(ceil(clips_amount / 10.0)) * 10 + add_parts_to_list(part_list, {wire_clip_large: clips_rounded}) + else: + add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count) add_parts_to_list(part_list, {stump: 1}, ceil(total_ac_run_length / 4.0)) - cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters)) + cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters), is_delta) add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports) - add_parts_to_list(part_list, {rear_skirt: -1}, ceil(cable_supports*.38)) + add_parts_to_list(part_list, {rear_skirt_1_1: -1}, ceil(cable_supports*.38)) dependent_part_list = {} @@ -121,21 +123,37 @@ class EbomCalculator(object): if inverter['dc_switch']: add_parts_to_list(part_list, dc_switch_parts, multiplier) - def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count): - if sum(panel_board_counts) == 0: + def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count, is_delta): + # There are some tests that don't have panels + if is_delta and self.panels is None: return 0 - - if self.values.system_type() == SystemType.dualTilt: - dimension1 = self.column_count - dimension2 = self.row_count + if is_delta: + panels_count = len(self.panels) + if panels_count == 0: + return 0 + if self.values.system_type() == SystemType.dualTilt: + Avg_Columns = self.column_count / panels_count + Col2Row_Ratio = self.column_count / self.row_count + Col2Row_Ratio = 1 if Col2Row_Ratio < 1 else Col2Row_Ratio + return ceil(standalone_inverter_count * 1.25 * Avg_Columns * Col2Row_Ratio + Avg_Columns) + else: + Avg_Rows = self.row_count / panels_count + Row2Col_Ratio = self.row_count / self.column_count + Row2Col_Ratio = 1 if Row2Col_Ratio < 1 else Row2Col_Ratio + return ceil(standalone_inverter_count * 1.25 * Avg_Rows * Row2Col_Ratio + Avg_Rows) else: - dimension1 = self.row_count - dimension2 = self.column_count - - result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts) - result *= dimension1 * max(dimension1 / dimension2, 1) - result += dimension1 - return ceil(result) + if sum(panel_board_counts) == 0: + return 0 + if self.values.system_type() == SystemType.dualTilt: + dimension1 = self.column_count + dimension2 = self.row_count + else: + dimension1 = self.row_count + dimension2 = self.column_count + result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts) + result *= dimension1 * max(dimension1 / dimension2, 1) + result += dimension1 + return ceil(result) def get_standalone_inverters(self, power_station): standalone_inverters = self.values.standalone_inverters() diff --git a/helix/calculators/mechanical_bom_calculator.py b/helix/calculators/mechanical_bom_calculator.py index c4315d8..015ecaa 100644 --- a/helix/calculators/mechanical_bom_calculator.py +++ b/helix/calculators/mechanical_bom_calculator.py @@ -5,7 +5,7 @@ from helix.calculators.bom_helper import add_parts_to_list, apply_fudge_factors, from helix.calculators.subarray_helper import extract_subarray from helix.constants.module_type import ModuleType from helix.constants.panel_type import PanelType -from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1, leading_tray +from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1 from helix.constants.system_type import SystemType diff --git a/helix/calculators/seismic_calculator.py b/helix/calculators/seismic_calculator.py index c2f4157..42f7681 100644 --- a/helix/calculators/seismic_calculator.py +++ b/helix/calculators/seismic_calculator.py @@ -1,6 +1,5 @@ from math import ceil, floor -from helix.calculators.subarray_graph import SubarrayGraph from helix.calculators.subarray_helper import extract_subarray from helix.constants.global_constants import minimum_racking_capacity diff --git a/helix/calculators/subarray_graph.py b/helix/calculators/subarray_graph.py index c97cf43..b6ac01c 100644 --- a/helix/calculators/subarray_graph.py +++ b/helix/calculators/subarray_graph.py @@ -1,4 +1,3 @@ -import copy from enum import Enum from helix.constants.system_type import SystemType diff --git a/helix/constants/inverter_type.py b/helix/constants/inverter_type.py index eadfcb6..aad8714 100644 --- a/helix/constants/inverter_type.py +++ b/helix/constants/inverter_type.py @@ -7,6 +7,10 @@ class InverterTypeSMA(IntEnum): MODEL_20KW = 6 MODEL_24KW = 8 + @property + def get_type(self): + return "SMA" + @property def default_string(self): return { @@ -49,6 +53,10 @@ class InverterTypeDelta(IntEnum): MODEL_60KW = 11 MODEL_80KW = 12 + @property + def get_type(self): + return "Delta" + @property def label(self): return { diff --git a/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py b/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py index 616f75a..4a0776b 100644 --- a/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py +++ b/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py @@ -60,14 +60,37 @@ class DualTilt128CellConstants(object): return 1, 1 def base_weight(self, panel_type, tray_count): - if panel_type == PanelType.Corner: - return [108.66, 110.96, 112.11, 116.44, 119.62, 122.80, 125.98][tray_count] - elif panel_type == PanelType.NorthSouth: - return [107.58, 109.88, 111.03, 114.21, 117.39, 120.57, 123.75][tray_count] - elif panel_type == PanelType.EastWest: - return [103.19, 105.49, 105.49, 108.67, 111.85, 115.03, 118.21][tray_count] - else: - return [102.11, 104.41, 104.41, 107.59, 110.77, 113.95, 117.13][tray_count] + values_per_panel_type = { + PanelType.Corner: [122.70, + 124.55, + 126.40, + 129.55, + 132.71, + 135.86, + 139.01][tray_count], + PanelType.NorthSouth: [121.63, + 123.48, + 125.33, + 128.48, + 131.64, + 134.79, + 137.94][tray_count], + PanelType.EastWest: [118.28, + 120.13, + 121.99, + 125.14, + 128.29, + 131.45, + 134.60][tray_count], + PanelType.Middle: [117.21, + 119.06, + 120.92, + 124.07, + 127.22, + 130.38, + 133.53][tray_count], + } + return values_per_panel_type.get(panel_type) def link_tray_thresholds(self, panel_type): if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: diff --git a/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py b/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py index a1273db..79c15ac 100644 --- a/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py +++ b/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py @@ -60,23 +60,38 @@ class DualTilt96CellConstants(object): return 1, 1 def base_weight(self, panel_type, tray_count): - if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: - return [92.58, - 94.31, - 96.03, - 98.33, - 100.63, - 102.93, - 105.23][tray_count] - else: - return [87.11, - 88.84, - 89.41, - 91.71, - 94.01, - 96.31, - 98.61][tray_count] - + values_per_panel_type = { + PanelType.Corner: [92.41, + 94.26, + 96.12, + 98.54, + 100.97, + 103.39, + 105.82][tray_count], + PanelType.NorthSouth: [91.63, + 93.48, + 95.33, + 97.76, + 100.18, + 102.61, + 105.03][tray_count], + PanelType.EastWest: [88.00, + 89.85, + 91.70, + 94.13, + 96.55, + 98.98, + 101.40][tray_count], + PanelType.Middle: [87.21, + 89.06, + 90.92, + 93.34, + 95.77, + 98.19, + 100.62][tray_count], + } + return values_per_panel_type.get(panel_type) + def link_tray_thresholds(self, panel_type): if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: return [7.5, 10, 15] diff --git a/helix/constants/module_type_constants/dual_tilt_pseries_constants.py b/helix/constants/module_type_constants/dual_tilt_pseries_constants.py index 647001f..8ca5bba 100644 --- a/helix/constants/module_type_constants/dual_tilt_pseries_constants.py +++ b/helix/constants/module_type_constants/dual_tilt_pseries_constants.py @@ -60,14 +60,37 @@ class DualTiltPSeriesConstants(object): return 1, 1 def base_weight(self, panel_type, tray_count): - if panel_type == PanelType.Corner: - return [103.66, 105.96, 107.11, 111.44, 114.62, 117.80, 120.98][tray_count] - elif panel_type == PanelType.NorthSouth: - return [102.58, 104.88, 106.03, 109.21, 112.39, 115.57, 118.75][tray_count] - elif panel_type == PanelType.EastWest: - return [98.19, 100.49, 100.49, 103.67, 106.85, 110.03, 113.21][tray_count] - else: - return [97.11, 99.41, 99.41, 102.59, 105.77, 108.95, 112.13][tray_count] + values_per_panel_type = { + PanelType.Corner: [116.70, + 118.55, + 120.40, + 123.55, + 126.71, + 129.86, + 133.01][tray_count], + PanelType.NorthSouth: [115.63, + 117.48, + 119.33, + 122.48, + 125.64, + 128.79, + 131.94][tray_count], + PanelType.EastWest: [112.28, + 114.13, + 115.99, + 119.14, + 122.29, + 125.45, + 128.60][tray_count], + PanelType.Middle: [111.21, + 113.06, + 114.92, + 118.07, + 121.22, + 124.38, + 127.53][tray_count], + } + return values_per_panel_type.get(panel_type) def link_tray_thresholds(self, panel_type): if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: diff --git a/helix/constants/module_type_constants/single_tilt_128_cell_constants.py b/helix/constants/module_type_constants/single_tilt_128_cell_constants.py index 2815154..13c03f2 100644 --- a/helix/constants/module_type_constants/single_tilt_128_cell_constants.py +++ b/helix/constants/module_type_constants/single_tilt_128_cell_constants.py @@ -131,14 +131,10 @@ class SingleTilt128CellConstants(object): return 1, 1 def base_weight(self, panel_type, tray_count): - if panel_type == PanelType.Corner: - return [71.91, 71.91, 75.09, 78.27][tray_count] - elif panel_type == PanelType.NorthSouth: - return [65.8, 65.8, 68.98, 72.16][tray_count] - elif panel_type == PanelType.EastWest: - return [69.75, 72.05, 75.23, 78.41][tray_count] - else: - return [65.08, 67.38, 70.56, 73.74][tray_count] + return [[68.02, 68.02, 71.17, 74.32], + [65.05, 65.05, 68.20, 71.35], + [65.87, 67.73, 70.88, 74.03], + [63.26, 65.11, 68.26, 71.41]][panel_type.index()][tray_count] def link_tray_thresholds(self, panel_type): return [[0, 13.0], diff --git a/helix/constants/module_type_constants/single_tilt_96_cell_constants.py b/helix/constants/module_type_constants/single_tilt_96_cell_constants.py index de639fd..944cfb6 100644 --- a/helix/constants/module_type_constants/single_tilt_96_cell_constants.py +++ b/helix/constants/module_type_constants/single_tilt_96_cell_constants.py @@ -130,10 +130,10 @@ class SingleTilt96CellConstants(object): return 1, 1 def base_weight(self, panel_type, tray_count): - return [[54.50, 54.50, 56.80, 59.10], - [49.47, 49.47, 51.77, 54.07], - [53.42, 55.72, 58.02, 60.32], - [48.75, 51.05, 53.35, 55.65]][panel_type.index()][tray_count] + return [[51.10, 51.10, 53.52, 55.95], + [48.13, 48.13, 50.55, 52.98], + [49.24, 51.09, 53.52, 55.94], + [48.33, 50.19, 52.61, 55.04]][panel_type.index()][tray_count] def link_tray_thresholds(self, panel_type): return [[0, 12.0], diff --git a/helix/constants/module_type_constants/single_tilt_pseries_constants.py b/helix/constants/module_type_constants/single_tilt_pseries_constants.py index 0e50141..1fc8778 100644 --- a/helix/constants/module_type_constants/single_tilt_pseries_constants.py +++ b/helix/constants/module_type_constants/single_tilt_pseries_constants.py @@ -130,14 +130,10 @@ class SingleTiltPSeriesConstants(object): return 1, 1 def base_weight(self, panel_type, tray_count): - if panel_type == PanelType.Corner: - return [66.91, 66.91, 70.09, 73.27][tray_count] - elif panel_type == PanelType.NorthSouth: - return [60.8, 60.8, 63.98, 67.16][tray_count] - elif panel_type == PanelType.EastWest: - return [64.75, 67.05, 70.23, 73.41][tray_count] - else: - return [60.08, 62.38, 65.56, 68.74][tray_count] + return [[65.02, 65.02, 68.17, 71.32], + [62.05, 62.05, 65.20, 68.35], + [62.87, 64.73, 67.88, 71.03], + [60.26, 62.11, 65.26, 68.41]][panel_type.index()][tray_count] def link_tray_thresholds(self, panel_type): return [[0, 13.0], diff --git a/helix/constants/single_tilt_parts.py b/helix/constants/single_tilt_parts.py index fe98e1c..f397eba 100644 --- a/helix/constants/single_tilt_parts.py +++ b/helix/constants/single_tilt_parts.py @@ -55,7 +55,7 @@ class SingleTiltParts(object): def module(self, module_type): if module_type == ModuleType.Cell96: - rear_skirt_parts = rear_skirt + rear_skirt_parts = rear_skirt_1_1 spoiler_parts = spoiler else: rear_skirt_parts = rear_skirt_1_1 diff --git a/helix/helpers/camel_case.py b/helix/helpers/camel_case.py new file mode 100644 index 0000000..172e4d4 --- /dev/null +++ b/helix/helpers/camel_case.py @@ -0,0 +1,16 @@ +import re + + +def convert_camel_case_to_snake_case(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +def convert_dict_keys_to_snake_case(a_dict): + new_dict = {} + for old_key in a_dict.keys(): + new_key = convert_camel_case_to_snake_case(old_key) + new_dict[new_key] = a_dict[old_key] + return new_dict + + diff --git a/helix/helpers/nodequadtree.py b/helix/helpers/nodequadtree.py index 290fb7c..44dffce 100644 --- a/helix/helpers/nodequadtree.py +++ b/helix/helpers/nodequadtree.py @@ -130,8 +130,14 @@ class NodeQuadTree(): self.nodeList = toKeep # Return a list of all possible nodes that can be near this point - def retrieve(self, nearPoint): - retNodes = list(self.nodeList) + # Optimization `only_quads_related = True`: + # Avoid replicate a large self.nodeList. Get only the other elements. + # Call this in complement of `self.nodeList` + def retrieve(self, nearPoint, only_quads_related=False): + if only_quads_related: + retNodes = [] + else: + retNodes = self.nodeList[:] # = list(self.nodeList) if self.quads[0] is not None: index = self.getIndex(nearPoint) diff --git a/helix/javascript/array_summary/auto_upload.js b/helix/javascript/array_summary/auto_upload.js index 6e3bd6e..375b3af 100644 --- a/helix/javascript/array_summary/auto_upload.js +++ b/helix/javascript/array_summary/auto_upload.js @@ -1,22 +1,26 @@ let AutoUpload = () => { $("#file_upload").change((e) => { - var ten_megabyte_max_upload = 10000000; - $("#error_container_txt").empty(); - if(e.currentTarget.files[0].size < ten_megabyte_max_upload){ - e.currentTarget.form.submit(); - }else{ - $("#error_container_txt").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); - } + var ten_megabyte_max_upload = 10000000; + $("#error_container_txt").empty(); + if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { + // $('#spinner-panel').show(); + $('#spinner-panel').css('width', '100%'); // Workaround for Safari issue + e.currentTarget.form.submit(); + } else { + $("#error_container_txt").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); + } }); $("#dxf_upload").change((e) => { - var ten_megabyte_max_upload = 10000000; - $("#error_container_dxf").empty(); - if(e.currentTarget.files[0].size < ten_megabyte_max_upload){ - e.currentTarget.form.submit(); - }else{ - $("#error_container_dxf").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); - } + var ten_megabyte_max_upload = 10000000; + $("#error_container_dxf").empty(); + if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { + // $('#spinner-panel').show(); + $('#spinner-panel').css('width', '100%'); // Workaround for Safari issue + e.currentTarget.form.submit(); + } else { + $("#error_container_dxf").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); + } }); }; diff --git a/helix/javascript/array_summary/index.js b/helix/javascript/array_summary/index.js index d80113b..551df09 100644 --- a/helix/javascript/array_summary/index.js +++ b/helix/javascript/array_summary/index.js @@ -8,13 +8,16 @@ import AutoUpload from './auto_upload'; $(document).ready(function () { AutoUpload(); - let subarrayDisplay = new SubarrayDisplay(); - subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); - let arrayVisualization = new ArrayVisualization(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); - arrayVisualization.init(); - new ZoomControl(arrayVisualization).init($('#zoom_control')); - new OverlayControl(arrayVisualization).init($('#overlay_control'), $('#legend_container')); - new SeismicControl(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save")); - window.arrayVisualization = arrayVisualization; + + if (is_csv_available) { + let subarrayDisplay = new SubarrayDisplay(); + subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); + let arrayVisualization = new ArrayVisualization(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); + arrayVisualization.init(); + new ZoomControl(arrayVisualization).init($('#zoom_control')); + new OverlayControl(arrayVisualization).init($('#overlay_control'), $('#legend_container')); + new SeismicControl(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save")); + window.arrayVisualization = arrayVisualization; + } }); diff --git a/helix/json_builder.py b/helix/json_builder.py new file mode 100644 index 0000000..40dbd3a --- /dev/null +++ b/helix/json_builder.py @@ -0,0 +1,16 @@ +try: + import ujson as json +except ImportError: + import json + + +class JsonBuilder: + def build_bom_output(self, rows): + data = [] + headers = ['Part #', 'Description', 'Total'] + for row in rows: + d = {} + for i, value in enumerate(row): + d[headers[i]] = value + data.append(d) + return json.dumps(data) diff --git a/helix/main.py b/helix/main.py index 180883d..f1d65c6 100644 --- a/helix/main.py +++ b/helix/main.py @@ -6,8 +6,10 @@ from flask import Flask, request, make_response, session, render_template, \ redirect, url_for from flask import got_request_exception from flask.ext import assets +from flask_oauthlib.client import OAuth from webassets.filter import get_filter +from helix.sales_force import tasks as sf_tasks from helix.Services.doc_gen_service import DocGenService from helix.Services.dxf_helper import DXFHelper from helix.Services.dxf_service import DXFService @@ -34,6 +36,20 @@ app.register_blueprint(api, url_prefix='/api') app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey') app.config['PROFILE'] = True + +# Sales Force integrations +oauth = OAuth() +SF_BASE_URL = os.getenv('SFDC_BASE_URL', 'https://test.salesforce.com') +sales_force = oauth.remote_app('sales_force', + consumer_key=os.getenv('SFDC_ACCESS_KEY_ID'), + consumer_secret=os.getenv('SFDC_SECRET_ACCESS_KEY'), + base_url=SF_BASE_URL, + request_token_url=None, # OAuth 2 + access_token_method='POST', # Sales Force requirement + access_token_url=SF_BASE_URL + '/services/oauth2/token', + authorize_url=SF_BASE_URL + '/services/oauth2/authorize', +) + assets_env = assets.Environment(app) assets_env.init_app(app) assets_env.load_path = [ @@ -67,6 +83,10 @@ def init_rollbar(): got_request_exception.connect(rollbar.contrib.flask.report_exception, app) +def is_sfdc_session(): + return 'SFID' in session + + @app.route("/") def index(): return redirect(url_for('site_characterization')) @@ -109,6 +129,9 @@ def test_dxf(): # wizard steps @app.route("/site_characterization/", methods=['GET', 'POST']) def site_characterization(): + if is_sfdc_session(): + return redirect('/summary/') + db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) site_info_form = InputForm() @@ -136,6 +159,7 @@ def summary(): context['current_step'] = 2 if context['site_data_available']: + context['project_name'] = session_manager.site.project_name user_values = session_manager.user_values() calculator = Calculator(user_values, calculate_panel_data=False) context['wind_zones'] = user_values.system_type().system_constants().wind_zones @@ -153,6 +177,9 @@ def summary(): else: context['no_proceed'] = True + if is_sfdc_session(): + context['hide_back'] = True + db_session.close() return render_template('site_summary.html.jinja', context=context) @@ -199,6 +226,7 @@ def array_summary(): else: try: module_constants = user_values.module_system_constants() + # FIXME: parsing a file with many entities is very slow dxf_data = DXFService().parse(file_contents, module_constants, user_values.system_type(), @@ -428,6 +456,70 @@ def helix_documentation(): return render_template('helix_documentation.jinja', context=context) +# Sales Force Integration +@app.route('/sales_force_login') +def sales_force_login(): + # To test it locally: https://localhost:8443/sales_force_login?SFID=a3cL00000004QsQIAU + sfid = request.args.get('SFID') + if sfid: + session.clear() + session['SFID'] = sfid + return sales_force.authorize(callback=url_for('sales_force_authorized', _external=True, SFID=sfid), SFID=sfid) + else: + return redirect('/') + + +@app.route('/sales_force_authorized') +def sales_force_authorized(): + next_url = url_for('summary') + + resp = sales_force.authorized_response() + if resp is None: + print('Unable to authenticate to SFDC.') + return redirect(next_url) + + print('New Sales Force - OAuth2 login') + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + session['sales_force_token'] = resp['access_token'] + + data = sf_tasks.get_site_characterization_from_sales_force(session, resp['instance_url']) + if data: + session_manager.save_form_submission(data) + return redirect(next_url) + else: + return sales_force_logout() + + +# FIXME +from flask import jsonify +@app.route("/export-sfdc") +def export_sfdc(): + if not is_sfdc_session(): + return redirect('/') + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + session_id = session_manager.session['id'] + data = sf_tasks.export_to_sfdc(session_id) + db_session.close() + return jsonify(data) + # return redirect('/download') + + +@sales_force.tokengetter +def get_sales_force_token(token=None): + return session.get('sales_force_token') + + +@app.route('/sales_force_logout') +def sales_force_logout(): + session.pop('SFID', None) + session.pop('sales_force_token', None) + session.clear() + return redirect('/') +# End of Sales Force Integration + + @app.template_filter('format_number') def format_number(number): return "{:,g}".format(number) @@ -457,8 +549,13 @@ def enum(): def main(): host = '0.0.0.0' - port = int(os.getenv('PORT', 5000)) - app.run(host=host, port=port, debug=bool(os.getenv("FLASK_DEBUG", False))) + debug = bool(os.getenv("FLASK_DEBUG", False)) + if os.getenv('FLASK_DEBUG_SSL', None): + port = int(os.getenv('PORT', 8443)) + app.run(host=host, port=port, debug=debug, ssl_context='adhoc') + else: + port = int(os.getenv('PORT', 5000)) + app.run(host=host, port=port, debug=debug) @app.route("/fail-test") diff --git a/helix/models/dxf/graph_node_store.py b/helix/models/dxf/graph_node_store.py index bd5b9c5..d947fa8 100644 --- a/helix/models/dxf/graph_node_store.py +++ b/helix/models/dxf/graph_node_store.py @@ -25,6 +25,7 @@ class GraphNodeStore(list): dy = node.coordinate.y - coordinate.y return dx * dx + dy * dy + # FIXME: This is slow if called thousands of times. Do not add any overhead in this method. def find_coordinate(self, coordinate): # create and populate the quadtree on first request if self.quadTree is None: @@ -33,9 +34,15 @@ class GraphNodeStore(list): self.quadTree.insert(node) del self[:] - possibilities = self.quadTree.retrieve(coordinate) + variance_square = self.variance ** 2 + # Optimization: avoid creating a copy of the big list `self.quadTree.nodeList` + # Old: possibilities = self.quadTree.retrieve(coordinate) + possibilities = self.quadTree.nodeList for node in possibilities: - if self.distance_squared(node, coordinate) <= self.variance ** 2: + if self.distance_squared(node, coordinate) <= variance_square: return node - else: - return None + more_possibilities = self.quadTree.retrieve(coordinate, only_quads_related=True) + for node in more_possibilities: + if self.distance_squared(node, coordinate) <= variance_square: + return node + return None diff --git a/helix/presenters/panel_presenter.py b/helix/presenters/panel_presenter.py index c601500..d328498 100644 --- a/helix/presenters/panel_presenter.py +++ b/helix/presenters/panel_presenter.py @@ -1,6 +1,3 @@ -import math - -from helix.models.coordinate import Coordinate import sys class ProjectPresenter(object): diff --git a/helix/sales_force/__init__.py b/helix/sales_force/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/sales_force/tasks.py b/helix/sales_force/tasks.py new file mode 100644 index 0000000..e66f004 --- /dev/null +++ b/helix/sales_force/tasks.py @@ -0,0 +1,118 @@ +import io +import uuid + +import requests + +from helix.calculators.calculator import Calculator +from helix.constants import redis_constant, sql_constant +from helix.constants.system_type import SystemType +from helix.csv_builder import CsvBuilder +from helix.doc_gen_params_builder import DocGenParamsBuilder +from helix.helpers.camel_case import convert_dict_keys_to_snake_case +from helix.json_builder import JsonBuilder +from helix.presenters.image_presenter import ImagePresenter +from helix.Services.doc_gen_service import DocGenService +from helix.Services.s3_helper import s3_upload +from helix.session_manager import SessionManager + + +def get_site_characterization_from_sales_force(session, base_url): + ''' + @base_url: Avoid URL_NOT_RESET errors + ''' + access_token = session['sales_force_token'] + sfid = session['SFID'] + helix_id = session['id'] + url = base_url + '/services/apexrest/v1/HelixRoofDetails' + headers = {'Authorization': 'Bearer {}'.format(access_token)} + result = requests.get(url, headers=headers, params={'SFID': sfid, 'helix_session_id': helix_id}) + if result.status_code == 200: + data = result.json() + if data: + data = convert_sales_force_data_format_to_helix(data) + return data + else: + print('Error while getting data from Sales Force: {}'.format(result.status_code)) + print(result.content) + + +def convert_sales_force_data_format_to_helix(data): + data = convert_dict_keys_to_snake_case(data) + + if data['system_type'] == 'Single-Tilt': + data['system_type'] = SystemType.singleTilt.value + elif data['system_type'] == 'Dual-Tilt': + data['system_type'] = SystemType.dualTilt.value + + data['ballast_block_weight'] = data['ballast_weight'] + data['max_system_pressure'] = data['max_psf'] = data['system_pressure'] + return data + # data['spectral_response_acceleration'] + + +def export_to_sfdc(session_id): + step = 'Exporting to SFDC' + try: + # 1. Load User Values + step = 'Loading User Values' + session = {'id': session_id} + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_values = session_manager.user_values() + calculator = Calculator(user_values) + + # 2. Generate BOM CSV file + step = 'Generating BOM' + bom = calculator.compute_bom() + csv_file = CsvBuilder().build_bom_output(bom) + # 2.1 Generate BOM CSV file + step = 'Generating BOM as JSON' + json_str = JsonBuilder().build_bom_output(bom) + + # 3. Generate DOCUMENTATION PDF file + step = 'Generating Documentation' + image_presenter = ImagePresenter(user_values.system_type(), user_values.module_type()) + doc_gen_service = DocGenService(requests, DocGenParamsBuilder(user_values, user_values.system_type(), calculator, image_presenter)) + document = doc_gen_service.generate() # Call external service + + # 4. Get Uploaded DXF file in the Helix system + step = 'Loading uploaded DXF' + dxf_contents = session_manager.site.dxf_file or session_manager.site.cad_file + # dxf_filename = session_manager.site.dxf_file_name + + # 5. Save CSV/PDF/DXF files into AWS-S3 + step = 'Uploading to S3' + filename = uuid.uuid4().hex + bom_csv_url = s3_upload(io.StringIO(csv_file), filename=filename + '.csv') + bom_json_url = s3_upload(io.StringIO(json_str), filename=filename + '.json') + doc_url = s3_upload(io.BytesIO(document), filename=filename + '.pdf') + if dxf_contents: # Optional + dxf_url = s3_upload(io.StringIO(dxf_contents), filename=filename + '.dxf') + else: + dxf_url = None + + # 6. Notify SFDC system with an API request + step = 'Notifying SFDC' + SFDC_API_URL = 'https://localhost:8443/' # FIXME + data = { + 'dxf_url': dxf_url, + 'bom_csv_url': bom_csv_url, + 'bom_json_url': bom_json_url, + 'documentation_url': doc_url, + } + print(data) + # result = requests.post(SFDC_API_URL, data=data, timeout=30) + + # 7. Internal logs + # if result.status_code != 200: # FIXME + # print('') + # else: + # print('') + + db_session.close() + return data + # return result.status_code + except Exception as e: + msg = 'Error while {} for session {}'.format(step, session_id) + print(msg) + raise e diff --git a/helix/static/css/animation.css b/helix/static/css/animation.css new file mode 100755 index 0000000..ac5a956 --- /dev/null +++ b/helix/static/css/animation.css @@ -0,0 +1,85 @@ +/* + Animation example, for spinners +*/ +.animate-spin { + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + -webkit-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; + display: inline-block; +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-webkit-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-o-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@-ms-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -moz-transform: rotate(359deg); + -o-transform: rotate(359deg); + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/helix/static/css/fontello.css b/helix/static/css/fontello.css index 92ce5c5..fa49e84 100755 --- a/helix/static/css/fontello.css +++ b/helix/static/css/fontello.css @@ -1,11 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?10976371'); - src: url('../font/fontello.eot?10976371#iefix') format('embedded-opentype'), - url('../font/fontello.woff2?10976371') format('woff2'), - url('../font/fontello.woff?10976371') format('woff'), - url('../font/fontello.ttf?10976371') format('truetype'), - url('../font/fontello.svg?10976371#fontello') format('svg'); + src: url('../font/fontello.eot?24283821'); + src: url('../font/fontello.eot?24283821#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?24283821') format('woff2'), + url('../font/fontello.woff?24283821') format('woff'), + url('../font/fontello.ttf?24283821') format('truetype'), + url('../font/fontello.svg?24283821#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -15,7 +15,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?10976371#fontello') format('svg'); + src: url('../font/fontello.svg?24283821#fontello') format('svg'); } } */ @@ -69,4 +69,5 @@ .icon-info:before { content: '\e80b'; } /* '' */ .icon-close:before { content: '\e80c'; } /* '' */ .icon-sunpower-logo:before { content: '\e80d'; } /* '' */ -.icon-upload-cloud:before { content: '\e80e'; } /* '' */ \ No newline at end of file +.icon-upload-cloud:before { content: '\e80e'; } /* '' */ +.icon-spin6:before { content: '\e839'; } /* '' */ \ No newline at end of file diff --git a/helix/static/css/main.css b/helix/static/css/main.css index 258f70e..1c52064 100644 --- a/helix/static/css/main.css +++ b/helix/static/css/main.css @@ -1023,3 +1023,21 @@ table .right_border_cell { margin-bottom: 25px; } +.spinner-panel { + position: fixed; + margin: 0 auto; + top: 0; + left: 0; + width: 0; /* It will be updated to 100% in JS. Workaround for Safari issue with display:none; */ + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + z-index: 1; + + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: center; + color: #fff; + font-weight: bold; + font-size: 24px; +} diff --git a/helix/static/font/fontello.eot b/helix/static/font/fontello.eot index 7e6f481..9c5e2d9 100755 Binary files a/helix/static/font/fontello.eot and b/helix/static/font/fontello.eot differ diff --git a/helix/static/font/fontello.svg b/helix/static/font/fontello.svg index b486eee..0111048 100755 --- a/helix/static/font/fontello.svg +++ b/helix/static/font/fontello.svg @@ -1,7 +1,7 @@ -Copyright (C) 2016 by original authors @ fontello.com +Copyright (C) 2017 by original authors @ fontello.com @@ -35,6 +35,8 @@ + + \ No newline at end of file diff --git a/helix/static/font/fontello.ttf b/helix/static/font/fontello.ttf index 7ffe2a9..4d81b60 100755 Binary files a/helix/static/font/fontello.ttf and b/helix/static/font/fontello.ttf differ diff --git a/helix/static/font/fontello.woff b/helix/static/font/fontello.woff index ed3436e..8823e8b 100755 Binary files a/helix/static/font/fontello.woff and b/helix/static/font/fontello.woff differ diff --git a/helix/static/font/fontello.woff2 b/helix/static/font/fontello.woff2 index 65bfcc8..c0e2473 100755 Binary files a/helix/static/font/fontello.woff2 and b/helix/static/font/fontello.woff2 differ diff --git a/helix/static/javascripts/array_summary_bundle.js b/helix/static/javascripts/array_summary_bundle.js index 639096c..7e406ae 100644 --- a/helix/static/javascripts/array_summary_bundle.js +++ b/helix/static/javascripts/array_summary_bundle.js @@ -73,15 +73,18 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } $(document).ready(function () { - (0, _auto_upload2.default)(); - var subarrayDisplay = new _subarray_display2.default(); - subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); - var arrayVisualization = new _array_visualization2.default(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); - arrayVisualization.init(); - new _zoom_control2.default(arrayVisualization).init($('#zoom_control')); - new _overlay_control2.default(arrayVisualization).init($('#overlay_control'), $('#legend_container')); - new _seismic_control2.default(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save")); - window.arrayVisualization = arrayVisualization; + (0, _auto_upload2.default)(); + + if (is_csv_available) { + var subarrayDisplay = new _subarray_display2.default(); + subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); + var arrayVisualization = new _array_visualization2.default(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); + arrayVisualization.init(); + new _zoom_control2.default(arrayVisualization).init($('#zoom_control')); + new _overlay_control2.default(arrayVisualization).init($('#overlay_control'), $('#legend_container')); + new _seismic_control2.default(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save")); + window.arrayVisualization = arrayVisualization; + } }); /***/ }), @@ -24730,28 +24733,32 @@ "use strict"; Object.defineProperty(exports, "__esModule", { - value: true + value: true }); var AutoUpload = function AutoUpload() { - $("#file_upload").change(function (e) { - var ten_megabyte_max_upload = 10000000; - $("#error_container_txt").empty(); - if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { - e.currentTarget.form.submit(); - } else { - $("#error_container_txt").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); - } - }); + $("#file_upload").change(function (e) { + var ten_megabyte_max_upload = 10000000; + $("#error_container_txt").empty(); + if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { + // $('#spinner-panel').show(); + $('#spinner-panel').css('width', '100%'); // Workaround for Safari issue + e.currentTarget.form.submit(); + } else { + $("#error_container_txt").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); + } + }); - $("#dxf_upload").change(function (e) { - var ten_megabyte_max_upload = 10000000; - $("#error_container_dxf").empty(); - if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { - e.currentTarget.form.submit(); - } else { - $("#error_container_dxf").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); - } - }); + $("#dxf_upload").change(function (e) { + var ten_megabyte_max_upload = 10000000; + $("#error_container_dxf").empty(); + if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { + // $('#spinner-panel').show(); + $('#spinner-panel').css('width', '100%'); // Workaround for Safari issue + e.currentTarget.form.submit(); + } else { + $("#error_container_dxf").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); + } + }); }; exports.default = AutoUpload; diff --git a/helix/templates/array_summary.html.jinja b/helix/templates/array_summary.html.jinja index 242163f..b7d82e9 100644 --- a/helix/templates/array_summary.html.jinja +++ b/helix/templates/array_summary.html.jinja @@ -1,6 +1,15 @@ {% extends "layout.html.jinja" %} {% set title = "Helix Calculator" %} {% block contents %} + + +
+

Uploading files. Please wait, this may take a while.

+ +
+ {% if not context['csv_available'] %}
{{ form.csrf_token }} diff --git a/helix/templates/download.html.jinja b/helix/templates/download.html.jinja index 347ae8a..568c808 100644 --- a/helix/templates/download.html.jinja +++ b/helix/templates/download.html.jinja @@ -1,6 +1,10 @@ {% extends "layout.html.jinja" %} {% set title = "Helix Calculator" %} {% block contents %} + {% for warning in context['warning_messages'] %} +
{{ warning.value }}
+ {% endfor %} +

Download

@@ -20,6 +24,14 @@ + + {% if 'SFID' in session %} +
+ + + +
+ {% endif %} {% else %} Please complete previous steps first! {% endif %} diff --git a/helix/templates/layout.html.jinja b/helix/templates/layout.html.jinja index c2f9540..92371f5 100644 --- a/helix/templates/layout.html.jinja +++ b/helix/templates/layout.html.jinja @@ -8,6 +8,7 @@ + diff --git a/helix/templates/navigation_buttons.html.jinja b/helix/templates/navigation_buttons.html.jinja index 027bade..d28c984 100644 --- a/helix/templates/navigation_buttons.html.jinja +++ b/helix/templates/navigation_buttons.html.jinja @@ -1,5 +1,9 @@