diff --git a/Dockerfile b/Dockerfile
index 1e7fbd8..2ca32e8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-from python:3.5
+from python:3.6
RUN apt-get update
diff --git a/helix/constants/anchor_type.py b/helix/constants/anchor_type.py
index ba02ea6..fc4a6c7 100644
--- a/helix/constants/anchor_type.py
+++ b/helix/constants/anchor_type.py
@@ -5,6 +5,7 @@ from helix.constants.global_constants import system_force_capacity
class AnchorType(Enum):
+ # These values are being used by Salesforce integration, do not change it
OMG_PowerGrip = 'OMG PowerGrip'
OMG_PowerGrip_Plus = 'OMG PowerGrip Plus'
EcoFasten = 'EcoFasten Eco 65'
diff --git a/helix/constants/global_constants.py b/helix/constants/global_constants.py
index 1476765..85e2638 100644
--- a/helix/constants/global_constants.py
+++ b/helix/constants/global_constants.py
@@ -8,4 +8,4 @@ system_force_capacity = 418.
minimum_racking_capacity = 226
-max_corner_angle = 135
\ No newline at end of file
+max_corner_angle = 135
diff --git a/helix/constants/module_type.py b/helix/constants/module_type.py
index fa61097..23e8fd6 100644
--- a/helix/constants/module_type.py
+++ b/helix/constants/module_type.py
@@ -2,6 +2,7 @@ from enum import Enum
class ModuleType(Enum):
+ # These values are being used by Salesforce integration, do not change it
Cell96 = '96 Cell'
Cell128 = '128 Cell'
PSeries = 'P-Series'
diff --git a/helix/main.py b/helix/main.py
index 94b0c83..08589ca 100644
--- a/helix/main.py
+++ b/helix/main.py
@@ -4,7 +4,7 @@ from urllib.parse import urlparse
import rollbar
import rollbar.contrib.flask
from flask import Flask, request, make_response, session, render_template, \
- redirect, url_for, jsonify
+ redirect, url_for, jsonify, flash
from flask import got_request_exception
from flask.ext import assets
from flask_oauthlib.client import OAuth
@@ -29,17 +29,32 @@ from helix.forms.input_form import InputForm, ArrayForm, TestDXFForm
from helix.models.dxf.dxf_error import DXFError, OldDxfFormatException
from helix.presenters.image_presenter import ImagePresenter
from helix.presenters.panel_presenter import ProjectPresenter
+from helix.qa_helper import QAScenario
from helix.session_manager import SessionManager
from helix.validators.file_validator import FileValidator, FileType
from helix.validators.subarray_validator import SubarrayValidator
+from flask_featureflags import FeatureFlag
+import flask_featureflags as feature
+import pprint
app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api')
app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey')
app.config['PROFILE'] = True
+app.config['FEATURE_FLAGS'] = {
+ 'ff_dummy_feature': True,
+ 'ff_cpp': True
+}
-# Sales Force integrations
+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:
@@ -53,7 +68,7 @@ try:
authorize_url=SFDC_BASE_URL + '/services/oauth2/authorize',
)
except TypeError:
- print('Sales Force integration disabled')
+ print('Salesforce integration disabled')
sales_force = None
@@ -90,8 +105,8 @@ def init_rollbar():
got_request_exception.connect(rollbar.contrib.flask.report_exception, app)
-def is_sfdc_session():
- return 'SFID' in session
+def is_sales_force_session():
+ return 'sf_session' in session
@app.route("/")
@@ -136,7 +151,7 @@ def test_dxf():
# wizard steps
@app.route("/site_characterization/", methods=['GET', 'POST'])
def site_characterization():
- if is_sfdc_session():
+ if is_sales_force_session():
return redirect('/summary/')
db_session = sql_constant.sql_session_maker()
@@ -184,14 +199,14 @@ def summary():
else:
context['no_proceed'] = True
- if is_sfdc_session():
+ if is_sales_force_session():
context['hide_back'] = True
db_session.close()
return render_template('site_summary.html.jinja', context=context)
-def handle_dxf_file(session_manager, file_contents, filename=None, save_file=True):
+def handle_dxf_file(session_manager, file_contents, filename=None):
errors = []
user_values = session_manager.user_values()
validator = FileValidator(user_values)
@@ -215,8 +230,7 @@ def handle_dxf_file(session_manager, file_contents, filename=None, save_file=Tru
DXFHelper(),
SubarrayValidator())
csv = CsvBuilder().build_cad_output(dxf_data['panels'])
- if save_file:
- session_manager.save_uploaded_file(csv, dxf_file_name=filename)
+ session_manager.save_uploaded_file(csv, dxf_file_name=filename)
buildings = dxf_data['buildings']
session_manager.save_buildings_polygons(buildings)
@@ -264,6 +278,8 @@ def array_summary():
file_contents = validator.obtain_stream(file)
success, errors = handle_dxf_file(session_manager, file_contents, filename=file.filename)
if success:
+ if is_sales_force_session():
+ session['sf_session']['new_dxf_file'] = True
return redirect(url_for('array_summary'))
else:
array_form.dxf_upload.errors.extend(errors)
@@ -317,7 +333,10 @@ def array_summary():
elif not context['site_data_available']:
context['no_proceed'] = True
- if is_sfdc_session() and 'dxf_link_loaded' not in session:
+ 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()
@@ -326,14 +345,21 @@ def array_summary():
@app.route("/load_dxf/", methods=['GET', 'POST'])
def load_dxf_file():
- if 'dxf_link' not in session:
+ 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['dxf_link']
- response = requests.get(dxf_link)
+ 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('/')
@@ -341,12 +367,12 @@ def load_dxf_file():
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, save_file=False)
- session['dxf_link_loaded'] = True
+ success, errors = handle_dxf_file(session_manager, file_contents, filename=filename)
+ session['sf_session']['dxf_link_loaded'] = True
if success:
- return jsonify({'status': 'success'})
+ return jsonify({'status': 'success', 'messages': ['DXF from Salesforce loaded successfully.']})
else:
- errors = ['Unable to download DXF file from Sales Force ({})'.format(response.status_code)]
+ errors = ['Unable to download DXF file from Salesforce ({})'.format(response.status_code)]
response = jsonify({'errors': errors})
response.status_code = 400
@@ -431,10 +457,11 @@ def download():
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
context = session_manager.context()
context['current_step'] = 5
- error, data = session.pop('sfdc_export_urls', (None, None))
- if data is not None:
- context['sfdc_export_error'] = error
- context['sfdc_export_urls'] = data
+ 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()
return render_template('download.html.jinja', context=context)
@@ -524,55 +551,83 @@ def helix_documentation():
return render_template('helix_documentation.jinja', context=context)
-# Sales Force Integration
+# 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')
+ # To test it locally: https://localhost:8443/sales_force_login?sfid=a3cL00000004QsQIAU
+ sfid = request.args.get('sfid')
if sfid:
session.clear()
- session['SFID'] = sfid
- return sales_force.authorize(callback=url_for('sales_force_authorized', _external=True, SFID=sfid), SFID=sfid)
+ 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')
- resp = sales_force.authorized_response()
+ 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:
- print('Unable to authenticate to SFDC.')
+ flash('Unknown response from Salesforce.')
+ print('Unknown response from Salesforce.')
return redirect(next_url)
- print('New Sales Force - OAuth2 login')
+ print('New Salesforce - OAuth2 login')
db_session = sql_constant.sql_session_maker()
session_manager = SessionManager(session, redis_constant.redis_store, db_session)
- session['sales_force_token'] = resp['access_token']
+ session['sf_session']['access_token'] = resp['access_token']
- data = sf_tasks.get_site_characterization_from_sales_force(session)
- if data:
- session['dxf_link'] = data['dxf_link']
- session_manager.save_form_submission(data)
- return redirect(next_url)
- else:
+ 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_sfdc_session():
+ 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']
- access_token = session['sales_force_token']
- sfid = session['SFID']
- error, data = sf_tasks.export_to_sfdc(helix_session_id, access_token, sfid)
+ 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'] = session['dxf_link']
- session['sfdc_export_urls'] = (error, data)
+ data['dxfUrlFromSF'] = sf_session['dxf_link']
+ sf_session['export_urls'] = (error, data)
db_session.close()
return redirect('/download')
@@ -580,19 +635,55 @@ def export_sfdc():
if sales_force:
@sales_force.tokengetter
def get_sales_force_token(token=None):
- return session.get('sales_force_token')
+ return session.get('sf_session', {}).get('access_token', None)
@app.route('/sales_force_logout')
def sales_force_logout():
- session.pop('SFID', None)
- session.pop('sales_force_token', None)
- session.pop('dxf_link', None)
- session.pop('dxf_link_loaded', None)
- session.pop('sfdc_export_urls', None)
- session.clear()
+ session.pop('sf_session', None)
return redirect('/')
-# End of Sales Force Integration
+# 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: ' + test + '.'
+ if bad_request:
+ tests = ''.join(['
' + s + '' for s in QAScenario.all()])
+ tests = ''
+ 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 = '' + test + ' test enabled.'
+ if url:
+ msg += ' Access: ' + url + ' to check.'
+ return msg, 200
@app.template_filter('format_number')
diff --git a/helix/models/corner.py b/helix/models/corner.py
index 24e474f..fa6166e 100644
--- a/helix/models/corner.py
+++ b/helix/models/corner.py
@@ -10,4 +10,4 @@ class Corner(object):
@property
def dictionary(self):
- return {"x": self.x, "y": self.y, "length ccw": self.length_ccw, "length cw : ": self.length_cw, "angle": self.angle}
+ return {"x": self.x, "y": self.y, "length_ccw": self.length_ccw, "length_cw": self.length_cw, "angle": self.angle}
diff --git a/helix/presenters/panel_presenter.py b/helix/presenters/panel_presenter.py
index 22ecae0..dd10bb2 100644
--- a/helix/presenters/panel_presenter.py
+++ b/helix/presenters/panel_presenter.py
@@ -2,6 +2,7 @@ import sys
from math import sqrt, atan, degrees
from helix.models.corner import Corner
from helix.constants.global_constants import max_corner_angle
+import flask_featureflags as feature
class ProjectPresenter(object):
def __init__(self, system_type, module_type):
@@ -76,40 +77,41 @@ class ProjectPresenter(object):
def get_corners(self, buildings):
result = []
- for building in buildings:
- presentable_building = []
- result.append(presentable_building)
- previous_corner = building[-1]
- for i, corner in enumerate(building):
- if (i+1 == len(building)):
- next_corner = building[0]
- else:
- next_corner = building[i+1]
+ if feature.is_active('ff_cpp'):
+ for building in buildings:
+ presentable_building = []
+ result.append(presentable_building)
+ previous_corner = building[-1]
+ for i, corner in enumerate(building):
+ if (i+1 == len(building)):
+ next_corner = building[0]
+ else:
+ next_corner = building[i+1]
- #x coordinate is stored as first element of corner variable
- #y coordinate is stored as second element of corner variable
- corner_length_ccw = sqrt((next_corner[0] - corner[0])**2 + (next_corner[1] - corner[1])**2)
- corner_length_cw = sqrt((previous_corner[0] - corner[0])**2 + (previous_corner[1] - corner[1])**2)
+ #x coordinate is stored as first element of corner variable
+ #y coordinate is stored as second element of corner variable
+ corner_length_ccw = sqrt((next_corner[0] - corner[0])**2 + (next_corner[1] - corner[1])**2)
+ corner_length_cw = sqrt((previous_corner[0] - corner[0])**2 + (previous_corner[1] - corner[1])**2)
- k1 = float('Inf') if (corner[0]==previous_corner[0]) else (corner[1] - previous_corner[1]) / (corner[0] - previous_corner[0])
- k2 = float('Inf') if (corner[0]==next_corner[0]) else (next_corner[1] - corner[1]) / (next_corner[0] - corner[0])
+ k1 = float('Inf') if (corner[0]==previous_corner[0]) else (corner[1] - previous_corner[1]) / (corner[0] - previous_corner[0])
+ k2 = float('Inf') if (corner[0]==next_corner[0]) else (next_corner[1] - corner[1]) / (next_corner[0] - corner[0])
- theta2 = degrees(atan(k2))
- theta1 = degrees(atan(k1)) - theta2
+ theta2 = degrees(atan(k2))
+ theta1 = degrees(atan(k1)) - theta2
- if (theta1 < 0):
- if (k1 < 0 and k2 < 0):
- theta1 = 360 + theta1
- elif (k1 <= 0 and k2 >= 0):
- theta1 = 180 + theta1
- else:
- if (k1 > 0 and k2 > 0):
- theta1 = 180 + theta1
+ if (theta1 < 0):
+ if (k1 < 0 and k2 < 0):
+ theta1 = 360 + theta1
+ elif (k1 <= 0 and k2 >= 0):
+ theta1 = 180 + theta1
+ else:
+ if (k1 > 0 and k2 > 0):
+ theta1 = 180 + theta1
- if (theta1 < max_corner_angle):
- presentable_building.append(Corner(corner[0], corner[1], corner_length_ccw,corner_length_cw, theta1).__dict__)
+ if (theta1 < max_corner_angle):
+ presentable_building.append(Corner(corner[0], corner[1], corner_length_ccw,corner_length_cw, theta1).__dict__)
- previous_corner = corner
+ previous_corner = corner
return result
diff --git a/helix/qa_helper.py b/helix/qa_helper.py
new file mode 100644
index 0000000..dadc947
--- /dev/null
+++ b/helix/qa_helper.py
@@ -0,0 +1,20 @@
+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)]
diff --git a/helix/sales_force/tasks.py b/helix/sales_force/tasks.py
index 0b72e4f..61a4a9f 100644
--- a/helix/sales_force/tasks.py
+++ b/helix/sales_force/tasks.py
@@ -1,6 +1,5 @@
import io
import os
-import uuid
import requests
@@ -12,45 +11,50 @@ 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(session):
- access_token = session['sales_force_token']
- sfid = session['SFID']
- helix_id = session['id']
+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)}
- result = requests.get(url, headers=headers, params={'SFID': sfid, 'helix_session_id': helix_id})
+ 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
+ return data, 200
else:
- print('Error while getting data from Sales Force: {}'.format(result.status_code))
+ 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'] == 'Single-Tilt':
+ if data['system_type'] in ('Single-Tilt', 'Single Tilt'):
data['system_type'] = SystemType.singleTilt.value
- elif data['system_type'] == 'Dual-Tilt':
+ 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):
- step = 'Exporting to SFDC'
+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'
@@ -83,36 +87,43 @@ def export_to_sfdc(helix_session_id, access_token, sfid):
# 5. Save CSV/PDF/DXF files into AWS-S3
step = 'Uploading to S3'
- filename = uuid.uuid4().hex
- bom_csv_url = s3_upload(io.StringIO(csv_file), filename=filename + '.csv')
- doc_url = s3_upload(io.BytesIO(document), filename=filename + '.pdf')
- bom_json_url = s3_upload(io.StringIO(bom_list_json), filename=filename + '.json')
- if dxf_contents: # Optional
- dxf_url = s3_upload(io.StringIO(dxf_contents), filename=filename + '.dxf')
+ 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 SFDC'
+ 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,
+ '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, data=data, timeout=30)
+ result = requests.post(url, headers=headers, json=data, timeout=30)
# 7. Internal logs
if result.status_code <= 299:
- print('Sales Force notified successfully for session {}.'.format(helix_session_id))
+ print('Salesforce notified successfully for session {}.'.format(helix_session_id))
# print(result.content)
error = None
else:
- error = 'Helix wasn\'t able to notify the Sales Force ({})'.format(result.status_code)
+ error = 'Helix Calculator was not able to notify Salesforce ({})'.format(result.status_code)
+
print(error)
print(result.content)
diff --git a/helix/scss/forms.scss b/helix/scss/forms.scss
index a1116d4..cae1638 100644
--- a/helix/scss/forms.scss
+++ b/helix/scss/forms.scss
@@ -72,6 +72,12 @@ i.icon-info-circled {
margin: 0 10px;
}
+.info_message {
+ color: black;
+ padding-left: 5px;
+ margin: 0 auto;
+}
+
.error_message {
color: red;
padding-left: 5px;
diff --git a/helix/session_manager.py b/helix/session_manager.py
index 388e2e4..c92ed8c 100644
--- a/helix/session_manager.py
+++ b/helix/session_manager.py
@@ -81,17 +81,17 @@ class SessionManager(object):
building_height=form_data.get('building_height', 0),
building_width=form_data.get('building_width', 0),
building_length=form_data.get('building_length', 0),
- parapet_height=form_data.get('building_parapet_height', 0),
+ parapet_height=form_data.get('parapet_height', form_data.get('building_parapet_height', 0)),
wind_speed=form_data.get('wind_speed', 0),
exposure_category=form_data.get('exposure_category', 'C'),
exposure_transition_distance=form_data.get('exposure_category_transition_distance', 0),
- ballast_block_weight=form_data.get('ballast_block_weight', 0),
- max_psf=form_data.get('max_system_pressure', 0),
+ ballast_block_weight=form_data.get('ballast_block_weight', form_data.get('ballast_weight', 0)),
+ max_psf=form_data.get('system_pressure', form_data.get('max_system_pressure', form_data.get('max_psf', 0))),
system_type=form_data.get('system_type', SystemType.dualTilt.value),
module_type=form_data.get('module_type', ModuleType.Cell96.value),
anchor_type=form_data.get('anchor_type', AnchorType.OMG_PowerGrip_Plus.value),
- spectral_response=form_data.get('design_spectral_response', 1),
- seismic_importance_factor=form_data.get('importance_factor', 1)
+ spectral_response=form_data.get('design_spectral_response', form_data.get('spectral_response_acceleration', form_data.get('spectral_response', 1))),
+ seismic_importance_factor=form_data.get('seismic_importance_factor', form_data.get('importance_factor', 1))
)
self.db_session.add(self.site)
self.db_session.commit()
diff --git a/helix/static/css/main.css b/helix/static/css/main.css
index 6f35df5..872cde7 100644
--- a/helix/static/css/main.css
+++ b/helix/static/css/main.css
@@ -616,13 +616,20 @@ i.icon-info-circled {
}
/* line 75 */
+.info_message {
+ color: black;
+ padding-left: 5px;
+ margin: 0 auto;
+}
+
+/* line 81 */
.error_message {
color: red;
padding-left: 5px;
margin: 0 auto;
}
-/* line 81 */
+/* line 87 */
input, select {
width: calc(100% - 20px);
padding: 10px;
@@ -633,7 +640,7 @@ input, select {
letter-spacing: 2px;
}
-/* line 91 */
+/* line 97 */
select {
-webkit-appearance: none;
-moz-appearance: none;
@@ -641,14 +648,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);
}
-/* line 98 */
+/* line 104 */
.submit, .download {
padding: 20px;
text-align: center;
border-top: 1px solid #EAEAEA;
}
-/* line 104 */
+/* line 110 */
.button, button {
background: #5199F5;
color: white;
@@ -660,7 +667,7 @@ select {
letter-spacing: 2px;
}
-/* line 115 */
+/* line 121 */
.navigation_buttons {
display: flex;
justify-content: flex-end;
@@ -668,18 +675,18 @@ select {
border-bottom: 1px solid #EAEAEA;
padding: 20px;
}
-/* line 122 */
+/* line 128 */
.navigation_buttons a {
text-decoration: none;
width: auto;
margin: 0 20px;
}
-/* line 128 */
+/* line 134 */
.navigation_buttons .button {
width: auto;
}
-/* line 133 */
+/* line 139 */
.back {
border: 2px solid #5199F5;
color: #5199F5;
@@ -687,12 +694,12 @@ select {
margin-left: 10px;
}
-/* line 140 */
+/* line 146 */
a.back {
text-decoration: none;
}
-/* line 144 */
+/* line 150 */
.sample_images {
display: flex;
justify-content: flex-end;
@@ -700,18 +707,18 @@ a.back {
border-bottom: 1px solid #EAEAEA;
padding: 15px;
}
-/* line 151 */
+/* line 157 */
.sample_images a {
text-decoration: none;
width: auto;
margin: 2px 5px;
}
-/* line 157 */
+/* line 163 */
.sample_images .button {
width: auto;
}
-/* line 162 */
+/* line 168 */
.image_button, button {
background: #5199F5;
color: white;
diff --git a/helix/static/javascripts/auto_dxf_load.js b/helix/static/javascripts/auto_dxf_load.js
index 7ae3a91..a3e97bc 100644
--- a/helix/static/javascripts/auto_dxf_load.js
+++ b/helix/static/javascripts/auto_dxf_load.js
@@ -2,21 +2,29 @@
$(document).ready(function () {
$("#sf_msg_container").empty();
- $("#sf_msg_container").show();
$('#sf-spinner-panel').css('width', '100%'); // Workaround for Safari issue
$.ajax({
type: 'POST',
url: '/load_dxf/'
- }).done(function(r) {
- console.log(r)
- $("#sf_msg_container").append('DXF from Sales Force loaded successfully.');
- setTimeout(function() { window.location.reload() }, 500);
- }).fail(function(r) {
- // console.log(r.status)
- r.responseJSON.errors.forEach((msg) => {
- $("#sf_msg_container").append('' + msg + '');
- })
- }).always(function() {
+ }).done(function(data, textStatus, jqXHR) { // 200 or 202
+ if (data && data.messages) {
+ data.messages.forEach((msg) => {
+ $("#sf_msg_container").append('' + msg + '');
+ })
+ $("#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('' + msg + '');
+ })
+ $("#sf_msg_container").show();
+ }
+ }).always(function(r) {
$('#sf-spinner-panel').css('width', '0%'); // Workaround for Safari issue
});
});
diff --git a/helix/static/javascripts/power_station_configuration.js b/helix/static/javascripts/power_station_configuration.js
index 7f680da..54826ab 100644
--- a/helix/static/javascripts/power_station_configuration.js
+++ b/helix/static/javascripts/power_station_configuration.js
@@ -73,7 +73,7 @@
}
});
inverterStringsPerInverter.val(inverterStringsPerInverterValue);
- inverterStringsPerInverterForm.css('display', 'inherit');
+ inverterStringsPerInverterForm.css('display', 'block');
} else {
inverterStringsPerInverter.append($(''));
inverterStringsPerInverterForm.css('display', 'none');
@@ -160,7 +160,8 @@
if (data) {
$('#standalone_inverter_id').val(uuid);
$('#standalone_ac_run_length').val(data['ac_run_length']);
- $('#attachment-point').val(data['attachment_point'][0]);
+ var attach_point = data['attachment_point'][1];
+ $('#attachment_point option[value=' + attach_point + ']').attr('selected','selected');
fillInverterData(data, '#inverter');
diff --git a/helix/templates/array_summary.html.jinja b/helix/templates/array_summary.html.jinja
index bb1f041..201093a 100644
--- a/helix/templates/array_summary.html.jinja
+++ b/helix/templates/array_summary.html.jinja
@@ -2,7 +2,7 @@
{% set title = "Helix Calculator" %}
{% block contents %}
diff --git a/helix/templates/site_summary.html.jinja b/helix/templates/site_summary.html.jinja
index 2547868..90b06a9 100644
--- a/helix/templates/site_summary.html.jinja
+++ b/helix/templates/site_summary.html.jinja
@@ -87,7 +87,4 @@
{% endif %}
{% include "navigation_buttons.html.jinja" %}
- {% if 'SFID' in session %}
- Sales Force project (Logout)
- {% endif %}
{% endblock %}
diff --git a/requirements.test.txt b/requirements.test.txt
index 924173b..0cd71e8 100644
--- a/requirements.test.txt
+++ b/requirements.test.txt
@@ -2,7 +2,7 @@ nose==1.3.7
selenium==2.53.1
splinter==0.7.3
cssselect==0.9.1
-lxml==3.6.0
+lxml==4.1.1
mockredispy==2.9.0.11
Flask-Testing==0.4.2
splinter[flask]
diff --git a/requirements.txt b/requirements.txt
index ae2e6f4..9d91a58 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -23,3 +23,5 @@ blinker==1.4
Flask-OAuthlib==0.9.4
boto3==1.4.8
ujson==1.35
+Flask-FeatureFlags==0.6
+mock==2.0.0
diff --git a/runtime.txt b/runtime.txt
index 78082e3..3d2fe0c 100644
--- a/runtime.txt
+++ b/runtime.txt
@@ -1 +1,2 @@
python-3.5.1
+
diff --git a/test/helpers/panel_presenter_test.py b/test/helpers/panel_presenter_test.py
index 10e281d..c9c47d1 100644
--- a/test/helpers/panel_presenter_test.py
+++ b/test/helpers/panel_presenter_test.py
@@ -1,4 +1,5 @@
import unittest
+import mock
from numpy.testing import assert_array_equal, assert_equal
from helix.constants.module_type import ModuleType
@@ -9,11 +10,15 @@ from helix.models.subarray import Subarray
from helix.presenters.panel_presenter import ProjectPresenter
from helix.constants.system_type import SystemType
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):
-
def test_get_table_data_single_tilt_96cell(self):
self.subject = ProjectPresenter(SystemType.singleTilt, ModuleType.Cell96)
panels = [
@@ -76,6 +81,7 @@ class PanelPresenterTest(unittest.TestCase):
actual_buildings = self.subject.get_buildings(buildings,60)
assert_array_equal(actual_buildings,expected_buildings)
+ #@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
@@ -88,6 +94,7 @@ class PanelPresenterTest(unittest.TestCase):
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
@@ -100,6 +107,7 @@ class PanelPresenterTest(unittest.TestCase):
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]] ]
@@ -112,6 +120,7 @@ class PanelPresenterTest(unittest.TestCase):
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] ] ]
diff --git a/test/test_helpers.py b/test/test_helpers.py
index 1edec10..b71ca3f 100644
--- a/test/test_helpers.py
+++ b/test/test_helpers.py
@@ -105,3 +105,7 @@ def assert_image_equal(image_1, image_2, error=5e-2):
average_error = total_error / pixels
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