merge with upstream master

This commit is contained in:
Senad Uka
2017-12-19 15:18:35 +01:00
parent efb4547a37
commit 197db1003b
85 changed files with 2522 additions and 697 deletions

View File

@@ -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 = []

View File

@@ -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:

View File

@@ -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

View File

@@ -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')

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,3 @@
import copy
from enum import Enum
from helix.constants.system_type import SystemType

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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]

View File

@@ -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:

View File

@@ -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],

View File

@@ -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],

View File

@@ -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],

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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('<span class="error_message centered_error" id="error_message_txt">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
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('<span class="error_message centered_error" id="error_message_txt">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
$("#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('<span class="error_message centered_error" id="error_message_dxf">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
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('<span class="error_message centered_error" id="error_message_dxf">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
};

View File

@@ -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;
}
});

16
helix/json_builder.py Normal file
View File

@@ -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)

View File

@@ -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")

View File

@@ -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

View File

@@ -1,6 +1,3 @@
import math
from helix.models.coordinate import Coordinate
import sys
class ProjectPresenter(object):

View File

118
helix/sales_force/tasks.py Normal file
View File

@@ -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

85
helix/static/css/animation.css Executable file
View File

@@ -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);
}
}

View File

@@ -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'; } /* '' */
.icon-upload-cloud:before { content: '\e80e'; } /* '' */
.icon-spin6:before { content: '\e839'; } /* '' */

View File

@@ -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;
}

Binary file not shown.

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
<metadata>Copyright (C) 2017 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
@@ -35,6 +35,8 @@
<glyph glyph-name="sunpower-logo" unicode="&#xe80d;" d="M466 633c-35 61-77 96-152 96-78 0-146-56-146-137 0-76 78-111 138-138l59-26c114-50 211-107 211-248 0-154-124-272-277-272-141 0-247 91-275 228l96 27c13-90 82-164 177-164 94 0 181 73 181 172 0 103-81 138-163 175l-53 24c-103 47-192 99-192 225 0 137 116 225 247 225 98 0 181-50 228-137l-79-50 0 0z m537-353c0-66-3-140 38-197 39-53 114-84 178-84 63 0 132 29 173 79 47 56 43 133 43 202l0 525 99 0 0-552c0-96-7-172-78-244-60-66-148-101-237-101-83 0-168 32-228 90-77 74-86 154-86 255l0 552 98 0 0-525 0 0z m874 562l688-720 0 683 98 0 0-922-688 720 0-680-98 0 0 919z m1255-128l36 0c114 0 230-11 230-155 0-126-92-159-201-159l-65 0 0 314 0 0z m0-405l71 0c74 0 150 9 209 58 55 46 84 119 84 190 0 78-33 155-99 201-64 44-143 47-219 47l-145 0 0-882 99 0 0 386 0 0z m1949-165l285 706 285-706 232 661 106 0-338-927-285 709-285-709-338 927 106 0 232-661z m1128 661l457 0 0-91-359 0 0-262 348 0 0-91-348 0 0-347 359 0 0-91-457 0 0 882z m886-91l29 0c118 0 225-14 225-159 0-137-113-158-224-158l-30 0 0 317 0 0z m0-404l24 0 267-387 120 0-280 395c136 12 221 108 221 244 0 198-156 243-323 243l-127 0 0-882 98 0 0 387 0 0z m-2487 53c0-257-204-453-463-453-253 0-456 205-456 451 0 249 201 454 456 454 259 0 463-195 463-452m-816-2c0-188 150-354 348-354 204 0 363 154 363 354 0 203-157 357-363 357-200 0-348-166-348-357m3755 309c0 24 6 46 18 67 12 21 29 38 50 50 21 12 43 18 67 18 24 0 47-6 68-18 21-12 37-29 49-49 12-21 18-44 18-68 0-24-6-46-17-66-12-21-28-38-49-50-21-13-44-19-69-19-24 0-47 6-68 19-21 12-37 29-49 49-12 21-18 43-18 67l0 0z m17 0c0-21 5-41 16-59 11-18 25-33 43-43 18-11 38-16 59-16 22 0 41 5 59 16 19 10 33 25 44 43 10 18 16 38 16 59 0 21-6 40-16 58-10 18-24 33-43 44-18 11-38 16-60 16-21 0-40-5-59-16-18-10-32-25-43-43-11-19-16-38-16-59l0 0z m166 33c0-9-2-17-6-25-5-7-12-12-20-17l42-70-22 0-37 65-30 0 0-65-18 0 0 158 37 0c18 0 31-4 40-11 10-8 14-19 14-35l0 0z m-73-33l20 0c11 0 19 3 25 8 7 6 10 14 10 25 0 20-12 30-36 30l-19 0 0-63 0 0z" horiz-adv-x="7828" />
<glyph glyph-name="upload-cloud" unicode="&#xe80e;" d="M760 494q100 0 170-68t70-166-70-166-170-68l-190 0 0 190 106 0-176 230-174-230 104 0 0-190-248 0q-74 0-128 52t-54 124q0 74 53 126t129 52q14 0 20-2-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z" horiz-adv-x="1000" />
<glyph glyph-name="spin6" unicode="&#xe839;" d="M855 9c-189-190-520-172-705 13-190 190-200 494-28 695 11 13 21 26 35 34 36 23 85 18 117-13 30-31 35-76 16-112-5-9-9-15-16-22-140-151-145-379-8-516 153-153 407-121 542 34 106 122 142 297 77 451-83 198-305 291-510 222l0 1c236 82 492-24 588-252 71-167 37-355-72-493-11-15-23-29-36-42z" horiz-adv-x="1000" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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('<span class="error_message centered_error" id="error_message_txt">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
$("#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('<span class="error_message centered_error" id="error_message_txt">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
$("#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('<span class="error_message centered_error" id="error_message_dxf">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
$("#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('<span class="error_message centered_error" id="error_message_dxf">The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.</span>');
}
});
};
exports.default = AutoUpload;

View File

@@ -1,6 +1,15 @@
{% extends "layout.html.jinja" %}
{% set title = "Helix Calculator" %}
{% block contents %}
<script>
var is_csv_available = {{ context['csv_available']|tojson|safe }};
</script>
<div id="spinner-panel" class="spinner-panel">
<p>Uploading files. Please wait, this may take a while.</p>
<i class="icon-spin6 animate-spin"></i>
</div>
{% if not context['csv_available'] %}
<form action="" method="post" enctype="multipart/form-data">
{{ form.csrf_token }}

View File

@@ -1,6 +1,10 @@
{% extends "layout.html.jinja" %}
{% set title = "Helix Calculator" %}
{% block contents %}
{% for warning in context['warning_messages'] %}
<div class="summary_warning">{{ warning.value }}</div>
{% endfor %}
<div class="form_section">
<h3>Download</h3>
</div>
@@ -20,6 +24,14 @@
<button>Download BOM (.txt)</button>
</a>
</div>
{% if 'SFID' in session %}
<div class="download">
<a href="/export-sfdc">
<button>Export documents to Salesforce</button>
</a>
</div>
{% endif %}
{% else %}
Please complete previous steps first!
{% endif %}

View File

@@ -8,6 +8,7 @@
<link href="//cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.css" type="text/css" rel="stylesheet" />
<link rel="shortcut icon" href="https://us.sunpower.com/sites/sunpower/files/favicon.ico">
<link rel="stylesheet" type="text/css" href={{ url_for('static', filename='css/fontello.css') }}>
<link rel="stylesheet" type="text/css" href={{ url_for('static', filename='css/animation.css') }}>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"
integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
crossorigin="anonymous"></script>

View File

@@ -1,5 +1,9 @@
<div class="navigation_buttons">
<a class="back button" href="{{ context['steps'][context['current_step'] - 2][2] }}">Back</a>
{% if context['hide_back'] %}
{% else %}
<a class="back button" href="{{ context['steps'][context['current_step'] - 2][2] }}">Back</a>
{% endif %}
{% if context['steps']|length > context['current_step'] and not context.get('no_proceed') %}
{% if not form or context['override_form'] %}
<a class="button" href="{{ context['steps'][context['current_step']][2] }}" value="Next">Next</a>

View File

@@ -3,10 +3,11 @@
{% block contents %}
{% if context['site_data_available'] %}
<div class="form_section">
<h3>Summary</h3>
<h3>Summary <small>({{ context['project_name'] }})</small></h3>
{% for warning in context['warning_messages'] %}
<div class="summary_warning">{{ warning.value }}</div>
{% endfor %}
<table class="summary_table" id="summary_table">
<tr>
<td colspan="2" class="table_meta_headers">OUTPUTS</td>
@@ -80,8 +81,13 @@
</tr>
</table>
</div>
{% endif %}
{% include "navigation_buttons.html.jinja" %}
{% if 'SFID' in session %}
<small><i>Sales Force project</i> (<a href="/sales_force_logout">Logout</a>)</small>
{% endif %}
{% endblock %}

View File

@@ -8,7 +8,7 @@ from helix.constants.panel_type import PanelType
from helix.constants.system_type import SystemType
from helix.models.coordinate import Coordinate
from helix.models.panel import PanelData, Panel
from helix.models.sql.inverter_brands import InverterBrand
class UserValues(object):
def __init__(self, store, site):

Binary file not shown.

View File

@@ -30,7 +30,7 @@ class CsvInputValidator(object):
file_validation_chain = [
CsvInputValidator.validate_file_for_panel_types,
CsvInputValidator.validate_for_spacing,
# CsvInputValidator.validate_for_spacing, # disabling this feature for the moment
]
result = self.run_validation_chain(headers, rows, file_validation_chain)
if result:

View File

@@ -1,5 +1,3 @@
from helix.constants.file_validation_error import FileValidationMessage, FileValidationError
class DxfInputValidator(object):
def __init__(self, _):