From fe35b0aa910c8fd7c497d9912c9fbedf972be684 Mon Sep 17 00:00:00 2001 From: Senad Uka Date: Mon, 15 Jan 2018 13:31:00 +0100 Subject: [PATCH] syncing with upstream --- Dockerfile | 2 +- helix/constants/anchor_type.py | 1 + helix/constants/global_constants.py | 2 +- helix/constants/module_type.py | 1 + helix/main.py | 193 +++++++++++++----- helix/models/corner.py | 2 +- helix/presenters/panel_presenter.py | 58 +++--- helix/qa_helper.py | 20 ++ helix/sales_force/tasks.py | 57 +++--- helix/scss/forms.scss | 6 + helix/session_manager.py | 10 +- helix/static/css/main.css | 33 +-- helix/static/javascripts/auto_dxf_load.js | 30 ++- .../power_station_configuration.js | 5 +- helix/templates/array_summary.html.jinja | 6 +- helix/templates/download.html.jinja | 6 +- helix/templates/ebom_form.html.jinja | 3 +- helix/templates/layout.html.jinja | 16 ++ helix/templates/site_summary.html.jinja | 3 - requirements.test.txt | 2 +- requirements.txt | 2 + runtime.txt | 1 + test/helpers/panel_presenter_test.py | 11 +- test/test_helpers.py | 4 + 24 files changed, 325 insertions(+), 149 deletions(-) create mode 100644 helix/qa_helper.py 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