1 Commits

Author SHA1 Message Date
GotPPay
3eb3463d50 value edit; test fix 2017-11-27 20:27:56 +01:00
80 changed files with 305 additions and 2573 deletions

View File

@@ -1,4 +1,4 @@
from python:3.6 from python:3.5
RUN apt-get update RUN apt-get update

View File

@@ -1 +1 @@
web: gunicorn -c gunicorn_config.py --pythonpath helix main:app web: python helix/main.py

View File

@@ -1,6 +1,3 @@
[![CircleCI](https://circleci.com/gh/SunPower/Helix_Roof_Calculator.svg?style=svg&circle-token=aacf2ae59dae99075992ed10d1e27f119e223e75)](https://circleci.com/gh/SunPower/Helix_Roof_Calculator)
## Helix Calculator ## Helix Calculator
- [Staging](https://sp-helix-staging.herokuapp.com) - [Staging](https://sp-helix-staging.herokuapp.com)
@@ -34,23 +31,6 @@
| google-chrome-stable/stable,now 62.0.3202.94-1 amd64 | | google-chrome-stable/stable,now 62.0.3202.94-1 amd64 |
| nodejs/unknown,now 6.12.0-1nodesource1 amd64 | | nodejs/unknown,now 6.12.0-1nodesource1 amd64 |
##### Environment Variables
Set the environment variables for your specific environment. Use the `.env` file below as reference:
```
AWS_S3_BUCKET="..."
AWS_ACCESS_KEY_ID="..."
AWS_SECRET_ACCESS_KEY="..."
SFDC_BASE_URL="https://test.salesforce.com"
SFDC_ACCESS_KEY_ID="..."
SFDC_SECRET_ACCESS_KEY="..."
SFDC_API_URL="https://sunpower--qa.cs8.my.salesforce.com"
```
##### Set up for Develop and Testing ##### Set up for Develop and Testing
- Run the docker container for the 1st time - Run the docker container for the 1st time
@@ -67,7 +47,7 @@ SFDC_API_URL="https://sunpower--qa.cs8.my.salesforce.com"
- Log in the docker container - Log in the docker container
```docker exec -ti helix bash``` ```docker exec -t -i helix /bin/bash```
- Run this commands inside the docker conatiner - Run this commands inside the docker conatiner
@@ -387,9 +367,3 @@ docker exec -it helixroofcalculator_helix_1 invoke db_migrate
``` ```
Debugging is possible directly from PyCharm, all code changes are transparent between host and container. Debugging is possible directly from PyCharm, all code changes are transparent between host and container.
## Database schema
![Database Schema](documentation/db_schema.png)

51
circle.yml Normal file
View File

@@ -0,0 +1,51 @@
version: 2
jobs:
build:
docker:
- image: ivannnn/heroku_cedar14:2.0
environment:
PGUSER: pivotal
PGPASSWORD: password
PASSWORD: password
environment:
PATH: /usr/local/rvm/gems/ruby-2.3.4/bin:/usr/local/rvm/gems/ruby-2.3.4@global/bin:/usr/local/rvm/rubies/ruby-2.3.4/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/rvm/bin
steps:
- checkout
- run:
name: Set up and Run tests
command: |
source /etc/profile.d/rvm.sh
rvm use 2.3.4
/etc/init.d/postgresql start
python3.4 -m venv env
source env/bin/activate
pip install invoke
invoke install
npm install
invoke db_migrate
invoke test_ci
- deploy:
name: staging
command: |
invoke update_heroku_version staging
"[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
git push -f git@heroku.com:sp-helix-staging.git $CIRCLE_SHA1:refs/heads/master
heroku run --app sp-helix-staging invoke db_migrate
rake ci:deliver
name: preprod
command: |
invoke update_heroku_version preprod
"[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
git push -f git@heroku.com:sp-helix-preprod.git $CIRCLE_SHA1:refs/heads/master
heroku run --app sp-helix-preprod invoke db_migrate
rake ci:deliver
name: production
commands: |
invoke update_heroku_version production
"[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
git push -f git@heroku.com:sp-helix-production.git $CIRCLE_SHA1:refs/heads/master
heroku run --app sp-helix-production invoke db_migrate
rake ci:deliver
notify:
webhooks:
- url: http://pulse.pivotallabs.com/projects/03ba990f-b8f5-4508-b4c1-19038b2cb791/status

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

BIN
dump.rdb

Binary file not shown.

View File

@@ -1,13 +0,0 @@
"""gunicorn WSGI server configuration."""
import multiprocessing
import os
bind = '0.0.0.0:' + os.getenv('PORT', '5000')
max_requests = 1000
worker_class = 'meinheld.gmeinheld.MeinheldWorker'
workers = multiprocessing.cpu_count()
timeout = os.getenv('TIMEOUT', 120)
graceful_timeout = os.getenv('TIMEOUT', 120)
keepalive = 5
daemon = False

View File

@@ -234,43 +234,24 @@ class DXFHelper(object):
node_store.add_node(node) 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: for node in nodes:
if len(node.neighboring_nodes()) == 8: if len(node.neighboring_nodes()) == 8:
continue 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 x in (0, 1, -1):
for y in (0, 1, -1): for y in (0, 1, -1):
if x == y == 0: if x == y == 0:
continue continue
x_spacing = (x * x_spacing_cos_rotation) - (y * y_spacing_sin_rotation) rotation = math.radians(node.coordinate.rotation)
y_spacing = (x * x_spacing_sin_rotation) + (y * y_spacing_cos_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))
coordinate = Coordinate(node.coordinate.x + x_spacing, node.coordinate.y + y_spacing, node.coordinate.rotation) coordinate = Coordinate(node.coordinate.x + x_spacing, node.coordinate.y + y_spacing, node.coordinate.rotation)
if coordinate.x < 0 or coordinate.y < 0: if coordinate.x < 0 or coordinate.y < 0:
continue continue
direction = GraphDirection((x, y))
# Optimization for `direction = GraphDirection((x, y))`
direction = graph_directions_cache[(x, y)]
if node.has_existing_neighbor(direction): if node.has_existing_neighbor(direction):
continue continue
# FIXME: This is the bottleneck of the loop
# Calling this ~10000 times needs ~20 seconds
neighbor = node_store.find_coordinate(coordinate) neighbor = node_store.find_coordinate(coordinate)
if neighbor: if neighbor:
node.add_neighbor(neighbor, direction) node.add_neighbor(neighbor, direction)
@@ -436,7 +417,7 @@ class DXFHelper(object):
def __compute_segment_direction(p1, p2): def __compute_segment_direction(p1, p2):
""" """
Computes direction of a segment. Points taken from building outline are assumed to be in counterclockwise order. Computes direction of a segment. Points taken from building outline are assumed to be in counterclockwise order.
:param p1: first point :param p1: first point
:param p2: second point :param p2: second point
:return: tuple representing orientation ('north', 'south', 'east', 'west') and variation in degrees from ideally :return: tuple representing orientation ('north', 'south', 'east', 'west') and variation in degrees from ideally
@@ -457,7 +438,7 @@ class DXFHelper(object):
def compute_corner_directions(vertex, prev, next, angle_correction): def compute_corner_directions(vertex, prev, next, angle_correction):
""" """
Determines if point is located in north/east corner Determines if point is located in north/east corner
:param vertex: point located in the corner :param vertex: point located in the corner
:param prev: point previous to vertex, assuming counterclockwise order :param prev: point previous to vertex, assuming counterclockwise order
:param next: point next to vertex, assuming counterclockwise order :param next: point next to vertex, assuming counterclockwise order
@@ -529,7 +510,7 @@ class DXFHelper(object):
@staticmethod @staticmethod
def __generate_wind_zone__(buildings, scaling_factor, points_callback, panel_orientation): 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 = [] wind_zones = []

View File

@@ -2,6 +2,9 @@ import io
import dxfgrabber import dxfgrabber
from helix.constants.file_validation_error import FileValidationMessage
from helix.models.dxf.dxf_error import OldDxfFormatException
class DXFService(object): class DXFService(object):
""" """
@@ -48,7 +51,6 @@ class DXFService(object):
panels = dxf_helper.generate_panels(modules, translated_modules) 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) node_graph = dxf_helper.build_node_graph(panels, module_constants.panel_spacing)
subarrays = dxf_helper.detect_subarrays(node_graph, panels) subarrays = dxf_helper.detect_subarrays(node_graph, panels)
for subarray in subarrays: for subarray in subarrays:

View File

@@ -1,27 +0,0 @@
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) # PermanentRedirect error
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,6 +5,7 @@ from helix.presenters.panel_presenter import ProjectPresenter
from helix.session_manager import SessionManager from helix.session_manager import SessionManager
from helix.constants import redis_constant, sql_constant from helix.constants import redis_constant, sql_constant
from helix.seismic_validator_user_values import SeismicValidatorUserValues from helix.seismic_validator_user_values import SeismicValidatorUserValues
from helix.validators.file_validator import FileValidator
from helix.validators.seismic_anchor_validator import SeismicAnchorValidator from helix.validators.seismic_anchor_validator import SeismicAnchorValidator
api = Blueprint('api', __name__, template_folder='templates') 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) row_count = sum(subarray.row_count for subarray in self.subarrays)
column_count = sum(subarray.column_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() 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), self.subarrays).compute_ebom() ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module)).compute_ebom()
add_parts_to_list(parts_list, ebom_parts_list) add_parts_to_list(parts_list, ebom_parts_list)

View File

@@ -1,3 +1,4 @@
from math import ceil, floor
import copy import copy
from helix.Repositories.graph_repository import GraphRepository from helix.Repositories.graph_repository import GraphRepository
from helix.calculators.ballast_calculator import BallastCalculator 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.parts import wire_clip_large, cable_support, cable_support_lid, channel_nut, sunshade
from helix.constants.system_type import SystemType from helix.constants.system_type import SystemType
from helix.constants.inverter_brand import InverterBrand from helix.constants.inverter_brand import InverterBrand
from helix.forms.ebom_form import InverterBrandForm
class EbomCalculator(object): class EbomCalculator(object):
def __init__(self, user_values, row_count, column_count, modules_count = None, panels = None): def __init__(self, user_values, row_count, column_count, modules_count = None):
self.values = user_values self.values = user_values
self.row_count = row_count self.row_count = row_count
self.column_count = column_count self.column_count = column_count
self.modules_count = modules_count self.modules_count = modules_count
self.panels = panels
def resolve_power_monitor_type(self): def resolve_power_monitor_type(self):
module_type = self.values.module_type() module_type = self.values.module_type()
@@ -30,14 +30,6 @@ class EbomCalculator(object):
else: else:
return monitor_controller_240_v 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): def compute_ebom(self):
part_list = {} part_list = {}
@@ -46,14 +38,25 @@ class EbomCalculator(object):
monitors = self.values.power_monitors() monitors = self.values.power_monitors()
module_type = self.values.module_type() module_type = self.values.module_type()
system_type = self.values.system_type() system_type = self.values.system_type()
is_delta = self.resolve_is_delta() 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
inverter_count = 0 inverter_count = 0
total_ac_run_length = 0 total_ac_run_length = 0
panel_board_counts = [0, 0] panel_board_counts = [0, 0]
proper_monitor_controller = self.resolve_power_monitor_type() 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: for power_station in power_stations:
power_station_count = power_station['power_station_quantity'] power_station_count = power_station['power_station_quantity']
total_ac_run_length += power_station['ac_run_length'] total_ac_run_length += power_station['ac_run_length']
@@ -88,18 +91,15 @@ class EbomCalculator(object):
for monitor in monitors: for monitor in monitors:
if monitor['power_source'][0] == 'Switch Gear/External': if monitor['power_source'][0] == 'Switch Gear/External':
add_parts_to_list(part_list, {proper_monitor_controller: 1}, 1) 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: add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count)
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)) 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), is_delta) cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters))
add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports) add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports)
add_parts_to_list(part_list, {rear_skirt_1_1: -1}, ceil(cable_supports*.38)) add_parts_to_list(part_list, {rear_skirt: -1}, ceil(cable_supports*.38))
dependent_part_list = {} dependent_part_list = {}
@@ -121,37 +121,21 @@ class EbomCalculator(object):
if inverter['dc_switch']: if inverter['dc_switch']:
add_parts_to_list(part_list, dc_switch_parts, multiplier) add_parts_to_list(part_list, dc_switch_parts, multiplier)
def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count, is_delta): def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count):
# There are some tests that don't have panels if sum(panel_board_counts) == 0:
if is_delta and self.panels is None:
return 0 return 0
if is_delta:
panels_count = len(self.panels) if self.values.system_type() == SystemType.dualTilt:
if panels_count == 0: dimension1 = self.column_count
return 0 dimension2 = self.row_count
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: else:
if sum(panel_board_counts) == 0: dimension1 = self.row_count
return 0 dimension2 = self.column_count
if self.values.system_type() == SystemType.dualTilt:
dimension1 = self.column_count result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts)
dimension2 = self.row_count result *= dimension1 * max(dimension1 / dimension2, 1)
else: result += dimension1
dimension1 = self.row_count return ceil(result)
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): def get_standalone_inverters(self, power_station):
standalone_inverters = self.values.standalone_inverters() 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.calculators.subarray_helper import extract_subarray
from helix.constants.module_type import ModuleType from helix.constants.module_type import ModuleType
from helix.constants.panel_type import PanelType from helix.constants.panel_type import PanelType
from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1 from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1, leading_tray
from helix.constants.system_type import SystemType from helix.constants.system_type import SystemType

View File

@@ -1,14 +1,13 @@
from math import ceil, floor from math import ceil, floor
from helix.calculators.subarray_graph import SubarrayGraph
from helix.calculators.subarray_helper import extract_subarray from helix.calculators.subarray_helper import extract_subarray
from helix.constants.global_constants import minimum_racking_capacity from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType from helix.constants.panel_type import PanelType
from helix.constants.file_validation_error import FileValidationMessage,FileValidationException
from helix.models.subarray import Subarray from helix.models.subarray import Subarray
class SeismicCalculator(object): class SeismicCalculator(object):
def __init__(self, values, graph_repository): def __init__(self, values, graph_repository):
self.values = values self.values = values
@@ -39,20 +38,13 @@ class SeismicCalculator(object):
more_anchors_needed = True more_anchors_needed = True
perimeter_covered = sds < 1.0 perimeter_covered = sds < 1.0
anchor_threshold = 0 anchor_threshold = 0
was_rung_empty = False
while more_anchors_needed: while more_anchors_needed:
rung = graph.pop_rung() rung = graph.pop_rung()
interval = int(self.seismic_anchor_interval()) interval = int(self.seismic_anchor_interval())
nodes_since_last_anchor = interval nodes_since_last_anchor = interval
if len(rung) == 0: 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() graph.reset()
anchor_threshold += 1 anchor_threshold += 1
was_rung_empty = True
continue continue
while more_anchors_needed and interval >= 0: while more_anchors_needed and interval >= 0:
for node in rung: for node in rung:

View File

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

View File

@@ -5,7 +5,6 @@ from helix.constants.global_constants import system_force_capacity
class AnchorType(Enum): class AnchorType(Enum):
# These values are being used by Salesforce integration, do not change it
OMG_PowerGrip = 'OMG PowerGrip' OMG_PowerGrip = 'OMG PowerGrip'
OMG_PowerGrip_Plus = 'OMG PowerGrip Plus' OMG_PowerGrip_Plus = 'OMG PowerGrip Plus'
EcoFasten = 'EcoFasten Eco 65' EcoFasten = 'EcoFasten Eco 65'

View File

@@ -49,7 +49,3 @@ class FileValidationError(object):
if self.__class__ != other.__class__: if self.__class__ != other.__class__:
return False return False
return self.row_number == other.row_number and self.validation_message == other.validation_message return self.row_number == other.row_number and self.validation_message == other.validation_message
class FileValidationException(Exception):
def __init__(self, message):
self.message = message

View File

@@ -7,5 +7,3 @@ parapet_coefficients = 0.88, 1.2
system_force_capacity = 418. system_force_capacity = 418.
minimum_racking_capacity = 226 minimum_racking_capacity = 226
max_corner_angle = 135

View File

@@ -7,10 +7,6 @@ class InverterTypeSMA(IntEnum):
MODEL_20KW = 6 MODEL_20KW = 6
MODEL_24KW = 8 MODEL_24KW = 8
@property
def get_type(self):
return "SMA"
@property @property
def default_string(self): def default_string(self):
return { return {
@@ -53,10 +49,6 @@ class InverterTypeDelta(IntEnum):
MODEL_60KW = 11 MODEL_60KW = 11
MODEL_80KW = 12 MODEL_80KW = 12
@property
def get_type(self):
return "Delta"
@property @property
def label(self): def label(self):
return { return {

View File

@@ -2,7 +2,6 @@ from enum import Enum
class ModuleType(Enum): class ModuleType(Enum):
# These values are being used by Salesforce integration, do not change it
Cell96 = '96 Cell' Cell96 = '96 Cell'
Cell128 = '128 Cell' Cell128 = '128 Cell'
PSeries = 'P-Series' PSeries = 'P-Series'

View File

@@ -55,7 +55,7 @@ class SingleTiltParts(object):
def module(self, module_type): def module(self, module_type):
if module_type == ModuleType.Cell96: if module_type == ModuleType.Cell96:
rear_skirt_parts = rear_skirt_1_1 rear_skirt_parts = rear_skirt
spoiler_parts = spoiler spoiler_parts = spoiler
else: else:
rear_skirt_parts = rear_skirt_1_1 rear_skirt_parts = rear_skirt_1_1

View File

@@ -1,16 +0,0 @@
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,14 +130,8 @@ class NodeQuadTree():
self.nodeList = toKeep self.nodeList = toKeep
# Return a list of all possible nodes that can be near this point # Return a list of all possible nodes that can be near this point
# Optimization `only_quads_related = True`: def retrieve(self, nearPoint):
# Avoid replicate a large self.nodeList. Get only the other elements. retNodes = list(self.nodeList)
# 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: if self.quads[0] is not None:
index = self.getIndex(nearPoint) index = self.getIndex(nearPoint)

View File

@@ -1,26 +1,22 @@
let AutoUpload = () => { let AutoUpload = () => {
$("#file_upload").change((e) => { $("#file_upload").change((e) => {
var ten_megabyte_max_upload = 10000000; var ten_megabyte_max_upload = 10000000;
$("#error_container_txt").empty(); $("#error_container_txt").empty();
if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { if(e.currentTarget.files[0].size < ten_megabyte_max_upload){
// $('#spinner-panel').show(); e.currentTarget.form.submit();
$('#spinner-panel').css('width', '100%'); // Workaround for Safari issue }else{
e.currentTarget.form.submit(); $("#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>');
} 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) => { $("#dxf_upload").change((e) => {
var ten_megabyte_max_upload = 10000000; var ten_megabyte_max_upload = 10000000;
$("#error_container_dxf").empty(); $("#error_container_dxf").empty();
if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { if(e.currentTarget.files[0].size < ten_megabyte_max_upload){
// $('#spinner-panel').show(); e.currentTarget.form.submit();
$('#spinner-panel').css('width', '100%'); // Workaround for Safari issue }else{
e.currentTarget.form.submit(); $("#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>');
} 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,16 +8,13 @@ import AutoUpload from './auto_upload';
$(document).ready(function () { $(document).ready(function () {
AutoUpload(); AutoUpload();
let subarrayDisplay = new SubarrayDisplay();
if (is_csv_available) { subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data);
let subarrayDisplay = new SubarrayDisplay(); let arrayVisualization = new ArrayVisualization(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates);
subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); arrayVisualization.init();
let arrayVisualization = new ArrayVisualization(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); new ZoomControl(arrayVisualization).init($('#zoom_control'));
arrayVisualization.init(); new OverlayControl(arrayVisualization).init($('#overlay_control'), $('#legend_container'));
new ZoomControl(arrayVisualization).init($('#zoom_control')); new SeismicControl(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save"));
new OverlayControl(arrayVisualization).init($('#overlay_control'), $('#legend_container')); window.arrayVisualization = arrayVisualization;
new SeismicControl(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save"));
window.arrayVisualization = arrayVisualization;
}
}); });

View File

@@ -1,22 +0,0 @@
try:
import ujson as json
except ImportError:
import json
class JsonBuilder:
def build_bom(self, rows):
data = []
headers = ['itemId', 'description', 'quantity']
for row in rows:
d = {}
for i, value in enumerate(row):
d[headers[i]] = value
data.append(d)
return data
def bom_to_json(self, data):
return json.dumps(data)
def build_bom_output(self, rows):
return self.bom_to_json(self.build_bom(rows))

View File

@@ -1,23 +1,19 @@
import os import os
import requests import requests
from urllib.parse import urlparse
import rollbar import rollbar
import rollbar.contrib.flask import rollbar.contrib.flask
from flask import Flask, request, make_response, session, render_template, \ from flask import Flask, request, make_response, session, render_template, \
redirect, url_for, jsonify, flash redirect, url_for
from flask import got_request_exception from flask import got_request_exception
from flask.ext import assets from flask.ext import assets
from flask_oauthlib.client import OAuth
from webassets.filter import get_filter 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.doc_gen_service import DocGenService
from helix.Services.dxf_helper import DXFHelper from helix.Services.dxf_helper import DXFHelper
from helix.Services.dxf_service import DXFService from helix.Services.dxf_service import DXFService
from helix.api.api import api from helix.api.api import api
from helix.calculators.calculator import Calculator from helix.calculators.calculator import Calculator
from helix.constants import redis_constant, sql_constant 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.inverter_type import InverterType
from helix.constants.system_type import SystemType from helix.constants.system_type import SystemType
from helix.csv_builder import CsvBuilder from helix.csv_builder import CsvBuilder
@@ -29,49 +25,15 @@ from helix.forms.input_form import InputForm, ArrayForm, TestDXFForm
from helix.models.dxf.dxf_error import DXFError, OldDxfFormatException from helix.models.dxf.dxf_error import DXFError, OldDxfFormatException
from helix.presenters.image_presenter import ImagePresenter from helix.presenters.image_presenter import ImagePresenter
from helix.presenters.panel_presenter import ProjectPresenter from helix.presenters.panel_presenter import ProjectPresenter
from helix.qa_helper import QAScenario
from helix.session_manager import SessionManager from helix.session_manager import SessionManager
from helix.validators.file_validator import FileValidator, FileType from helix.validators.file_validator import FileValidator, FileType
from helix.validators.subarray_validator import SubarrayValidator from helix.validators.subarray_validator import SubarrayValidator
from flask_featureflags import FeatureFlag
import flask_featureflags as feature
import pprint
app = Flask(__name__) app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api') app.register_blueprint(api, url_prefix='/api')
app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey') app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey')
app.config['PROFILE'] = True app.config['PROFILE'] = True
app.config['FEATURE_FLAGS'] = {
'ff_dummy_feature': True,
'ff_cpp': True
}
FeatureFlag(app) # initializes feature flags
if feature.is_active('ff_dummy_feature'):
app.logger.info('Feature flags working!')
app.logger.info('Current state: ')
app.logger.info(pprint.PrettyPrinter(width=41, compact=True).pformat(app.config['FEATURE_FLAGS']))
# Salesforce integrations
oauth = OAuth()
SFDC_BASE_URL = os.getenv('SFDC_BASE_URL', 'https://test.salesforce.com')
try:
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=SFDC_BASE_URL,
request_token_url=None, # OAuth 2
access_token_method='POST', # Sales Force requirement
access_token_url=SFDC_BASE_URL + '/services/oauth2/token',
authorize_url=SFDC_BASE_URL + '/services/oauth2/authorize',
)
except TypeError:
print('Salesforce integration disabled')
sales_force = None
assets_env = assets.Environment(app) assets_env = assets.Environment(app)
assets_env.init_app(app) assets_env.init_app(app)
assets_env.load_path = [ assets_env.load_path = [
@@ -105,10 +67,6 @@ def init_rollbar():
got_request_exception.connect(rollbar.contrib.flask.report_exception, app) got_request_exception.connect(rollbar.contrib.flask.report_exception, app)
def is_sales_force_session():
return 'sf_session' in session
@app.route("/") @app.route("/")
def index(): def index():
return redirect(url_for('site_characterization')) return redirect(url_for('site_characterization'))
@@ -151,9 +109,6 @@ def test_dxf():
# wizard steps # wizard steps
@app.route("/site_characterization/", methods=['GET', 'POST']) @app.route("/site_characterization/", methods=['GET', 'POST'])
def site_characterization(): def site_characterization():
if is_sales_force_session():
return redirect('/summary/')
db_session = sql_constant.sql_session_maker() db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session) session_manager = SessionManager(session, redis_constant.redis_store, db_session)
site_info_form = InputForm() site_info_form = InputForm()
@@ -181,7 +136,6 @@ def summary():
context['current_step'] = 2 context['current_step'] = 2
if context['site_data_available']: if context['site_data_available']:
context['project_name'] = session_manager.site.project_name
user_values = session_manager.user_values() user_values = session_manager.user_values()
calculator = Calculator(user_values, calculate_panel_data=False) calculator = Calculator(user_values, calculate_panel_data=False)
context['wind_zones'] = user_values.system_type().system_constants().wind_zones context['wind_zones'] = user_values.system_type().system_constants().wind_zones
@@ -199,51 +153,10 @@ def summary():
else: else:
context['no_proceed'] = True context['no_proceed'] = True
if is_sales_force_session():
context['hide_back'] = True
db_session.close() db_session.close()
return render_template('site_summary.html.jinja', context=context) 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']) @app.route("/array_summary/", methods=['GET', 'POST'])
def array_summary(): def array_summary():
"""This endpoint allows you to upload a file. """This endpoint allows you to upload a file.
@@ -262,7 +175,8 @@ def array_summary():
validator = FileValidator(session_manager.user_values()) validator = FileValidator(session_manager.user_values())
file = request.files['file_upload'] file = request.files['file_upload']
file_contents = validator.obtain_stream(file) file_contents = validator.obtain_stream(file)
validation_error = validator.validate(file_contents, FileType.Csv, file) validation_error = validator.validate(file_contents, file,
FileType.Csv)
if not validation_error: if not validation_error:
session_manager.save_uploaded_file(file_contents, session_manager.save_uploaded_file(file_contents,
cad_file_name=file.filename) cad_file_name=file.filename)
@@ -274,15 +188,35 @@ def array_summary():
elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename: elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename:
file = request.files['dxf_upload'] file = request.files['dxf_upload']
user_values = session_manager.user_values() user_values = session_manager.user_values()
calculator = Calculator(user_values, calculate_panel_data=False)
validator = FileValidator(user_values) validator = FileValidator(user_values)
file_contents = validator.obtain_stream(file) file_contents = validator.obtain_stream(file)
success, errors = handle_dxf_file(session_manager, file_contents, filename=file.filename) validation_error = validator.validate(file_contents, file,
if success: FileType.AuroraDxf)
if is_sales_force_session(): if validation_error:
session['sf_session']['new_dxf_file'] = True error_msg = validation_error.format_error_message()
return redirect(url_for('array_summary')) array_form.dxf_upload.errors.append(error_msg)
else: else:
array_form.dxf_upload.errors.extend(errors) try:
module_constants = user_values.module_system_constants()
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)
elif context['csv_available']: elif context['csv_available']:
return redirect(url_for('power_station_configuration')) return redirect(url_for('power_station_configuration'))
else: else:
@@ -294,91 +228,39 @@ def array_summary():
context['dxf_file_name'] = '' context['dxf_file_name'] = ''
if context['site_data_available'] and context['csv_available']: if context['site_data_available'] and context['csv_available']:
user_values = session_manager.user_values() user_values = session_manager.user_values()
try: calculator = Calculator(user_values)
calculator = Calculator(user_values) system_type = user_values.system_type()
system_type = user_values.system_type() module_type = user_values.module_type()
module_type = user_values.module_type() project_presenter = ProjectPresenter(system_type, module_type)
project_presenter = ProjectPresenter(system_type, module_type)
context['wind_zones'] = system_type.system_constants().wind_zones context['wind_zones'] = system_type.system_constants().wind_zones
context['summary_table'] = calculator.summary_table() context['summary_table'] = calculator.summary_table()
context['minimum_array_sizes'] = calculator.minimum_array_sizes() context['minimum_array_sizes'] = calculator.minimum_array_sizes()
context['seismic_anchors'] = calculator.subarray_summary() context['seismic_anchors'] = calculator.subarray_summary()
context['summary_values'] = calculator.summary_values() context['summary_values'] = calculator.summary_values()
panels = calculator.get_computed_csv_columns() panels = calculator.get_computed_csv_columns()
max_y = project_presenter.get_max_y(calculator.buildings_for_drawing,panels) context['panel_array'] = project_presenter.get_panel_data(panels,
context['panel_array'] = project_presenter.get_panel_data(panels, calculator.subarrays,
calculator.subarrays, project_presenter.get_max_y(
max_y) calculator.buildings_for_drawing,
context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing, max_y) panels))
context['corners'] = project_presenter.get_corners(calculator.buildings) context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing)
context['override_form'] = True context['override_form'] = True
context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data' 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['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['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, \ 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.' 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']: elif not context['site_data_available']:
context['no_proceed'] = True context['no_proceed'] = True
if is_sales_force_session() and \
session['sf_session'].get('dxf_link', None) and \
not session['sf_session'].get('dxf_link_loaded', None):
context['javascripts'].append('auto_dxf_load')
db_session.close() db_session.close()
return render_template('array_summary.html.jinja', context=context, form=array_form) return render_template('array_summary.html.jinja', context=context, form=array_form)
@app.route("/load_dxf/", methods=['GET', 'POST'])
def load_dxf_file():
if (not is_sales_force_session()) or session['sf_session'].get('dxf_link', None) is None:
errors = ['DXF link not found']
response = jsonify({'errors': errors})
response.status_code = 404
return response
dxf_link = session['sf_session'].get('dxf_link', None)
if not dxf_link:
session['sf_session']['dxf_link_loaded'] = True
response = jsonify({'status': 'success', 'messages': ['No DXF file to load from Salesforce.']})
response.status_code = 202
return response
print('Loading DXF file from {}'.format(dxf_link))
response = requests.get(dxf_link, timeout=30)
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['sf_session']['dxf_link_loaded'] = True
if success:
return jsonify({'status': 'success', 'messages': ['DXF from Salesforce loaded successfully.']})
else:
errors = ['Unable to download DXF file from Salesforce ({})'.format(response.status_code)]
response = jsonify({'errors': errors})
response.status_code = 400
return response
@app.route("/power_station_configuration/", methods=['GET', 'POST']) @app.route("/power_station_configuration/", methods=['GET', 'POST'])
def power_station_configuration(): def power_station_configuration():
db_session = sql_constant.sql_session_maker() db_session = sql_constant.sql_session_maker()
@@ -457,11 +339,6 @@ def download():
session_manager = SessionManager(session, redis_constant.redis_store, db_session) session_manager = SessionManager(session, redis_constant.redis_store, db_session)
context = session_manager.context() context = session_manager.context()
context['current_step'] = 5 context['current_step'] = 5
if is_sales_force_session():
error, data = session['sf_session'].pop('export_urls', (None, None))
if data is not None:
context['sfdc_export_error'] = error
context['sfdc_export_urls'] = data
db_session.close() db_session.close()
return render_template('download.html.jinja', context=context) return render_template('download.html.jinja', context=context)
@@ -551,141 +428,6 @@ def helix_documentation():
return render_template('helix_documentation.jinja', context=context) return render_template('helix_documentation.jinja', context=context)
# Salesforce 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['sf_session'] = {}
session['sf_session']['sfid'] = sfid
return sales_force.authorize(callback=url_for('sales_force_authorized', _external=True, sfid=sfid), sfid=sfid)
else:
flash('Missing sfid')
return redirect('/')
@app.route('/sales_force_authorized')
def sales_force_authorized():
if not is_sales_force_session():
flash('This is not a Salesforce session')
return redirect('/')
next_url = url_for('summary')
try:
resp = sales_force.authorized_response()
except Exception as e:
flash('Error on authenticating to Salesforce.')
print('Error on authenticating to Salesforce.')
print(str(e))
return sales_force_logout()
if resp is None:
flash('Unknown response from Salesforce.')
print('Unknown response from Salesforce.')
return redirect(next_url)
print('New Salesforce - OAuth2 login')
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
session['sf_session']['access_token'] = resp['access_token']
helix_session_id = session['id']
access_token = session['sf_session']['access_token']
sfid = session['sf_session']['sfid']
try:
data, status_code = sf_tasks.get_site_characterization_from_sales_force(helix_session_id, access_token, sfid)
if data and status_code == 200:
session['sf_session']['dxf_link'] = data['dxf_link']
session_manager.save_form_submission(data)
flash('Connected to Salesforce.')
return redirect(next_url)
else:
flash('Unable to get site characterization from Salesforce. ({})'.format(status_code))
return sales_force_logout()
except Exception as e:
print(str(e))
flash('Unable to get site characterization from Salesforce.')
return sales_force_logout()
@app.route("/export-sfdc")
def export_sfdc():
if not is_sales_force_session():
flash('This is not a Salesforce session')
return redirect('/')
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
helix_session_id = session_manager.session['id']
sf_session = session['sf_session']
access_token = sf_session['access_token']
sfid = sf_session['sfid']
new_dxf_file = sf_session.get('new_dxf_file', False)
error, data = sf_tasks.export_to_sfdc(helix_session_id, access_token, sfid, new_dxf_file=new_dxf_file, qa_test=session.get('qa_test', None))
data.pop('bom', None)
data['dxfUrlFromSF'] = sf_session['dxf_link']
sf_session['export_urls'] = (error, data)
db_session.close()
return redirect('/download')
if sales_force:
@sales_force.tokengetter
def get_sales_force_token(token=None):
return session.get('sf_session', {}).get('access_token', None)
@app.route('/sales_force_logout')
def sales_force_logout():
session.pop('sf_session', None)
return redirect('/')
# End of Salesforce Integration
# QA scenario simulations
@app.route('/qa')
def qa_simulations():
test = request.args.get('test', None)
bad_request = None
if not test:
bad_request = 'Missing `test` parameter.'
elif test not in QAScenario.all():
bad_request = 'Unknown test scenario: <b>' + test + '</b>.'
if bad_request:
tests = ''.join(['<li><a href="/qa?test=' + s + '">' + s + '</a></li>' for s in QAScenario.all()])
tests = '<ul>' + tests + '</ul>'
return bad_request + tests, 400
if test.startswith('sf_') and 'sf_session' not in session:
return 'Please, connect to Salesforce first.', 400
url = None
if test == QAScenario.SF_RESET_ALL.value:
session['sf_session']['dxf_link_loaded'] = False
session['qa_test'] = None
url = request.url_root
elif test == QAScenario.SF_NO_DXF.value:
session['sf_session']['dxf_link'] = None
session['sf_session']['dxf_link_loaded'] = False
url = request.url_root + 'array_summary/'
elif test == QAScenario.SF_VALID_DXF.value:
session['sf_session']['dxf_link'] = 'https://sunpower-test-dgplatform-spectrum.s3.amazonaws.com/a5FL00000008gKcMAI-DXF.dxf'
session['sf_session']['dxf_link_loaded'] = False
url = request.url_root + 'array_summary/'
elif test in (QAScenario.SF_ERROR_UPLOAD_S3.value, QAScenario.SF_ERROR_NOTIFY_SF.value):
session['qa_test'] = test
url = request.url_root + 'export-sfdc'
msg = '<b>' + test + '</b> test enabled.'
if url:
msg += ' Access: <a href="' + url + '">' + url + '</a> to check.'
return msg, 200
@app.template_filter('format_number') @app.template_filter('format_number')
def format_number(number): def format_number(number):
return "{:,g}".format(number) return "{:,g}".format(number)
@@ -715,13 +457,8 @@ def enum():
def main(): def main():
host = '0.0.0.0' host = '0.0.0.0'
debug = bool(os.getenv("FLASK_DEBUG", False)) port = int(os.getenv('PORT', 5000))
if os.getenv('FLASK_DEBUG_SSL', None): app.run(host=host, port=port, debug=bool(os.getenv("FLASK_DEBUG", False)))
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") @app.route("/fail-test")

View File

@@ -1,13 +0,0 @@
class Corner(object):
def __init__(self, x, y, length_ccw, length_cw, angle):
self.x = x
self.y = y
self.length_ccw = length_ccw
self.length_cw = length_cw
self.angle = angle
@property
def dictionary(self):
return {"x": self.x, "y": self.y, "length_ccw": self.length_ccw, "length_cw": self.length_cw, "angle": self.angle}

View File

@@ -25,7 +25,6 @@ class GraphNodeStore(list):
dy = node.coordinate.y - coordinate.y dy = node.coordinate.y - coordinate.y
return dx * dx + dy * dy 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): def find_coordinate(self, coordinate):
# create and populate the quadtree on first request # create and populate the quadtree on first request
if self.quadTree is None: if self.quadTree is None:
@@ -34,15 +33,9 @@ class GraphNodeStore(list):
self.quadTree.insert(node) self.quadTree.insert(node)
del self[:] del self[:]
variance_square = self.variance ** 2 possibilities = self.quadTree.retrieve(coordinate)
# 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: for node in possibilities:
if self.distance_squared(node, coordinate) <= variance_square: if self.distance_squared(node, coordinate) <= self.variance ** 2:
return node return node
more_possibilities = self.quadTree.retrieve(coordinate, only_quads_related=True) else:
for node in more_possibilities: return None
if self.distance_squared(node, coordinate) <= variance_square:
return node
return None

View File

@@ -1,8 +1,7 @@
import math
from helix.models.coordinate import Coordinate
import sys import sys
from math import sqrt, degrees, acos
from helix.models.corner import Corner
from helix.constants.global_constants import max_corner_angle
import flask_featureflags as feature
class ProjectPresenter(object): class ProjectPresenter(object):
def __init__(self, system_type, module_type): def __init__(self, system_type, module_type):
@@ -22,6 +21,8 @@ class ProjectPresenter(object):
spacing_x, spacing_y = module_constants.presenter_spacing spacing_x, spacing_y = module_constants.presenter_spacing
wind_zones = system_constants.wind_zones wind_zones = system_constants.wind_zones
for panel in panels: for panel in panels:
subarray = [x for x in subarrays if x.subarray_number == panel.subarray][0] subarray = [x for x in subarrays if x.subarray_number == panel.subarray][0]
origin = subarray.origin origin = subarray.origin
@@ -52,10 +53,9 @@ class ProjectPresenter(object):
self.offset = height self.offset = height
for panel in table_data: for panel in table_data:
panel['y'] = height - panel['y'] + first_cell panel['y'] = height - panel['y'] + first_cell
return table_data return table_data
def get_buildings(self, buildings, max_y): def get_buildings(self, buildings):
if self.offset is None: if self.offset is None:
self.offset = 0 self.offset = 0
@@ -70,50 +70,11 @@ class ProjectPresenter(object):
# origin = self.find_origin(building) # origin = self.find_origin(building)
for point in building: for point in building:
point.x = point.x * spacing_x point.x = point.x * spacing_x
point.y = abs((point.y * spacing_y) - max_y) point.y = point.y * spacing_y
presentable_building.append(point.__dict__) presentable_building.append(point.__dict__)
return result return result
def get_corners(self, buildings):
def angle(x1, y1, x2, y2):
# Use dotproduct to find angle between vectors
# This always returns an angle between 0, pi
numer = (x1 * x2 + y1 * y2)
denom = sqrt((x1 ** 2 + y1 ** 2) * (x2 ** 2 + y2 ** 2))
return acos(numer / denom)
def cross_sign(x1, y1, x2, y2):
# True if cross is positive
# False if negative or zero
return x1 * y2 > x2 * y1
result = []
if feature.is_active('ff_cpp'):
for building in buildings:
presentable_building = []
result.append(presentable_building)
for i in range(len(building)):
p1 = building[i]
ref = building[i - 1]
p2 = building[i - 2]
x1, y1 = p1[0] - ref[0], p1[1] - ref[1]
x2, y2 = p2[0] - ref[0], p2[1] - ref[1]
corner_length_cw = sqrt(x2**2 + y2**2)
corner_length_ccw = sqrt(x1**2 + y1**2)
theta = degrees(angle(x1, y1, x2, y2))
#print('Points', p1, ref, p2)
#print('Angle', theta)
if (cross_sign(x1, y1, x2, y2)) and (theta < max_corner_angle) :
#print('Inner Angle')
presentable_building.append(Corner(ref[0], ref[1], corner_length_ccw,corner_length_cw, theta).__dict__)
return result
def get_max_y(self,buildings, panels): def get_max_y(self,buildings, panels):
module_constants = self.system_type.module_constants(self.module_type) module_constants = self.system_type.module_constants(self.module_type)

View File

@@ -1,20 +0,0 @@
from enum import Enum
class QAException(Exception):
pass
class QAScenario(Enum):
SF_NO_DXF = 'sf_no_dxf'
SF_ERROR_UPLOAD_S3 = 'sf_error_upload_s3'
SF_ERROR_NOTIFY_SF = 'sf_error_notify_sf'
SF_VALID_DXF = 'sf_valid_dxf'
SF_RESET_ALL = 'sf_reset_all'
def __repr__(self):
return '<%s.%s>' % (self.__class__.__name__, self.name)
@classmethod
def all(cls):
return [s.value for s in list(cls)]

View File

@@ -1,138 +0,0 @@
import io
import os
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.qa_helper import QAScenario, QAException
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(helix_session_id, access_token, sfid):
SFDC_API_URL = os.getenv('SFDC_API_URL', 'https://sunpower--qa.cs8.my.salesforce.com')
url = SFDC_API_URL + '/services/apexrest/v1/HelixRoofDetails'
headers = {'Authorization': 'Bearer {}'.format(access_token)}
params = {'sfid': sfid, 'helix_session_id': helix_session_id}
result = requests.get(url, headers=headers, params=params, timeout=30)
if result.status_code == 200:
data = result.json()
if data:
data = convert_sales_force_data_format_to_helix(data)
return data, 200
else:
print('Error while getting data from Salesforce ({})'.format(result.status_code))
print(url, params)
print(result.content)
# print(result.request.headers)
return None, result.status_code
def convert_sales_force_data_format_to_helix(data):
data = convert_dict_keys_to_snake_case(data)
if data['system_type'] in ('Single-Tilt', 'Single Tilt'):
data['system_type'] = SystemType.singleTilt.value
elif data['system_type'] == ('Dual-Tilt', 'Dual Tilt'):
data['system_type'] = SystemType.dualTilt.value
if data['anchor_type'] == 'OMG Power Grip Plus':
data['anchor_type'] = 'OMG PowerGrip Plus'
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(helix_session_id, access_token, sfid, new_dxf_file=False, qa_test=None):
step = 'Exporting to Salesforce'
try:
# 1. Load User Values
step = 'Loading User Values'
session = {'id': helix_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_builder = JsonBuilder()
bom_list = json_builder.build_bom(bom)
bom_list_json = json_builder.bom_to_json(bom_list)
# 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'
if qa_test == QAScenario.SF_ERROR_UPLOAD_S3.value:
raise QAException(qa_test)
bom_csv_url = s3_upload(io.StringIO(csv_file), filename=sfid + '-csv.csv')
doc_url = s3_upload(io.BytesIO(document), filename=sfid + '-pdf.pdf')
bom_json_url = s3_upload(io.StringIO(bom_list_json), filename=sfid + '-json.json')
# Send only if the file DXF file was uploaded in Helix
if new_dxf_file and dxf_contents: # Optional
dxf_url = s3_upload(io.StringIO(dxf_contents), filename=sfid + '-dxf.dxf')
else:
dxf_url = None
# 6. Notify SFDC system with an API request
step = 'Notifying Salesforce'
if qa_test == QAScenario.SF_ERROR_NOTIFY_SF.value:
raise QAException(qa_test)
data = {
'dxfUrl': dxf_url,
'bomCsvUrl': bom_csv_url,
'documentationUrl': doc_url,
'bom': bom_list,
'helixSessionId': helix_session_id,
'sfid': sfid,
}
headers = {'Authorization': 'Bearer {}'.format(access_token)}
SFDC_API_URL = os.getenv('SFDC_API_URL', 'https://sunpower--qa.cs8.my.salesforce.com')
url = SFDC_API_URL + '/services/apexrest/v1/HelixRoofDetails'
result = requests.post(url, headers=headers, json=data, timeout=30)
# 7. Internal logs
if result.status_code <= 299:
print('Salesforce notified successfully for session {}.'.format(helix_session_id))
# print(result.content)
error = None
else:
error = 'Helix Calculator was not able to notify Salesforce ({})'.format(result.status_code)
print(error)
print(result.content)
# Only Helix need this information for audit
data['bomJsonUrl'] = bom_json_url
db_session.close()
return error, data
except Exception as e:
print('Error while {} for session {}'.format(step, helix_session_id))
error = 'Error while {}'.format(step)
return error, {}

View File

@@ -72,12 +72,6 @@ i.icon-info-circled {
margin: 0 10px; margin: 0 10px;
} }
.info_message {
color: black;
padding-left: 5px;
margin: 0 auto;
}
.error_message { .error_message {
color: red; color: red;
padding-left: 5px; padding-left: 5px;
@@ -157,7 +151,7 @@ a.back {
a { a {
text-decoration: none; text-decoration: none;
width: auto; width: auto;
margin: 2px 5px; margin: 0 5px;
} }
.button { .button {

View File

@@ -59,35 +59,3 @@ h1 {
.spacer { .spacer {
flex-grow: 1; 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;
}

View File

@@ -81,17 +81,17 @@ class SessionManager(object):
building_height=form_data.get('building_height', 0), building_height=form_data.get('building_height', 0),
building_width=form_data.get('building_width', 0), building_width=form_data.get('building_width', 0),
building_length=form_data.get('building_length', 0), building_length=form_data.get('building_length', 0),
parapet_height=form_data.get('parapet_height', form_data.get('building_parapet_height', 0)), parapet_height=form_data.get('building_parapet_height', 0),
wind_speed=form_data.get('wind_speed', 0), wind_speed=form_data.get('wind_speed', 0),
exposure_category=form_data.get('exposure_category', 'C'), exposure_category=form_data.get('exposure_category', 'C'),
exposure_transition_distance=form_data.get('exposure_category_transition_distance', 0), exposure_transition_distance=form_data.get('exposure_category_transition_distance', 0),
ballast_block_weight=form_data.get('ballast_block_weight', form_data.get('ballast_weight', 0)), ballast_block_weight=form_data.get('ballast_block_weight', 0),
max_psf=form_data.get('system_pressure', form_data.get('max_system_pressure', form_data.get('max_psf', 0))), max_psf=form_data.get('max_system_pressure', 0),
system_type=form_data.get('system_type', SystemType.dualTilt.value), system_type=form_data.get('system_type', SystemType.dualTilt.value),
module_type=form_data.get('module_type', ModuleType.Cell96.value), module_type=form_data.get('module_type', ModuleType.Cell96.value),
anchor_type=form_data.get('anchor_type', AnchorType.OMG_PowerGrip_Plus.value), anchor_type=form_data.get('anchor_type', AnchorType.OMG_PowerGrip_Plus.value),
spectral_response=form_data.get('design_spectral_response', form_data.get('spectral_response_acceleration', form_data.get('spectral_response', 1))), spectral_response=form_data.get('design_spectral_response', 1),
seismic_importance_factor=form_data.get('seismic_importance_factor', form_data.get('importance_factor', 1)) seismic_importance_factor=form_data.get('importance_factor', 1)
) )
self.db_session.add(self.site) self.db_session.add(self.site)
self.db_session.commit() self.db_session.commit()

View File

@@ -1,85 +0,0 @@
/*
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-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?24283821'); src: url('../font/fontello.eot?10976371');
src: url('../font/fontello.eot?24283821#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?10976371#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?24283821') format('woff2'), url('../font/fontello.woff2?10976371') format('woff2'),
url('../font/fontello.woff?24283821') format('woff'), url('../font/fontello.woff?10976371') format('woff'),
url('../font/fontello.ttf?24283821') format('truetype'), url('../font/fontello.ttf?10976371') format('truetype'),
url('../font/fontello.svg?24283821#fontello') format('svg'); url('../font/fontello.svg?10976371#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.svg?24283821#fontello') format('svg'); src: url('../font/fontello.svg?10976371#fontello') format('svg');
} }
} }
*/ */
@@ -69,5 +69,4 @@
.icon-info:before { content: '\e80b'; } /* '' */ .icon-info:before { content: '\e80b'; } /* '' */
.icon-close:before { content: '\e80c'; } /* '' */ .icon-close:before { content: '\e80c'; } /* '' */
.icon-sunpower-logo:before { content: '\e80d'; } /* '' */ .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

@@ -616,20 +616,13 @@ i.icon-info-circled {
} }
/* line 75 */ /* line 75 */
.info_message {
color: black;
padding-left: 5px;
margin: 0 auto;
}
/* line 81 */
.error_message { .error_message {
color: red; color: red;
padding-left: 5px; padding-left: 5px;
margin: 0 auto; margin: 0 auto;
} }
/* line 87 */ /* line 81 */
input, select { input, select {
width: calc(100% - 20px); width: calc(100% - 20px);
padding: 10px; padding: 10px;
@@ -640,7 +633,7 @@ input, select {
letter-spacing: 2px; letter-spacing: 2px;
} }
/* line 97 */ /* line 91 */
select { select {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
@@ -648,14 +641,14 @@ select {
background: #F4F4F4 url("https://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png") no-repeat calc(100% - 15px); background: #F4F4F4 url("https://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png") no-repeat calc(100% - 15px);
} }
/* line 104 */ /* line 98 */
.submit, .download { .submit, .download {
padding: 20px; padding: 20px;
text-align: center; text-align: center;
border-top: 1px solid #EAEAEA; border-top: 1px solid #EAEAEA;
} }
/* line 110 */ /* line 104 */
.button, button { .button, button {
background: #5199F5; background: #5199F5;
color: white; color: white;
@@ -667,7 +660,7 @@ select {
letter-spacing: 2px; letter-spacing: 2px;
} }
/* line 121 */ /* line 115 */
.navigation_buttons { .navigation_buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -675,18 +668,18 @@ select {
border-bottom: 1px solid #EAEAEA; border-bottom: 1px solid #EAEAEA;
padding: 20px; padding: 20px;
} }
/* line 128 */ /* line 122 */
.navigation_buttons a { .navigation_buttons a {
text-decoration: none; text-decoration: none;
width: auto; width: auto;
margin: 0 20px; margin: 0 20px;
} }
/* line 134 */ /* line 128 */
.navigation_buttons .button { .navigation_buttons .button {
width: auto; width: auto;
} }
/* line 139 */ /* line 133 */
.back { .back {
border: 2px solid #5199F5; border: 2px solid #5199F5;
color: #5199F5; color: #5199F5;
@@ -694,12 +687,12 @@ select {
margin-left: 10px; margin-left: 10px;
} }
/* line 146 */ /* line 140 */
a.back { a.back {
text-decoration: none; text-decoration: none;
} }
/* line 150 */ /* line 144 */
.sample_images { .sample_images {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
@@ -707,18 +700,18 @@ a.back {
border-bottom: 1px solid #EAEAEA; border-bottom: 1px solid #EAEAEA;
padding: 15px; padding: 15px;
} }
/* line 157 */ /* line 151 */
.sample_images a { .sample_images a {
text-decoration: none; text-decoration: none;
width: auto; width: auto;
margin: 2px 5px; margin: 2px 5px;
} }
/* line 163 */ /* line 157 */
.sample_images .button { .sample_images .button {
width: auto; width: auto;
} }
/* line 168 */ /* line 162 */
.image_button, button { .image_button, button {
background: #5199F5; background: #5199F5;
color: white; color: white;
@@ -839,41 +832,6 @@ h1 {
flex-grow: 1; 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 */ /* line 3 */
.navigation_header { .navigation_header {
display: flex; display: flex;

Binary file not shown.

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?> <?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"> <!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"> <svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2017 by original authors @ fontello.com</metadata> <metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
<defs> <defs>
<font id="fontello" horiz-adv-x="1000" > <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" /> <font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
@@ -35,8 +35,6 @@
<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="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="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> </font>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -73,18 +73,15 @@
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
$(document).ready(function () { $(document).ready(function () {
(0, _auto_upload2.default)(); (0, _auto_upload2.default)();
var subarrayDisplay = new _subarray_display2.default();
if (is_csv_available) { subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data);
var subarrayDisplay = new _subarray_display2.default(); var arrayVisualization = new _array_visualization2.default(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates);
subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); arrayVisualization.init();
var arrayVisualization = new _array_visualization2.default(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); new _zoom_control2.default(arrayVisualization).init($('#zoom_control'));
arrayVisualization.init(); new _overlay_control2.default(arrayVisualization).init($('#overlay_control'), $('#legend_container'));
new _zoom_control2.default(arrayVisualization).init($('#zoom_control')); new _seismic_control2.default(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save"));
new _overlay_control2.default(arrayVisualization).init($('#overlay_control'), $('#legend_container')); window.arrayVisualization = arrayVisualization;
new _seismic_control2.default(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save"));
window.arrayVisualization = arrayVisualization;
}
}); });
/***/ }), /***/ }),
@@ -24733,32 +24730,28 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { Object.defineProperty(exports, "__esModule", {
value: true value: true
}); });
var AutoUpload = function AutoUpload() { var AutoUpload = function AutoUpload() {
$("#file_upload").change(function (e) { $("#file_upload").change(function (e) {
var ten_megabyte_max_upload = 10000000; var ten_megabyte_max_upload = 10000000;
$("#error_container_txt").empty(); $("#error_container_txt").empty();
if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { if (e.currentTarget.files[0].size < ten_megabyte_max_upload) {
// $('#spinner-panel').show(); e.currentTarget.form.submit();
$('#spinner-panel').css('width', '100%'); // Workaround for Safari issue } else {
e.currentTarget.form.submit(); $("#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>');
} 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) { $("#dxf_upload").change(function (e) {
var ten_megabyte_max_upload = 10000000; var ten_megabyte_max_upload = 10000000;
$("#error_container_dxf").empty(); $("#error_container_dxf").empty();
if (e.currentTarget.files[0].size < ten_megabyte_max_upload) { if (e.currentTarget.files[0].size < ten_megabyte_max_upload) {
// $('#spinner-panel').show(); e.currentTarget.form.submit();
$('#spinner-panel').css('width', '100%'); // Workaround for Safari issue } else {
e.currentTarget.form.submit(); $("#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>');
} 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; exports.default = AutoUpload;

View File

@@ -1,30 +0,0 @@
"use strict";
$(document).ready(function () {
$("#sf_msg_container").empty();
$('#sf-spinner-panel').css('width', '100%'); // Workaround for Safari issue
$.ajax({
type: 'POST',
url: '/load_dxf/'
}).done(function(data, textStatus, jqXHR) { // 200 or 202
if (data && data.messages) {
data.messages.forEach((msg) => {
$("#sf_msg_container").append('<li class="info_message">' + msg + '</li>');
})
$("#sf_msg_container").show();
}
if (jqXHR.status == 200) {
setTimeout(function() { window.location.reload() }, 1000);
}
}).fail(function(jqXHR, textStatus, errorThrown) { // 400, 404, 5xx
// console.log(jqXHR, textStatus, errorThrown)
if (jqXHR.responseJSON && jqXHR.responseJSON.errors) {
jqXHR.responseJSON.errors.forEach((msg) => {
$("#sf_msg_container").append('<li class="error_message">' + msg + '</li>');
})
$("#sf_msg_container").show();
}
}).always(function(r) {
$('#sf-spinner-panel').css('width', '0%'); // Workaround for Safari issue
});
});

View File

@@ -73,7 +73,7 @@
} }
}); });
inverterStringsPerInverter.val(inverterStringsPerInverterValue); inverterStringsPerInverter.val(inverterStringsPerInverterValue);
inverterStringsPerInverterForm.css('display', 'block'); inverterStringsPerInverterForm.css('display', 'inherit');
} else { } else {
inverterStringsPerInverter.append($('<option selected value="0"></option>')); inverterStringsPerInverter.append($('<option selected value="0"></option>'));
inverterStringsPerInverterForm.css('display', 'none'); inverterStringsPerInverterForm.css('display', 'none');
@@ -160,8 +160,7 @@
if (data) { if (data) {
$('#standalone_inverter_id').val(uuid); $('#standalone_inverter_id').val(uuid);
$('#standalone_ac_run_length').val(data['ac_run_length']); $('#standalone_ac_run_length').val(data['ac_run_length']);
var attach_point = data['attachment_point'][1]; $('#attachment-point').val(data['attachment_point'][0]);
$('#attachment_point option[value=' + attach_point + ']').attr('selected','selected');
fillInverterData(data, '#inverter'); fillInverterData(data, '#inverter');

View File

@@ -1,35 +1,6 @@
{% extends "layout.html.jinja" %} {% extends "layout.html.jinja" %}
{% set title = "Helix Calculator" %} {% set title = "Helix Calculator" %}
{% block contents %} {% block contents %}
<script>
{%if context['corners'] and 'ff_cpp' is active_feature %}
var corners_data = {{ context['corners']|tojson}};
if (corners_data){
console.log("Corners data :");
console.log(corners_data);
}
{%endif%}
</script>
<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 'sf_session' 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 Salesforce. Please wait, this may take a while.</p>
<i class="icon-spin6 animate-spin"></i>
</div>
{% endif %}
{% if not context['csv_available'] %} {% if not context['csv_available'] %}
<form action="" method="post" enctype="multipart/form-data"> <form action="" method="post" enctype="multipart/form-data">
{{ form.csrf_token }} {{ form.csrf_token }}
@@ -53,11 +24,6 @@
<span id="error_message_txt" class="error_message centered_error">{{ field.errors[0] }}</span> <span id="error_message_txt" class="error_message centered_error">{{ field.errors[0] }}</span>
{% endif %} {% endif %}
{% endfor %} {% 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>
</div> </div>

View File

@@ -1,25 +1,6 @@
{% extends "layout.html.jinja" %} {% extends "layout.html.jinja" %}
{% set title = "Helix Calculator" %} {% set title = "Helix Calculator" %}
{% block contents %} {% block contents %}
{% if 'sfdc_export_urls' in context %}
{% if context['sfdc_export_error'] %}
<div class="summary_warning" style="margin-top: 20px;">{{ context['sfdc_export_error'] }}</div>
{% else %}
<div class="msg-container">Documents exported to Salesforce successfully.</div>
{% endif %}
<ul class="msg-container">
{% if context['sfdc_export_urls']['dxfUrl'] %}
<li><a href="{{ context['sfdc_export_urls']['dxfUrl'] }}">DXF file</a> (uploaded on Helix)</li>
{% else %}
<li><a href="{{ context['sfdc_export_urls']['dxfUrlFromSF'] }}">DXF file</a> (uploaded on Salesforce)</li>
{% endif %}
<li><a href="{{ context['sfdc_export_urls']['documentationUrl'] }}">Documentation file</a></li>
<li><a href="{{ context['sfdc_export_urls']['bomCsvUrl'] }}">BOM CSV file</a></li>
<li><a href="{{ context['sfdc_export_urls']['bomJsonUrl'] }}">BOM JSON file</a></li>
</ul>
{% endif %}
<div class="form_section"> <div class="form_section">
<h3>Download</h3> <h3>Download</h3>
</div> </div>
@@ -39,14 +20,6 @@
<button>Download BOM (.txt)</button> <button>Download BOM (.txt)</button>
</a> </a>
</div> </div>
{% if 'sf_session' in session %}
<div class="download">
<a href="/export-sfdc">
<button>Export documents to Salesforce</button>
</a>
</div>
{% endif %}
{% else %} {% else %}
Please complete previous steps first! Please complete previous steps first!
{% endif %} {% endif %}

View File

@@ -15,7 +15,8 @@
<div>Inverter Brand</div> <div>Inverter Brand</div>
</div> </div>
<form name="inverter_brand_form" action="" method="post" enctype="multipart/form-data"> <form name="inverter_brand_form" action="" method="post" enctype="multipart/form-data">
<div id="inverter_brand_form" class="form_section ebom_form"> <div id="inverter_brand_form"
class="form_section ebom_form">
{{ inverter_brand_form.csrf_token }} {{ inverter_brand_form.csrf_token }}
{% for field in inverter_brand_form.group('hidden') %} {% for field in inverter_brand_form.group('hidden') %}
{{ field }} {{ field }}

View File

@@ -8,7 +8,6 @@
<link href="//cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.css" type="text/css" rel="stylesheet" /> <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="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/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" <script src="https://code.jquery.com/jquery-2.2.4.min.js"
integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
@@ -59,23 +58,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% with messages = get_flashed_messages() %}
{% if messages %}
<br>
<div class="summary_warning">
{% for message in messages %}
<div>{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block contents %}{% endblock %} {% block contents %}{% endblock %}
{% if 'sf_session' in session %}
<small><i>Salesforce project</i> (<a href="/sales_force_logout">Log out</a>)</small>
{% endif %}
</div> </div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -100,13 +100,13 @@ class FileValidator(object):
finally: finally:
return content return content
def validate(self, stream, expected, file=None, extension=None): def validate(self, stream, file, expected):
"""Validates the uploaded file by extension """Validates the uploaded file by extension
and content and content
Arguments; Arguments;
stream (string): File content stream (string): File content
file (FileObject): A file object provided from the ui. Used only to get the file extension. file (FileObject): A file object provided from the ui
""" """
@@ -114,8 +114,7 @@ class FileValidator(object):
file_type = self.identify_file_type(stream) file_type = self.identify_file_type(stream)
assert file_type == expected assert file_type == expected
validator = file_type.validator(self.values) validator = file_type.validator(self.values)
if extension is None: extension = self.obtain_extension(file)
extension = self.obtain_extension(file)
file_type.valid_mapping(extension) file_type.valid_mapping(extension)
return validator.validate(stream) return validator.validate(stream)
except AssertionError: except AssertionError:

View File

@@ -2,12 +2,8 @@ nose==1.3.7
selenium==2.53.1 selenium==2.53.1
splinter==0.7.3 splinter==0.7.3
cssselect==0.9.1 cssselect==0.9.1
lxml==4.1.1 lxml==3.6.0
mockredispy==2.9.0.11 mockredispy==2.9.0.11
Flask-Testing==0.4.2 Flask-Testing==0.4.2
splinter[flask] splinter[flask]
pillow==3.3.1 pillow==3.3.1
eralchemy==1.1.0
locust==0.8
pylint==1.7.4
pyopenssl==17.5.0

View File

@@ -1,5 +1,3 @@
gunicorn==19.7.1
meinheld==0.6.1
Flask==0.10.1 Flask==0.10.1
itsdangerous==0.24 itsdangerous==0.24
Jinja2==2.8 Jinja2==2.8
@@ -20,8 +18,3 @@ invoke==0.13.0
dxfgrabber==0.8.1 dxfgrabber==0.8.1
rollbar==0.13.11 rollbar==0.13.11
blinker==1.4 blinker==1.4
Flask-OAuthlib==0.9.4
boto3==1.4.8
ujson==1.35
Flask-FeatureFlags==0.6
mock==2.0.0

View File

@@ -1,2 +1 @@
python-3.5.1 python-3.5.1

View File

@@ -1,5 +1,4 @@
from invoke import run, task from invoke import run, task
import os
@task @task
@@ -35,10 +34,9 @@ def test_js(ctx):
def update_version(ctx): def update_version(ctx):
run('echo "import os\n\n\ndef version():\n if os.getenv(\'VERSION\'):\n return os.getenv(\'VERSION\')\n return \'$(git describe --tags)\'" > helix/constants/version.py') run('echo "import os\n\n\ndef version():\n if os.getenv(\'VERSION\'):\n return os.getenv(\'VERSION\')\n return \'$(git describe --tags)\'" > helix/constants/version.py')
@task @task
def serve(ctx): def serve(ctx):
run('SP_DOCGEN_API_KEY=DC97-20AF-567E gunicorn -c gunicorn_config.py --pythonpath helix main:app') run('PYTHONPATH=. SP_DOCGEN_API_KEY=DC97-20AF-567E python helix/main.py')
@task @task
@@ -54,23 +52,3 @@ def db_migrate(ctx):
@task @task
def serve_debug(ctx): def serve_debug(ctx):
run('PYTHONPATH=. FLASK_DEBUG=1 SP_DOCGEN_API_KEY=DC97-20AF-567E python helix/main.py') run('PYTHONPATH=. FLASK_DEBUG=1 SP_DOCGEN_API_KEY=DC97-20AF-567E python helix/main.py')
@task
def db_diagram(ctx):
db_uri = os.getenv('DATABASE_URL', 'postgres://pivotal:@localhost:5432/pivotal')
run('eralchemy -i "{}" -o documentation/db_schema.png -x temp audit'.format(db_uri))
@task
def classes_diagrams(ctx):
run('pyreverse -k -o calculators-diagram.png helix/calculators')
run('pyreverse -k -o constants-diagram.png helix/constants')
run('pyreverse -k -o forms-diagram.png helix/forms')
run('pyreverse -k -o models-diagram.png helix/models')
run('pyreverse -k -o validators-diagram.png helix/validators')
run('rm -rf packages*.png ; mv classes.*-diagram.png documentation/')
@task
def serve_ssl(ctx):
run('FLASK_DEBUG_SSL=true PORT=8443 PYTHONPATH=. FLASK_DEBUG=1 SP_DOCGEN_API_KEY=DC97-20AF-567E python helix/main.py')

View File

@@ -118,7 +118,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2907, wire_clip_large: 2907,
cable_support_lid: 368, cable_support_lid: 368,
cable_support: 368, cable_support: 368,
rear_skirt_1_1: -140, rear_skirt: -140,
dc_switch_bracket: 2, dc_switch_bracket: 2,
front_legs: 23, front_legs: 23,
back_legs: 23, back_legs: 23,
@@ -193,10 +193,10 @@ class EbomCalculatorTest(unittest.TestCase):
self.user_values.module_type.return_value = ModuleType.Cell96 self.user_values.module_type.return_value = ModuleType.Cell96
expected_output = { expected_output = {
stump: 65, stump: 65,
wire_clip_large: 590, wire_clip_large: 513,
cable_support_lid: 0, cable_support_lid: 0,
cable_support: 0, cable_support: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
rubber_foot: 9, rubber_foot: 9,
delta_inverter_leg: 9, delta_inverter_leg: 9,
harness_4_string_mf: 4, harness_4_string_mf: 4,
@@ -228,10 +228,10 @@ class EbomCalculatorTest(unittest.TestCase):
self.user_values.module_type.return_value = ModuleType.Cell96 self.user_values.module_type.return_value = ModuleType.Cell96
expected_output = { expected_output = {
stump: 0, stump: 0,
wire_clip_large: 200, wire_clip_large: 171,
cable_support_lid: 0, cable_support_lid: 0,
cable_support: 0, cable_support: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
rubber_foot: 3, rubber_foot: 3,
delta_inverter_leg: 3, delta_inverter_leg: 3,
harness_4_string_mf: 2, harness_4_string_mf: 2,
@@ -380,7 +380,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2394, wire_clip_large: 2394,
cable_support: 270, cable_support: 270,
cable_support_lid: 270, cable_support_lid: 270,
rear_skirt_1_1: -103, rear_skirt: -103,
ethernet_plug: 7.2, ethernet_plug: 7.2,
monitor_power_plug: 1, monitor_power_plug: 1,
sunshade_bolt: 12, sunshade_bolt: 12,
@@ -492,7 +492,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2394, wire_clip_large: 2394,
cable_support: 270, cable_support: 270,
cable_support_lid: 270, cable_support_lid: 270,
rear_skirt_1_1: -103, rear_skirt: -103,
ethernet_plug: 7.2, ethernet_plug: 7.2,
monitor_power_plug: 1, monitor_power_plug: 1,
sunshade_bolt: 12, sunshade_bolt: 12,
@@ -604,7 +604,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2394, wire_clip_large: 2394,
cable_support: 270, cable_support: 270,
cable_support_lid: 270, cable_support_lid: 270,
rear_skirt_1_1: -103, rear_skirt: -103,
ethernet_plug: 7.2, ethernet_plug: 7.2,
monitor_power_plug: 1, monitor_power_plug: 1,
fuseshade: 14, fuseshade: 14,
@@ -624,13 +624,13 @@ class EbomCalculatorTest(unittest.TestCase):
}] }]
self.user_values.system_type.return_value = SystemType.singleTilt self.user_values.system_type.return_value = SystemType.singleTilt
self.user_values.module_type.return_value = ModuleType.Cell96 self.user_values.module_type.return_value = ModuleType.Cell96
expected_output = { expected_output = {
monitor_power_plug: 1, monitor_power_plug: 1,
stump: 0, stump: 0,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_controller_480_v:1, monitor_controller_480_v:1,
} }
@@ -644,13 +644,13 @@ class EbomCalculatorTest(unittest.TestCase):
}] }]
self.user_values.system_type.return_value = SystemType.singleTilt self.user_values.system_type.return_value = SystemType.singleTilt
self.user_values.module_type.return_value = ModuleType.Cell96 self.user_values.module_type.return_value = ModuleType.Cell96
expected_output = { expected_output = {
monitor_power_plug: 1, monitor_power_plug: 1,
stump: 0, stump: 0,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_controller_480_v:1, monitor_controller_480_v:1,
flat_washer: 4, flat_washer: 4,
channel_nut: 4, channel_nut: 4,
@@ -771,7 +771,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2394, wire_clip_large: 2394,
cable_support: 694, cable_support: 694,
cable_support_lid: 694, cable_support_lid: 694,
rear_skirt_1_1: -264, rear_skirt: -264,
ethernet_plug: 7.2, ethernet_plug: 7.2,
monitor_power_plug: 1, monitor_power_plug: 1,
fuseshade: 14, fuseshade: 14,
@@ -922,7 +922,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2394, wire_clip_large: 2394,
cable_support: 270, cable_support: 270,
cable_support_lid: 270, cable_support_lid: 270,
rear_skirt_1_1: -103, rear_skirt: -103,
ethernet_plug: 7.2, ethernet_plug: 7.2,
monitor_power_plug: 1, monitor_power_plug: 1,
sunshade_bolt: 12, sunshade_bolt: 12,
@@ -968,7 +968,7 @@ class EbomCalculatorTest(unittest.TestCase):
wire_clip_large: 2394, wire_clip_large: 2394,
cable_support: 270, cable_support: 270,
cable_support_lid: 270, cable_support_lid: 270,
rear_skirt_1_1: -103, rear_skirt: -103,
ethernet_plug: 7.2, ethernet_plug: 7.2,
sunshade_bolt: 12, sunshade_bolt: 12,
sunshade_washer: 12, sunshade_washer: 12,
@@ -1027,7 +1027,7 @@ class EbomCalculatorTest(unittest.TestCase):
mounting_back_plate: 1, mounting_back_plate: 1,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_power_plug: 1, monitor_power_plug: 1,
} }
@@ -1046,7 +1046,7 @@ class EbomCalculatorTest(unittest.TestCase):
mounting_back_plate: 1, mounting_back_plate: 1,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_power_plug: 1, monitor_power_plug: 1,
} }
@@ -1055,14 +1055,14 @@ class EbomCalculatorTest(unittest.TestCase):
stump: 0, stump: 0,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0 rear_skirt: 0
} }
expected_output_240_pseries = { expected_output_240_pseries = {
stump: 0, stump: 0,
cable_support_lid: 0, cable_support_lid: 0,
cable_support: 0, cable_support: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_controller_240_v: 1, monitor_controller_240_v: 1,
} }
@@ -1116,13 +1116,14 @@ class EbomCalculatorTest(unittest.TestCase):
stump: 0, stump: 0,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_controller_480_v: 1, monitor_controller_480_v: 1,
ethernet_plug: 2,
} }
assert_dictionary_equal(self.subject.compute_ebom(), expected_output) assert_dictionary_equal(self.subject.compute_ebom(), expected_output)
def test_computes_ebom_with_power_monitor_SMA_brand(self): def test_computes_ebom_with_power_monitor_SMA_brand(self):
self.user_values.inverter_brands.return_value = [{'inverter_brand_id':1}] self.user_values.inverter_brands.return_value = [{'inverter_brand_id':1}]
self.user_values.power_monitors.return_value = [{ self.user_values.power_monitors.return_value = [{
'monitor_id': 'foo', 'monitor_id': 'foo',
@@ -1130,13 +1131,13 @@ class EbomCalculatorTest(unittest.TestCase):
}] }]
self.user_values.system_type.return_value = SystemType.singleTilt self.user_values.system_type.return_value = SystemType.singleTilt
self.user_values.module_type.return_value = ModuleType.Cell96 self.user_values.module_type.return_value = ModuleType.Cell96
expected_output = { expected_output = {
monitor_power_plug: 1, monitor_power_plug: 1,
stump: 0, stump: 0,
cable_support: 0, cable_support: 0,
cable_support_lid: 0, cable_support_lid: 0,
rear_skirt_1_1: 0, rear_skirt: 0,
monitor_controller_480_v:1, monitor_controller_480_v:1,
flat_washer: 4, flat_washer: 4,
channel_nut: 4, channel_nut: 4,
@@ -1151,3 +1152,7 @@ class EbomCalculatorTest(unittest.TestCase):
} }
assert_dictionary_equal(self.subject.compute_ebom(), expected_output) assert_dictionary_equal(self.subject.compute_ebom(), expected_output)

View File

@@ -99,7 +99,7 @@ class MechanicalBomCalculatorWhenSingleTilt96CellTest(unittest.TestCase):
cross_tray: 5, cross_tray: 5,
rubber_foot: 4.4, rubber_foot: 4.4,
front_skirt: 24, front_skirt: 24,
rear_skirt_1_1: 44, rear_skirt: 44,
leading_tray: 25, leading_tray: 25,
ballast: 192, ballast: 192,
anchor: 15, # 12 + the 3 seismic anchors anchor: 15, # 12 + the 3 seismic anchors

View File

@@ -43,13 +43,13 @@ Part # Description Total
514865 BOLT, HH, 3/8-16 X 1/2, 18-8 SS 350 514865 BOLT, HH, 3/8-16 X 1/2, 18-8 SS 350
515059 ASSY, WHIP TRAY W/FUSE CLIPS, INVERTER, HELIX 16 515059 ASSY, WHIP TRAY W/FUSE CLIPS, INVERTER, HELIX 16
515928 FRONT SKIRT, HELIX ROOF 197 515928 FRONT SKIRT, HELIX ROOF 197
515929 REAR SKIRT, HELIX ROOF 1469
516043 AC SWITCH, CONNECTORIZED, HELIX ROOF 2 516043 AC SWITCH, CONNECTORIZED, HELIX ROOF 2
516045 AC SPLICE BOX, CONNECTORIZED, HELIX ROOF 1 516045 AC SPLICE BOX, CONNECTORIZED, HELIX ROOF 1
517871 TRAY, LEADING, HELIX ROOF, RIVETED VERSION 210 517871 TRAY, LEADING, HELIX ROOF, RIVETED VERSION 210
518058 CONNECTOR, ETHERNET, PLUG, RJ-45, WEATHERPROOF, SHIELDED 15 518058 CONNECTOR, ETHERNET, PLUG, RJ-45, WEATHERPROOF, SHIELDED 15
518331 MOUNTING BACK PLATE, INVERTER/PANEL BOARD, HELIX ROOF/TRACKER 22 518331 MOUNTING BACK PLATE, INVERTER/PANEL BOARD, HELIX ROOF/TRACKER 22
518477 WASHER, FLAT, 3/8, 1.00 OD, 18-8 SS 425 518477 WASHER, FLAT, 3/8, 1.00 OD, 18-8 SS 425
520301 REAR SKIRT, HELIX ROOF V1.1 1469
521031 WASHER, FLAT, M10 X 20MM OD, SS 10 521031 WASHER, FLAT, M10 X 20MM OD, SS 10
521794 DEFLECTOR, LH, HELIX ROOF V1.1 171 521794 DEFLECTOR, LH, HELIX ROOF V1.1 171
521795 DEFLECTOR, RH, HELIX ROOF V1.1 171 521795 DEFLECTOR, RH, HELIX ROOF V1.1 171
Can't render this file because it contains an unexpected character in line 10 and column 30.

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +0,0 @@
HANDLE BLOCKNAME WIND POS SUBARRAY PSF BAL LTRAY XTRAY ANC ID XCOORD YCOORD ANGLE
'1A66A5 *U5350 B 1 3 2.76 1 2 - 1 1 13906.282 -20190.065 4.124
'1A667F *U5350 B 1 3 2.76 1 2 - 1 2 13818.27 -20196.412 4.124
'1A6633 *U5350 B 1 2 2.76 1 2 - 1 3 13647.543 -20208.723 4.124
'1A6529 *U5425 B 3 3 2.26 0 1 - 1 4 13910.752 -20252.064 4.124
'1A6503 *U5425 B 3 3 2.26 0 1 - 1 5 13822.741 -20258.411 4.124
'1A64B7 *U5425 B 3 2 2.26 0 1 - 1 6 13652.014 -20270.722 4.124
'1A63AD *U5425 B 3 3 2.26 0 1 - 1 7 13915.223 -20314.063 4.124
'1A6387 *U5425 B 3 3 2.26 0 1 - 1 8 13827.212 -20320.41 4.124
'1A633B *U5425 B 3 2 2.26 0 1 - 1 9 13656.484 -20332.721 4.124
'1A6231 *U5425 C 3 3 6.36 11 1 1 - 10 13919.694 -20376.062 4.124
'1A620B *U5425 C 3 3 6.36 11 1 1 - 11 13831.682 -20382.409 4.124
'1A61BF *U5425 C 3 2 6.36 11 1 1 - 12 13660.955 -20394.72 4.124
'1A60B5 *U5425 C 3 3 6.36 11 1 1 - 13 13924.164 -20438.061 4.124
'1A608F *U5425 C 3 3 6.36 11 1 1 - 14 13836.153 -20444.408 4.124
'1A6043 *U5425 C 3 2 6.36 11 1 1 - 15 13665.426 -20456.719 4.124
'1A5F39 *U5425 C 3 3 6.36 11 1 1 - 16 13928.635 -20500.06 4.124
'1A5F13 *U5425 C 3 3 6.36 11 1 1 - 17 13840.624 -20506.407 4.124
'1A5EC7 *U5425 C 3 2 6.36 11 1 1 - 18 13669.896 -20518.718 4.124
'1A5DBD *U5425 C 3 3 6.36 11 1 1 - 19 13933.106 -20562.059 4.124
'1A5D97 *U5425 C 3 3 6.36 11 1 1 - 20 13845.094 -20568.406 4.124
'1A5D4B *U5425 C 3 2 6.36 11 1 1 - 21 13674.367 -20580.717 4.124
'1A5C41 *U5425 C 3 3 6.36 11 1 1 - 22 13937.577 -20624.058 4.124
'1A5C1B *U5425 C 3 3 6.36 11 1 1 - 23 13849.565 -20630.405 4.124
'1A5BCF *U5425 C 3 2 6.36 11 1 1 - 24 13678.838 -20642.716 4.124
'1A5AC5 *U5425 C 3 3 6.36 11 1 1 - 25 13942.047 -20686.057 4.124
'1A5A9F *U5425 C 3 3 6.36 11 1 1 - 26 13854.036 -20692.404 4.124
'1A5A53 *U5425 C 3 2 6.36 11 1 1 - 27 13683.309 -20704.715 4.124
'1A5949 *U5425 C 3 3 6.36 11 1 1 - 28 13946.518 -20748.056 4.124
'1A5923 *U5425 C 3 3 6.36 11 1 1 - 29 13858.506 -20754.403 4.124
'1A58D7 *U5425 C 3 2 6.36 11 1 1 - 30 13687.779 -20766.714 4.124
'1A57CD *U5425 C 3 3 6.36 11 1 1 - 31 13950.989 -20810.056 4.124
'1A57A7 *U5425 C 3 3 6.36 11 1 1 - 32 13862.977 -20816.402 4.124
'1A575B *U5425 C 3 2 6.36 11 1 1 - 33 13692.25 -20828.713 4.124
'1A5651 *U5425 C 3 3 6.36 11 1 1 - 34 13955.459 -20872.055 4.124
'1A562B *U5425 C 3 3 6.36 11 1 1 - 35 13867.448 -20878.401 4.124
'1A55DF *U5425 C 3 2 6.36 11 1 1 - 36 13696.721 -20890.712 4.124
'1A54D5 *U5425 C 3 3 6.36 11 1 1 - 37 13959.93 -20934.054 4.124
'1A54AF *U5425 C 3 3 6.36 11 1 1 - 38 13871.918 -20940.4 4.124
'1A5463 *U5425 C 3 2 6.36 11 1 1 - 39 13701.191 -20952.711 4.124
'1A5359 *U5350 C 1 3 8.35 16 2 1 - 40 13964.401 -20996.053 4.124
'1A5333 *U5350 C 1 3 8.35 16 2 1 - 41 13876.389 -21002.399 4.124
'1A52E7 *U5350 C 1 2 8.35 16 2 1 - 42 13705.662 -21014.71 4.124
'1A52C1 *U5424 B 2 2 2.4 0 2 - 1 43 13559.532 -20215.069 4.124
'1A529B *U5424 B 2 2 2.4 0 2 - 1 44 13471.52 -20221.416 4.124
'1A5275 *U5424 B 2 2 2.4 0 2 - 1 45 13383.509 -20227.762 4.124
'1A524F *U5424 B 2 2 2.4 0 2 - 1 46 13295.497 -20234.108 4.124
'1A5229 *U5350 B 1 2 2.76 1 2 - 1 47 13207.486 -20240.455 4.124
'1A51DD *U5350 B 1 1 2.76 1 2 - 1 48 13031.463 -20253.148 4.124
'1A51B7 *U5424 B 2 1 2.4 0 2 - 1 49 12943.451 -20259.494 4.124
'1A5191 *U5424 B 2 1 2.4 0 2 - 1 50 12855.44 -20265.84 4.124
'1A516B *U5350 B 1 1 2.76 1 2 - 1 51 12767.428 -20272.187 4.124
'1A5145 *U5349 B 4 2 5.58 9 2 - - 52 13564.002 -20277.068 4.124
'1A511F *U5349 B 4 2 5.58 9 2 - - 53 13475.991 -20283.415 4.124
'1A50F9 *U5349 B 4 2 5.58 9 2 - - 54 13387.979 -20289.761 4.124
'1A50D3 *U5349 B 4 2 5.58 9 2 - - 55 13299.968 -20296.107 4.124
'1A50AD *U5425 B 3 2 2.26 0 1 - 1 56 13211.956 -20302.454 4.124
'1A5061 *U5425 B 3 1 2.26 0 1 - 1 57 13035.933 -20315.147 4.124
'1A503B *U5349 B 4 1 5.58 9 2 - - 58 12947.922 -20321.493 4.124
'1A5015 *U5349 B 4 1 5.58 9 2 - - 59 12859.91 -20327.839 4.124
'1A4FEF *U5425 B 3 1 2.26 0 1 - 1 60 12771.899 -20334.186 4.124
'1A4FC9 *U5349 B 4 2 5.58 9 2 - - 61 13568.473 -20339.067 4.124
'1A4FA3 *U5349 B 4 2 5.58 9 2 - - 62 13480.461 -20345.414 4.124
'1A4F7D *U5349 C 4 2 3.71 4 - - - 63 13392.45 -20351.76 4.124
'1A4F57 *U5349 C 4 2 3.71 4 - - - 64 13304.439 -20358.106 4.124
'1A4F31 *U5425 C 3 2 6.36 11 1 1 - 65 13216.427 -20364.453 4.124
'1A4EE5 *U5425 B 3 1 2.26 0 1 - 1 66 13040.404 -20377.146 4.124
'1A4EBF *U5349 B 4 1 5.58 9 2 - - 67 12952.393 -20383.492 4.124
'1A4E99 *U5349 B 4 1 5.58 9 2 - - 68 12864.381 -20389.838 4.124
'1A4E73 *U5425 B 3 1 2.26 0 1 - 1 69 12776.37 -20396.185 4.124
'1A4E4D *U5349 C 4 2 3.71 4 - - - 70 13572.944 -20401.066 4.124
'1A4E27 *U5349 C 4 2 3.71 4 - - - 71 13484.932 -20407.413 4.124
'1A4E01 *U5349 C 4 2 3.71 4 - - - 72 13396.921 -20413.759 4.124
'1A4DDB *U5349 C 4 2 3.71 4 - - - 73 13308.909 -20420.105 4.124
'1A4DB5 *U5425 C 3 2 6.36 11 1 1 - 74 13220.898 -20426.452 4.124
'1A4D69 *U5425 B 3 1 2.26 0 1 - 1 75 13044.875 -20439.145 4.124
'1A4D43 *U5349 B 4 1 5.58 9 2 - - 76 12956.863 -20445.491 4.124
'1A4D1D *U5349 B 4 1 5.58 9 2 - - 77 12868.852 -20451.837 4.124
'1A4CF7 *U5425 B 3 1 2.26 0 1 - 1 78 12780.84 -20458.184 4.124
'1A4CD1 *U5349 C 4 2 3.71 4 - - - 79 13577.414 -20463.065 4.124
'1A4CAB *U5349 C 4 2 3.71 4 - - - 80 13489.403 -20469.412 4.124
'1A4C85 *U5349 C 4 2 3.71 4 - - - 81 13401.391 -20475.758 4.124
'1A4C5F *U5349 C 4 2 3.71 4 - - - 82 13313.38 -20482.104 4.124
'1A4C39 *U5425 C 3 2 6.36 11 1 1 - 83 13225.368 -20488.451 4.124
'1A4BED *U5425 B 3 1 2.26 0 1 - 1 84 13049.345 -20501.144 4.124
'1A4BC7 *U5349 B 4 1 5.58 9 2 - - 85 12961.334 -20507.49 4.124
'1A4BA1 *U5349 B 4 1 5.58 9 2 - - 86 12873.322 -20513.836 4.124
'1A4B7B *U5425 B 3 1 2.26 0 1 - 1 87 12785.311 -20520.183 4.124
'1A4B55 *U5349 C 4 2 3.71 4 - - - 88 13581.885 -20525.064 4.124
'1A4B2F *U5349 C 4 2 3.71 4 - - - 89 13493.874 -20531.411 4.124
'1A4B09 *U5349 C 4 2 3.71 4 - - - 90 13405.862 -20537.757 4.124
'1A4AE3 *U5349 C 4 2 3.71 4 - - - 91 13317.851 -20544.103 4.124
'1A4ABD *U5425 C 3 2 6.36 11 1 1 - 92 13229.839 -20550.45 4.124
'1A4A71 *U5425 B 3 1 2.26 0 1 - 1 93 13053.816 -20563.143 4.124
'1A4A4B *U5349 B 4 1 5.58 9 2 - - 94 12965.805 -20569.489 4.124
'1A4A25 *U5349 B 4 1 5.58 9 2 - - 95 12877.793 -20575.836 4.124
'1A49FF *U5425 B 3 1 2.26 0 1 - 1 96 12789.782 -20582.182 4.124
'1A49D9 *U5349 B 4 2 5.58 9 2 - - 97 13586.356 -20587.063 4.124
'1A49B3 *U5349 B 4 2 5.58 9 2 - - 98 13498.344 -20593.41 4.124
'1A498D *U5349 B 4 2 5.58 9 2 - - 99 13410.333 -20599.756 4.124
'1A4967 *U5349 B 4 2 5.58 9 2 - - 100 13322.321 -20606.102 4.124
'1A4941 *U5425 B 3 2 2.26 0 1 - 1 101 13234.31 -20612.449 4.124
'1A48F5 *U5425 B 3 1 2.26 0 1 - 1 102 13058.287 -20625.142 4.124
'1A48CF *U5349 B 4 1 5.58 9 2 - - 103 12970.275 -20631.488 4.124
'1A48A9 *U5349 B 4 1 5.58 9 2 - - 104 12882.264 -20637.835 4.124
'1A4883 *U5425 B 3 1 2.26 0 1 - 1 105 12794.252 -20644.181 4.124
'1A485D *U5349 B 4 2 5.58 9 2 - - 106 13590.826 -20649.062 4.124
'1A4837 *U5349 B 4 2 5.58 9 2 - - 107 13502.815 -20655.409 4.124
'1A4811 *U5349 B 4 2 5.58 9 2 - - 108 13414.803 -20661.755 4.124
'1A47EB *U5349 B 4 2 5.58 9 2 - - 109 13326.792 -20668.101 4.124
'1A47C5 *U5425 B 3 2 2.26 0 1 - 1 110 13238.78 -20674.448 4.124
'1A4779 *U5425 B 3 1 2.26 0 1 - 1 111 13062.757 -20687.141 4.124
'1A4753 *U5349 B 4 1 5.58 9 2 - - 112 12974.746 -20693.487 4.124
'1A472D *U5349 B 4 1 5.58 9 2 - - 113 12886.735 -20699.834 4.124
'1A4707 *U5425 B 3 1 2.26 0 1 - 1 114 12798.723 -20706.18 4.124
'1A46E1 *U5349 B 4 2 5.58 9 2 - - 115 13595.297 -20711.061 4.124
'1A46BB *U5349 B 4 2 5.58 9 2 - - 116 13507.286 -20717.408 4.124
'1A4695 *U5349 B 4 2 5.58 9 2 - - 117 13419.274 -20723.754 4.124
'1A466F *U5349 B 4 2 5.58 9 2 - - 118 13331.263 -20730.101 4.124
'1A4649 *U5425 B 3 2 2.26 0 1 - 1 119 13243.251 -20736.447 4.124
'1A45FD *U5425 B 3 1 2.26 0 1 - 1 120 13067.228 -20749.14 4.124
'1A45D7 *U5349 B 4 1 5.58 9 2 - - 121 12979.217 -20755.486 4.124
'1A45B1 *U5349 B 4 1 5.58 9 2 - - 122 12891.205 -20761.833 4.124
'1A458B *U5425 B 3 1 2.26 0 1 - 1 123 12803.194 -20768.179 4.124
'1A4565 *U5349 B 4 2 5.58 9 2 - - 124 13599.768 -20773.06 4.124
'1A453F *U5349 B 4 2 5.58 9 2 - - 125 13511.756 -20779.407 4.124
'1A4519 *U5349 B 4 2 5.58 9 2 - - 126 13423.745 -20785.753 4.124
'1A44F3 *U5349 B 4 2 5.58 9 2 - - 127 13335.733 -20792.1 4.124
'1A44CD *U5425 B 3 2 2.26 0 1 - 1 128 13247.722 -20798.446 4.124
'1A4481 *U5425 B 3 1 2.26 0 1 - 1 129 13071.699 -20811.139 4.124
'1A445B *U5349 B 4 1 5.58 9 2 - - 130 12983.687 -20817.485 4.124
'1A4435 *U5349 B 4 1 5.58 9 2 - - 131 12895.676 -20823.832 4.124
'1A440F *U5425 B 3 1 2.26 0 1 - 1 132 12807.664 -20830.178 4.124
'1A43E9 *U5349 B 4 2 5.58 9 2 - - 133 13604.238 -20835.059 4.124
'1A43C3 *U5349 B 4 2 5.58 9 2 - - 134 13516.227 -20841.406 4.124
'1A439D *U5349 B 4 2 5.58 9 2 - - 135 13428.215 -20847.752 4.124
'1A4377 *U5349 B 4 2 5.58 9 2 - - 136 13340.204 -20854.099 4.124
'1A4351 *U5425 B 3 2 2.26 0 1 - 1 137 13252.192 -20860.445 4.124
'1A4305 *U5425 B 3 1 2.26 0 1 - 1 138 13076.17 -20873.138 4.124
'1A42DF *U5349 B 4 1 5.58 9 2 - - 139 12988.158 -20879.484 4.124
'1A42B9 *U5349 B 4 1 5.58 9 2 - - 140 12900.147 -20885.831 4.124
'1A4293 *U5425 B 3 1 2.26 0 1 - 1 141 12812.135 -20892.177 4.124
'1A426D *U5349 B 4 2 5.58 9 2 - - 142 13608.709 -20897.058 4.124
'1A4247 *U5349 B 4 2 5.58 9 2 - - 143 13520.698 -20903.405 4.124
'1A4221 *U5349 B 4 2 5.58 9 2 - - 144 13432.686 -20909.751 4.124
'1A41FB *U5349 B 4 2 5.58 9 2 - - 145 13344.675 -20916.098 4.124
'1A41D5 *U5425 B 3 2 2.26 0 1 - 1 146 13256.663 -20922.444 4.124
'1A4189 *U5425 B 3 1 2.26 0 1 - 1 147 13080.64 -20935.137 4.124
'1A4163 *U5349 B 4 1 5.58 9 2 - - 148 12992.629 -20941.483 4.124
'1A413D *U5349 B 4 1 5.58 9 2 - - 149 12904.617 -20947.83 4.124
'1A4117 *U5425 B 3 1 2.26 0 1 - 1 150 12816.606 -20954.176 4.124
'1A40F1 *U5349 B 4 2 5.58 9 2 - - 151 13613.18 -20959.057 4.124
'1A40CB *U5349 B 4 2 5.58 9 2 - - 152 13525.168 -20965.404 4.124
'1A40A5 *U5349 B 4 2 5.58 9 2 - - 153 13437.157 -20971.75 4.124
'1A407F *U5349 B 4 2 5.58 9 2 - - 154 13349.145 -20978.097 4.124
'1A4059 *U5425 B 3 2 2.26 0 1 - 1 155 13261.134 -20984.443 4.124
'1A400D *U5425 B 3 1 2.26 0 1 - 1 156 13085.111 -20997.136 4.124
'1A3FE7 *U5349 B 4 1 5.58 9 2 - - 157 12997.099 -21003.482 4.124
'1A3FC1 *U5349 B 4 1 5.58 9 2 - - 158 12909.088 -21009.829 4.124
'1A3F9B *U5425 B 3 1 2.26 0 1 - 1 159 12821.076 -21016.175 4.124
'1A3F75 *U5424 B 2 2 2.4 0 2 - 1 160 13617.65 -21021.056 4.124
'1A3F4F *U5424 B 2 2 2.4 0 2 - 1 161 13529.639 -21027.403 4.124
'1A3F29 *U5424 B 2 2 2.4 0 2 - 1 162 13441.628 -21033.749 4.124
'1A3F03 *U5424 B 2 2 2.4 0 2 - 1 163 13353.616 -21040.096 4.124
'1A3EDD *U5350 B 1 2 2.76 1 2 - 1 164 13265.605 -21046.442 4.124
'1A3E91 *U5350 B 1 1 2.76 1 2 - 1 165 13089.582 -21059.135 4.124
'1A3E6B *U5424 B 2 1 2.4 0 2 - 1 166 13001.57 -21065.481 4.124
'1A3E45 *U5424 B 2 1 2.4 0 2 - 1 167 12913.559 -21071.828 4.124
'1A3E1F *U5350 B 1 1 2.76 1 2 - 1 168 12825.547 -21078.174 4.124

View File

@@ -1,5 +1,4 @@
import unittest import unittest
import mock
from numpy.testing import assert_array_equal, assert_equal from numpy.testing import assert_array_equal, assert_equal
from helix.constants.module_type import ModuleType from helix.constants.module_type import ModuleType
@@ -10,15 +9,11 @@ from helix.models.subarray import Subarray
from helix.presenters.panel_presenter import ProjectPresenter from helix.presenters.panel_presenter import ProjectPresenter
from helix.constants.system_type import SystemType from helix.constants.system_type import SystemType
from helix.constants.panel_type import PanelType from helix.constants.panel_type import PanelType
from test.test_helpers import feature_is_always_active
import flask_featureflags
flask_featureflags.is_active = feature_is_always_active
class PanelPresenterTest(unittest.TestCase): class PanelPresenterTest(unittest.TestCase):
def test_get_table_data_single_tilt_96cell(self): def test_get_table_data_single_tilt_96cell(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96) self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
panels = [ panels = [
@@ -71,81 +66,16 @@ class PanelPresenterTest(unittest.TestCase):
def test_get_buildings_data(self): def test_get_buildings_data(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96) self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
buildings = [ [ Coordinate(0,0), Coordinate(60,0), Coordinate(60,60), Coordinate(0,60) ] ] # big square buildings = [ [ Coordinate(-60,-60), Coordinate(60,-60), Coordinate(60,60), Coordinate(-60,60) ] ] # big square
expected_buildings = [[ expected_buildings = [[
{'x': 0, '_Coordinate__rounded_x': 0, 'y': 60, '_Coordinate__rounded_y': 0, 'rotation': 0.0}, {'x': -60, '_Coordinate__rounded_x': -60, 'y': -60, '_Coordinate__rounded_y': -60, 'rotation': 0.0},
{'x': 60, '_Coordinate__rounded_x': 60, 'y': 60, '_Coordinate__rounded_y': 0, 'rotation': 0.0}, {'x': 60, '_Coordinate__rounded_x': 60, 'y': -60, '_Coordinate__rounded_y': -60, 'rotation': 0.0},
{'x': 60, '_Coordinate__rounded_x': 60, 'y': 0, '_Coordinate__rounded_y': 60, 'rotation': 0.0}, {'x': 60, '_Coordinate__rounded_x': 60, 'y': 60, '_Coordinate__rounded_y': 60, 'rotation': 0.0},
{'x': 0, '_Coordinate__rounded_x': 0, 'y': 0, '_Coordinate__rounded_y': 60, 'rotation': 0.0} {'x': -60, '_Coordinate__rounded_x': -60, 'y': 60, '_Coordinate__rounded_y': 60, 'rotation': 0.0}
]] ]]
actual_buildings = self.subject.get_buildings(buildings,60) actual_buildings = self.subject.get_buildings(buildings)
assert_array_equal(actual_buildings,expected_buildings) assert_array_equal(actual_buildings,expected_buildings)
def test_get_corners_fancy_building(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
buildings = [ [ [2, 5], [2, 3], [0,3], [0, 0], [2,0], [2,-3], [6,-3], [8,-1], [8,1], [6,3], [4,3], [4,5] ] ] # fancy builgin with three interior corners
expected_corners = [[
{'length_ccw': 2.0, 'angle': 90.0, 'y': 5, 'x': 4, 'length_cw': 2.0},
{'length_ccw': 2.0, 'angle': 90.0, 'y': 5, 'x': 2, 'length_cw': 2.0},
{'length_ccw': 3.0, 'angle': 90.0, 'y': 3, 'x': 0, 'length_cw': 2.0},
{'length_ccw': 2.0, 'angle': 90.0, 'y': 0, 'x': 0, 'length_cw': 3.0},
{'length_ccw': 4.0, 'angle': 90.0, 'y': -3, 'x': 2, 'length_cw': 3.0}]]
actual_corners = self.subject.get_corners(buildings)
assert_array_equal(actual_corners,expected_corners)
#@mock.patch('flask_featureflags.is_active',side_effect=feature_is_always_active)
def test_get_corners_box_building(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
buildings = [ [ [0, 0], [60, 0], [60,60], [0, 60] ] ] # big square
expected_corners = [[
{'x': 0, 'y': 60, 'length_cw': 60, 'length_ccw':60, 'angle':90},
{'x': 0, 'y': 0, 'length_cw': 60, 'length_ccw':60, 'angle':90},
{'x': 60, 'y': 0, 'length_cw': 60, 'length_ccw':60, 'angle':90},
{'x': 60, 'y': 60, 'length_cw': 60, 'length_ccw':60, 'angle':90}
]]
actual_corners = self.subject.get_corners(buildings)
assert_array_equal(actual_corners,expected_corners)
#@mock.patch('flask_featureflags.is_active',side_effect=feature_is_always_active)
def test_get_corners_box_building_rotated_30_degrees(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
buildings = [ [ [0, 0], [51.96, 30], [21.96, 81.96], [-30, 51.96] ] ] # big square
expected_corners = [[
{'x': -30, 'y': 51.96, 'length_cw': 59.998679985479676, 'length_ccw':59.99867998547968, 'angle':90},
{'x': 0, 'y': 0, 'length_cw': 59.99867998547968, 'length_ccw':59.99867998547968, 'angle':90},
{'x': 51.96, 'y': 30, 'length_cw': 59.99867998547968, 'length_ccw':59.998679985479676, 'angle':90},
{'x': 21.96, 'y': 81.96, 'length_cw': 59.998679985479676, 'length_ccw':59.998679985479676, 'angle':90.00000000000001}
]]
actual_corners = self.subject.get_corners(buildings)
assert_array_equal(actual_corners,expected_corners)
#@mock.patch('flask_featureflags.is_active',side_effect=feature_is_always_active)
def test_get_corners_building(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
buildings = [ [ [-3.42, 1.51], [-1.66, -1.64], [4.22, -0.87], [-0.8, 5.64]] ]
expected_corners = [[
{'x': -0.8, 'y': 5.64, 'length_cw': 8.22073597678456, 'length_ccw':4.890940604832571, 'angle':70.02691327912069},
{'x': -3.42, 'y': 1.51, 'length_cw': 4.890940604832571, 'length_ccw':3.6083375673570233, 'angle':118.41626123074676},
{'x': -1.66, 'y': -1.64, 'length_cw': 3.6083375673570233, 'length_ccw':5.930202357424239, 'angle':111.73284308914216},
{'x': 4.22, 'y': -0.87, 'length_cw': 5.930202357424239, 'length_ccw':8.22073597678456, 'angle':59.82398240099042}
]]
actual_corners = self.subject.get_corners(buildings)
assert_array_equal(actual_corners,expected_corners)
#@mock.patch('flask_featureflags.is_active',side_effect=feature_is_always_active)
def test_get_corners_wild_building_with_big_angles(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
buildings = [ [ [-3.58, 3.32], [-0.78, -2.9], [-1.56,0.88], [0.66, -2.16], [1.5, 1.16], [2.72, 2.36], [-0.8, 5.64] ] ]
expected_corners = [[
{'x': -0.8, 'y': 5.64, 'length_cw': 4.811319985201567, 'length_ccw':3.6208838699963852, 'angle':97.17527350158385},
{'x': -3.58, 'y': 3.32, 'length_cw': 3.6208838699963852, 'length_ccw':6.821172919667115, 'angle':105.6106862572924},
{'x': -0.78, 'y': -2.9, 'length_cw': 6.821172919667115, 'length_ccw':3.8596372886580936, 'angle':12.576112527956782},
{'x': 0.66, 'y': -2.16, 'length_cw': 3.7643060449437424, 'length_ccw':3.424616766880639, 'angle':50.337833086033},
{'x': 2.72, 'y': 2.36, 'length_cw': 1.7112568480505783, 'length_ccw':4.811319985201567, 'angle':87.50512700090906}
]]
actual_corners = self.subject.get_corners(buildings)
assert_array_equal(actual_corners,expected_corners)
def test_get_max_y(self): def test_get_max_y(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96) self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
panels = [ panels = [

View File

@@ -1,35 +0,0 @@
from locust import HttpLocust, TaskSet, task
class UserBehavior(TaskSet):
@task(1)
def wizard_sequence(self):
# files => enctype=multipart/form-data
self.client.post('/site_characterization/', files={
'project_name': (None, 'Amazon Fresno'),
'system_type': (None, '0'),
'module_type': (None, '96 Cell'),
'building_height': (None, '30'),
'building_width': (None, '600'),
'building_length': (None, '1500'),
'building_parapet_height': (None, '0'),
'wind_speed': (None, '110'),
'exposure_category': (None, 'C'),
'exposure_category_transition_distance': (None, '0'),
'ballast_block_weight': (None, '14.0'),
'max_system_pressure': (None, '12.0'),
'anchor_type': (None, 'OMG PowerGrip Plus'),
'design_spectral_response': (None, '0'),
'importance_factor': (None, '1'),
})
self.client.get('/array_summary/')
self.client.post('/array_summary/', files={'dxf_upload': open('Fresno First Half.dxf', 'rb')})
class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 500
max_wait = 5000
# locust -f locustfile.py --host=http://localhost:5000

View File

@@ -105,7 +105,3 @@ def assert_image_equal(image_1, image_2, error=5e-2):
average_error = total_error / pixels average_error = total_error / pixels
assert average_error <= error, "Images are not equal to within %f error (got %f)" % (error, average_error) assert average_error <= error, "Images are not equal to within %f error (got %f)" % (error, average_error)
# used for mocking response of feature flags
def feature_is_always_active(feature_name):
return True

View File

@@ -103,4 +103,4 @@ class CsvInputValidatorTest(unittest.TestCase):
self.user_values.module_system_constants().panel_spacing = (88.24, 62.0) self.user_values.module_system_constants().panel_spacing = (88.24, 62.0)
with open('test/fixtures/invalid_too_close.txt', 'r', newline='') as csv_file: with open('test/fixtures/invalid_too_close.txt', 'r', newline='') as csv_file:
cad_input = csv_file.read() cad_input = csv_file.read()
#self.should_have_error(self.subject.validate(cad_input), FileValidationMessage.PanelsTooClose , None) self.should_have_error(self.subject.validate(cad_input), FileValidationMessage.PanelsTooClose , None)

View File

@@ -28,12 +28,12 @@ class FileValidatorTest(unittest.TestCase):
with open('test/fixtures/input_single_tilt.csv', 'r', with open('test/fixtures/input_single_tilt.csv', 'r',
newline='') as file: newline='') as file:
cad_input = file.read() cad_input = file.read()
eq_(self.subject.validate(cad_input, FileType.Csv, fake_file), None) eq_(self.subject.validate(cad_input, fake_file, FileType.Csv), None)
def test_unknown_files_are_invalid(self): def test_unknown_files_are_invalid(self):
fake_file = MagicMock() fake_file = MagicMock()
type(fake_file).filename = PropertyMock(return_value="Hi") type(fake_file).filename = PropertyMock(return_value="Hi")
result = self.subject.validate("Hi", FileType.Unknown, fake_file) result = self.subject.validate("Hi", fake_file, FileType.Unknown)
self.should_have_error(result, self.should_have_error(result,
FileValidationMessage.UnknownFileUploaded, None) FileValidationMessage.UnknownFileUploaded, None)
@@ -62,7 +62,8 @@ class FileValidatorTest(unittest.TestCase):
fake_file.read.return_value = open(fname, "rb").read() fake_file.read.return_value = open(fname, "rb").read()
fake_file.filename.return_value = "expected_dual_tilt_pseries_image.png" fake_file.filename.return_value = "expected_dual_tilt_pseries_image.png"
stream = self.subject.obtain_stream(fake_file) stream = self.subject.obtain_stream(fake_file)
self.should_have_error(self.subject.validate(stream, FileType.AuroraDxf, fake_file), self.should_have_error(self.subject.validate(stream, fake_file,
FileType.AuroraDxf),
FileValidationMessage.ExpectedDxfFile, FileValidationMessage.ExpectedDxfFile,
None) None)