merge with upstream
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
164
helix/main.py
164
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
|
||||
|
||||
@@ -151,7 +151,7 @@ a.back {
|
||||
a {
|
||||
text-decoration: none;
|
||||
width: auto;
|
||||
margin: 0 5px;
|
||||
margin: 2px 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
22
helix/static/javascripts/auto_dxf_load.js
Normal file
22
helix/static/javascripts/auto_dxf_load.js
Normal file
@@ -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('<li class="">DXF from Sales Force loaded successfully.</li>');
|
||||
setTimeout(function() { window.location.reload() }, 500);
|
||||
}).fail(function(r) {
|
||||
// console.log(r.status)
|
||||
r.responseJSON.errors.forEach((msg) => {
|
||||
$("#sf_msg_container").append('<li class="error_message">' + msg + '</li>');
|
||||
})
|
||||
}).always(function() {
|
||||
$('#sf-spinner-panel').css('width', '0%'); // Workaround for Safari issue
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,16 @@
|
||||
<i class="icon-spin6 animate-spin"></i>
|
||||
</div>
|
||||
|
||||
{% if 'SFID' in session %}
|
||||
<ul id="sf_msg_container" class="msg-container" style="display:none;">
|
||||
</ul>
|
||||
|
||||
<div id="sf-spinner-panel" class="spinner-panel">
|
||||
<p>Loading DXF file from Sales Force. Please wait, this may take a while.</p>
|
||||
<i class="icon-spin6 animate-spin"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not context['csv_available'] %}
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
{{ form.csrf_token }}
|
||||
@@ -33,6 +43,11 @@
|
||||
<span id="error_message_txt" class="error_message centered_error">{{ field.errors[0] }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if context['infinite_loop_detection_message'] %}
|
||||
<span id="error_message_txt_inf_loop" class="error_message centered_error">{{ context['infinite_loop_detection_message'] }}</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user