diff --git a/helix/calculators/ebom_calculator.py b/helix/calculators/ebom_calculator.py index 747a669..afe7a1a 100644 --- a/helix/calculators/ebom_calculator.py +++ b/helix/calculators/ebom_calculator.py @@ -88,8 +88,6 @@ class EbomCalculator(object): for monitor in monitors: if monitor['power_source'][0] == 'Switch Gear/External': add_parts_to_list(part_list, {proper_monitor_controller: 1}, 1) - if (is_delta): - add_parts_to_list(part_list, {ethernet_plug: 2},1) if is_delta: clips_amount = inverter_count * self.row_count * 1.15 diff --git a/helix/calculators/seismic_calculator.py b/helix/calculators/seismic_calculator.py index 42f7681..90f47df 100644 --- a/helix/calculators/seismic_calculator.py +++ b/helix/calculators/seismic_calculator.py @@ -4,9 +4,11 @@ from helix.calculators.subarray_helper import extract_subarray from helix.constants.global_constants import minimum_racking_capacity from helix.constants.panel_type import PanelType +from helix.constants.file_validation_error import FileValidationMessage,FileValidationException from helix.models.subarray import Subarray + class SeismicCalculator(object): def __init__(self, values, graph_repository): self.values = values @@ -37,13 +39,20 @@ class SeismicCalculator(object): more_anchors_needed = True perimeter_covered = sds < 1.0 anchor_threshold = 0 + was_rung_empty = False while more_anchors_needed: rung = graph.pop_rung() interval = int(self.seismic_anchor_interval()) nodes_since_last_anchor = interval if len(rung) == 0: + if was_rung_empty: + # detected an infinite loop + # something is wrong with the input file + # probably panels overlapping + raise FileValidationException(FileValidationMessage.PanelsTooClose.value) graph.reset() anchor_threshold += 1 + was_rung_empty = True continue while more_anchors_needed and interval >= 0: for node in rung: diff --git a/helix/constants/file_validation_error.py b/helix/constants/file_validation_error.py index 339ff95..f1658c7 100644 --- a/helix/constants/file_validation_error.py +++ b/helix/constants/file_validation_error.py @@ -49,3 +49,7 @@ class FileValidationError(object): if self.__class__ != other.__class__: return False return self.row_number == other.row_number and self.validation_message == other.validation_message + +class FileValidationException(Exception): + def __init__(self, message): + self.message = message diff --git a/helix/main.py b/helix/main.py index f1d65c6..112e57e 100644 --- a/helix/main.py +++ b/helix/main.py @@ -1,9 +1,10 @@ import os import requests +from urllib.parse import urlparse import rollbar import rollbar.contrib.flask from flask import Flask, request, make_response, session, render_template, \ - redirect, url_for + redirect, url_for, jsonify from flask import got_request_exception from flask.ext import assets from flask_oauthlib.client import OAuth @@ -16,6 +17,7 @@ from helix.Services.dxf_service import DXFService from helix.api.api import api from helix.calculators.calculator import Calculator from helix.constants import redis_constant, sql_constant +from helix.constants.file_validation_error import FileValidationError, FileValidationException from helix.constants.inverter_type import InverterType from helix.constants.system_type import SystemType from helix.csv_builder import CsvBuilder @@ -184,6 +186,44 @@ def summary(): return render_template('site_summary.html.jinja', context=context) +def handle_dxf_file(session_manager, file_contents, filename=None): + errors = [] + user_values = session_manager.user_values() + validator = FileValidator(user_values) + calculator = Calculator(user_values, calculate_panel_data=False) + + extension = os.path.splitext(filename)[1] or 'dxf' + extension = extension.split('.')[-1] + + validation_error = validator.validate(file_contents, FileType.AuroraDxf, extension=extension) + if validation_error: + error_msg = validation_error.format_error_message() + errors.append(error_msg) + 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(), + calculator.L_B() * 12, + DXFHelper(), + SubarrayValidator()) + csv = CsvBuilder().build_cad_output(dxf_data['panels']) + session_manager.save_uploaded_file(csv, dxf_file_name=filename) + + buildings = dxf_data['buildings'] + session_manager.save_buildings_polygons(buildings) + session_manager.save_is_drawing_inaccurate(dxf_data['is_panel_drawing_inaccurate']) + + return True, errors + except DXFError as error: + errors.append(error.message) + except OldDxfFormatException as error: + errors.append(error.message) + return False, errors + + @app.route("/array_summary/", methods=['GET', 'POST']) def array_summary(): """This endpoint allows you to upload a file. @@ -202,8 +242,7 @@ def array_summary(): validator = FileValidator(session_manager.user_values()) file = request.files['file_upload'] file_contents = validator.obtain_stream(file) - validation_error = validator.validate(file_contents, file, - FileType.Csv) + validation_error = validator.validate(file_contents, FileType.Csv, file) if not validation_error: session_manager.save_uploaded_file(file_contents, cad_file_name=file.filename) @@ -215,36 +254,13 @@ def array_summary(): elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename: file = request.files['dxf_upload'] user_values = session_manager.user_values() - calculator = Calculator(user_values, calculate_panel_data=False) validator = FileValidator(user_values) file_contents = validator.obtain_stream(file) - validation_error = validator.validate(file_contents, file, - FileType.AuroraDxf) - if validation_error: - error_msg = validation_error.format_error_message() - array_form.dxf_upload.errors.append(error_msg) + success, errors = handle_dxf_file(session_manager, file_contents, filename=file.filename) + if success: + return redirect(url_for('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(), - calculator.L_B() * 12, - DXFHelper(), - SubarrayValidator()) - csv = CsvBuilder().build_cad_output(dxf_data['panels']) - session_manager.save_uploaded_file(csv, dxf_file_name=file.filename) - - buildings = dxf_data['buildings'] - session_manager.save_buildings_polygons(buildings) - session_manager.save_is_drawing_inaccurate(dxf_data['is_panel_drawing_inaccurate']) - - return redirect(url_for('array_summary')) - except DXFError as error: - array_form.dxf_upload.errors.append(error.message) - except OldDxfFormatException as error: - array_form.dxf_upload.errors.append(error.message) + array_form.dxf_upload.errors.extend(errors) elif context['csv_available']: return redirect(url_for('power_station_configuration')) else: @@ -256,39 +272,81 @@ def array_summary(): context['dxf_file_name'] = '' if context['site_data_available'] and context['csv_available']: user_values = session_manager.user_values() - calculator = Calculator(user_values) - system_type = user_values.system_type() - module_type = user_values.module_type() - project_presenter = ProjectPresenter(system_type, module_type) + try: + calculator = Calculator(user_values) + system_type = user_values.system_type() + module_type = user_values.module_type() + project_presenter = ProjectPresenter(system_type, module_type) - context['wind_zones'] = system_type.system_constants().wind_zones - context['summary_table'] = calculator.summary_table() - context['minimum_array_sizes'] = calculator.minimum_array_sizes() - context['seismic_anchors'] = calculator.subarray_summary() - context['summary_values'] = calculator.summary_values() + context['wind_zones'] = system_type.system_constants().wind_zones + context['summary_table'] = calculator.summary_table() + context['minimum_array_sizes'] = calculator.minimum_array_sizes() + context['seismic_anchors'] = calculator.subarray_summary() + context['summary_values'] = calculator.summary_values() - panels = calculator.get_computed_csv_columns() - context['panel_array'] = project_presenter.get_panel_data(panels, - calculator.subarrays, - project_presenter.get_max_y( - calculator.buildings_for_drawing, - panels)) - context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing) - context['override_form'] = True - context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data' - context['dxf_file_name'] = session_manager.site.dxf_file_name or 'Upload System DXF' - context['is_drawing_inaccurate'] = session_manager.user_values().is_panel_drawing_inaccurate() - context['inaccurate_drawing_warning'] = 'The subarrays in this design are not parallel to each other, \ + panels = calculator.get_computed_csv_columns() + context['panel_array'] = project_presenter.get_panel_data(panels, + calculator.subarrays, + project_presenter.get_max_y( + calculator.buildings_for_drawing, + panels)) + context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing) + context['override_form'] = True + context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data' + context['dxf_file_name'] = session_manager.site.dxf_file_name or 'Upload System DXF' + context['is_drawing_inaccurate'] = session_manager.user_values().is_panel_drawing_inaccurate() + context['inaccurate_drawing_warning'] = 'The subarrays in this design are not parallel to each other, \ and the graphical representation on this page may not be accurate.' + except FileValidationException as error: + # when calculator is about to enter infinte loop + # it throws an exception - it is supplied wrong data + context['site_data_available'] = False + context['csv_available'] = False + context['no_proceed'] = True + context['cad_file_name'] = '' + context['dxf_file_name'] = '' + context['infinite_loop_detection_message'] = error.message elif not context['site_data_available']: context['no_proceed'] = True + if is_sfdc_session() and 'dxf_link_loaded' not in session: + context['javascripts'].append('auto_dxf_load') + db_session.close() return render_template('array_summary.html.jinja', context=context, form=array_form) +@app.route("/load_dxf/", methods=['GET', 'POST']) +def load_dxf_file(): + if 'dxf_link' not in session: + errors = ['DXF link not found'] + response = jsonify({'errors': errors}) + response.status_code = 404 + return response + + dxf_link = session['dxf_link'] + response = requests.get(dxf_link) + if response.status_code == 200: + file_contents = response.content.decode('utf-8') + filename = urlparse(dxf_link).path.strip('/') + + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + + success, errors = handle_dxf_file(session_manager, file_contents, filename=filename) + session['dxf_link_loaded'] = True + if success: + return jsonify({'status': 'success'}) + else: + errors = ['Unable to download DXF file from Sales Force ({})'.format(response.status_code)] + + response = jsonify({'errors': errors}) + response.status_code = 400 + return response + + @app.route("/power_station_configuration/", methods=['GET', 'POST']) def power_station_configuration(): db_session = sql_constant.sql_session_maker() @@ -485,6 +543,7 @@ def sales_force_authorized(): data = sf_tasks.get_site_characterization_from_sales_force(session, resp['instance_url']) if data: + session['dxf_link'] = data['dxf_link'] session_manager.save_form_submission(data) return redirect(next_url) else: @@ -492,7 +551,6 @@ def sales_force_authorized(): # FIXME -from flask import jsonify @app.route("/export-sfdc") def export_sfdc(): if not is_sfdc_session(): @@ -515,6 +573,8 @@ def get_sales_force_token(token=None): def sales_force_logout(): session.pop('SFID', None) session.pop('sales_force_token', None) + session.pop('dxf_link', None) + session.pop('dxf_link_loaded', None) session.clear() return redirect('/') # End of Sales Force Integration diff --git a/helix/scss/forms.scss b/helix/scss/forms.scss index 38fe31d..a1116d4 100644 --- a/helix/scss/forms.scss +++ b/helix/scss/forms.scss @@ -151,7 +151,7 @@ a.back { a { text-decoration: none; width: auto; - margin: 0 5px; + margin: 2px 5px; } .button { diff --git a/helix/scss/main.scss b/helix/scss/main.scss index f425bb7..5c83fe1 100644 --- a/helix/scss/main.scss +++ b/helix/scss/main.scss @@ -59,3 +59,35 @@ h1 { .spacer { flex-grow: 1; } + +.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; + overflow: hidden; +} + + +.msg-container { + background-color: $off-white; + border: 1px solid $medium-border-color; + margin: 20px; + padding: 10px; + padding-left: 30px; +} +.msg-container li { + padding-left: 0 !important; +} diff --git a/helix/static/css/main.css b/helix/static/css/main.css index 1c52064..6f35df5 100644 --- a/helix/static/css/main.css +++ b/helix/static/css/main.css @@ -832,6 +832,41 @@ h1 { flex-grow: 1; } +/* line 63 */ +.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; + overflow: hidden; +} + +/* line 84 */ +.msg-container { + background-color: #FDFDFD; + border: 1px solid #D8D8D8; + margin: 20px; + padding: 10px; + padding-left: 30px; +} + +/* line 91 */ +.msg-container li { + padding-left: 0 !important; +} + /* line 3 */ .navigation_header { display: flex; @@ -1023,21 +1058,3 @@ 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/javascripts/auto_dxf_load.js b/helix/static/javascripts/auto_dxf_load.js new file mode 100644 index 0000000..7ae3a91 --- /dev/null +++ b/helix/static/javascripts/auto_dxf_load.js @@ -0,0 +1,22 @@ +"use strict"; + +$(document).ready(function () { + $("#sf_msg_container").empty(); + $("#sf_msg_container").show(); + $('#sf-spinner-panel').css('width', '100%'); // Workaround for Safari issue + $.ajax({ + type: 'POST', + url: '/load_dxf/' + }).done(function(r) { + console.log(r) + $("#sf_msg_container").append('
Loading DXF file from Sales Force. Please wait, this may take a while.
+ +