From 2ea9e2e70283267fcb8f2a337b7e11bd4ae83f12 Mon Sep 17 00:00:00 2001 From: Senad Uka Date: Wed, 20 Dec 2017 20:27:55 +0100 Subject: [PATCH] merge with upstream --- helix/calculators/ebom_calculator.py | 2 - helix/calculators/seismic_calculator.py | 9 ++ helix/constants/file_validation_error.py | 4 + helix/main.py | 164 +++++++++++++++------- helix/scss/forms.scss | 2 +- helix/scss/main.scss | 32 +++++ helix/static/css/main.css | 53 ++++--- helix/static/javascripts/auto_dxf_load.js | 22 +++ helix/templates/array_summary.html.jinja | 15 ++ helix/validators/file_validator.py | 7 +- test/calculators/ebom_calculator_test.py | 1 - test/validators/file_validator_test.py | 7 +- 12 files changed, 237 insertions(+), 81 deletions(-) create mode 100644 helix/static/javascripts/auto_dxf_load.js 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('
  • DXF from Sales Force loaded successfully.
  • '); + setTimeout(function() { window.location.reload() }, 500); + }).fail(function(r) { + // console.log(r.status) + r.responseJSON.errors.forEach((msg) => { + $("#sf_msg_container").append('
  • ' + msg + '
  • '); + }) + }).always(function() { + $('#sf-spinner-panel').css('width', '0%'); // Workaround for Safari issue + }); +}); diff --git a/helix/templates/array_summary.html.jinja b/helix/templates/array_summary.html.jinja index b7d82e9..7e9147a 100644 --- a/helix/templates/array_summary.html.jinja +++ b/helix/templates/array_summary.html.jinja @@ -10,6 +10,16 @@ + {% if 'SFID' in session %} + + +
    +

    Loading DXF file from Sales Force. Please wait, this may take a while.

    + +
    + {% endif %} + {% if not context['csv_available'] %}
    {{ form.csrf_token }} @@ -33,6 +43,11 @@ {{ field.errors[0] }} {% endif %} {% endfor %} + + {% if context['infinite_loop_detection_message'] %} + {{ context['infinite_loop_detection_message'] }} + {% endif %} + diff --git a/helix/validators/file_validator.py b/helix/validators/file_validator.py index 1e6626b..f97ec6c 100644 --- a/helix/validators/file_validator.py +++ b/helix/validators/file_validator.py @@ -100,13 +100,13 @@ class FileValidator(object): finally: return content - def validate(self, stream, file, expected): + def validate(self, stream, expected, file=None, extension=None): """Validates the uploaded file by extension and content Arguments; stream (string): File content - file (FileObject): A file object provided from the ui + file (FileObject): A file object provided from the ui. Used only to get the file extension. """ @@ -114,7 +114,8 @@ class FileValidator(object): file_type = self.identify_file_type(stream) assert file_type == expected validator = file_type.validator(self.values) - extension = self.obtain_extension(file) + if extension is None: + extension = self.obtain_extension(file) file_type.valid_mapping(extension) return validator.validate(stream) except AssertionError: diff --git a/test/calculators/ebom_calculator_test.py b/test/calculators/ebom_calculator_test.py index dcf3fc5..2b40597 100644 --- a/test/calculators/ebom_calculator_test.py +++ b/test/calculators/ebom_calculator_test.py @@ -1118,7 +1118,6 @@ class EbomCalculatorTest(unittest.TestCase): cable_support_lid: 0, rear_skirt_1_1: 0, monitor_controller_480_v: 1, - ethernet_plug: 2, } assert_dictionary_equal(self.subject.compute_ebom(), expected_output) diff --git a/test/validators/file_validator_test.py b/test/validators/file_validator_test.py index 66dca26..cb32ddd 100644 --- a/test/validators/file_validator_test.py +++ b/test/validators/file_validator_test.py @@ -28,12 +28,12 @@ class FileValidatorTest(unittest.TestCase): with open('test/fixtures/input_single_tilt.csv', 'r', newline='') as file: cad_input = file.read() - eq_(self.subject.validate(cad_input, fake_file, FileType.Csv), None) + eq_(self.subject.validate(cad_input, FileType.Csv, fake_file), None) def test_unknown_files_are_invalid(self): fake_file = MagicMock() type(fake_file).filename = PropertyMock(return_value="Hi") - result = self.subject.validate("Hi", fake_file, FileType.Unknown) + result = self.subject.validate("Hi", FileType.Unknown, fake_file) self.should_have_error(result, FileValidationMessage.UnknownFileUploaded, None) @@ -62,8 +62,7 @@ class FileValidatorTest(unittest.TestCase): fake_file.read.return_value = open(fname, "rb").read() fake_file.filename.return_value = "expected_dual_tilt_pseries_image.png" stream = self.subject.obtain_stream(fake_file) - self.should_have_error(self.subject.validate(stream, fake_file, - FileType.AuroraDxf), + self.should_have_error(self.subject.validate(stream, FileType.AuroraDxf, fake_file), FileValidationMessage.ExpectedDxfFile, None)