import os import requests from urllib.parse import urlparse import rollbar import rollbar.contrib.flask from flask import Flask, request, make_response, session, render_template, \ redirect, url_for, jsonify from flask import got_request_exception from flask.ext import assets from flask_oauthlib.client import OAuth from webassets.filter import get_filter from helix.sales_force import tasks as sf_tasks from helix.Services.doc_gen_service import DocGenService from helix.Services.dxf_helper import DXFHelper from helix.Services.dxf_service import DXFService from helix.api.api import api from helix.calculators.calculator import Calculator from helix.constants import redis_constant, sql_constant from helix.constants.file_validation_error import FileValidationError, FileValidationException from helix.constants.inverter_type import InverterType from helix.constants.system_type import SystemType from helix.csv_builder import CsvBuilder from helix.doc_gen_params_builder import DocGenParamsBuilder from helix.forms.ebom_form import EbomForm, InverterBrandForm, \ SupervisorForm, SupervisorFormSMA, StandAloneInverterFormSMA, \ StandAloneInverterFormDelta 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.session_manager import SessionManager from helix.validators.file_validator import FileValidator, FileType from helix.validators.subarray_validator import SubarrayValidator app = Flask(__name__) app.register_blueprint(api, url_prefix='/api') app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey') app.config['PROFILE'] = True # Sales Force 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('Sales Force integration disabled') sales_force = None assets_env = assets.Environment(app) assets_env.init_app(app) assets_env.load_path = [ os.path.join(os.path.dirname(__file__), 'scss') ] sass = get_filter('scss') sass.load_paths = [os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scss')] assets_env.register( 'main_css', assets.Bundle( '*.scss', filters=(sass,), output='css/main.css' ) ) @app.before_first_request def init_rollbar(): # Do nothing unless Rollbar is configured if not os.getenv("ROLLBAR_ACCESS_TOKEN"): return rollbar.init(os.getenv("ROLLBAR_ACCESS_TOKEN"), # Setup this var in heroku to distinguish errors from different envs os.getenv("ROLLBAR_ENV", "development"), root=os.path.dirname(os.path.realpath(__file__)), allow_logging_basic_config=False) got_request_exception.connect(rollbar.contrib.flask.report_exception, app) def is_sfdc_session(): return 'SFID' in session @app.route("/") def index(): return redirect(url_for('site_characterization')) @app.route("/test_dxf/", methods=['GET', 'POST']) def test_dxf(): form = TestDXFForm() if form.validate_on_submit(): file = request.files['dxf_upload'] file_contents = file.read().decode('utf-8') 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, calculate_panel_data=False) l_b = calculator.L_B() * 12 # convert from feet to inches try: dxf_data = DXFService().parse(file_contents, user_values.module_system_constants(), user_values.system_type(), l_b, DXFHelper(), SubarrayValidator()) dxf_data['panels'].sort(key=lambda p: p.id) dxf_data['l_b'] = l_b if not form.show_wind_zones.data: dxf_data['lb_polygons'] = [] except DXFError as error: form.dxf_upload.errors.append(error.message) dxf_data = {} else: dxf_data = {} dxf_data['colors'] = [ 'red', 'orange', 'yellow', 'green', 'blue', 'purple', ] return render_template('test_dxf.html.jinja', context=dxf_data, form=form) # wizard steps @app.route("/site_characterization/", methods=['GET', 'POST']) def site_characterization(): if is_sfdc_session(): return redirect('/summary/') db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) site_info_form = InputForm() context = session_manager.context() context['current_step'] = 1 context['javascripts'] = ['site_characterization'] if site_info_form.validate_on_submit(): session_manager.save_form_submission(request.form) return redirect(url_for('summary')) if request.method != 'POST': session_manager.fill_saved_values_in_form(site_info_form) db_session.close() return render_template('site_characterization.html.jinja', context=context, form=site_info_form) @app.route("/summary/") def summary(): db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) context = session_manager.context() context['current_step'] = 2 if context['site_data_available']: context['project_name'] = session_manager.site.project_name user_values = session_manager.user_values() calculator = Calculator(user_values, calculate_panel_data=False) context['wind_zones'] = user_values.system_type().system_constants().wind_zones context['summary_table'] = calculator.summary_table() context['minimum_array_sizes'] = calculator.minimum_array_sizes() context['l_b'] = round(calculator.L_B(), 2) context['k_z'] = round(calculator.k_z(), 2) context['q_z'] = round(calculator.q_z(), 2) context['warning_messages'] = set() context['javascripts'] = ['https://cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.js'] for panel_type, values in context['summary_table'].items(): for panel_type_warnings in values['warnings']: for warning in panel_type_warnings: context['warning_messages'].add(warning) else: context['no_proceed'] = True if is_sfdc_session(): context['hide_back'] = True db_session.close() return render_template('site_summary.html.jinja', context=context) def handle_dxf_file(session_manager, file_contents, filename=None, save_file=True): 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']) if save_file: session_manager.save_uploaded_file(csv, dxf_file_name=filename) buildings = dxf_data['buildings'] session_manager.save_buildings_polygons(buildings) session_manager.save_is_drawing_inaccurate(dxf_data['is_panel_drawing_inaccurate']) return True, errors except DXFError as error: errors.append(error.message) except OldDxfFormatException as error: errors.append(error.message) return False, errors @app.route("/array_summary/", methods=['GET', 'POST']) def array_summary(): """This endpoint allows you to upload a file. The content of the file is parsed, and then several objects are created that aid in the validation of the uploaded file. """ db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) context = session_manager.context() context['current_step'] = 3 array_form = ArrayForm() if array_form.validate_on_submit(): if 'file_upload' in request.files and request.files['file_upload'].filename: validator = FileValidator(session_manager.user_values()) file = request.files['file_upload'] file_contents = validator.obtain_stream(file) validation_error = validator.validate(file_contents, FileType.Csv, file) if not validation_error: session_manager.save_uploaded_file(file_contents, cad_file_name=file.filename) session_manager.save_buildings_polygons([]) # no buildings in the csv file session_manager.save_is_drawing_inaccurate(False) return redirect(url_for('array_summary')) else: array_form.file_upload.errors.append(validation_error.format_error_message()) elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename: file = request.files['dxf_upload'] user_values = session_manager.user_values() validator = FileValidator(user_values) file_contents = validator.obtain_stream(file) success, errors = handle_dxf_file(session_manager, file_contents, filename=file.filename) if success: return redirect(url_for('array_summary')) else: array_form.dxf_upload.errors.extend(errors) elif context['csv_available']: return redirect(url_for('power_station_configuration')) else: array_form.file_upload.errors.append('Please provide a .txt file!') context['javascripts'] = ['array_summary_bundle'] context['hide_submit'] = True context['cad_file_name'] = '' context['dxf_file_name'] = '' if context['site_data_available'] and context['csv_available']: user_values = session_manager.user_values() try: calculator = Calculator(user_values) system_type = user_values.system_type() module_type = user_values.module_type() project_presenter = ProjectPresenter(system_type, module_type) context['wind_zones'] = system_type.system_constants().wind_zones context['summary_table'] = calculator.summary_table() context['minimum_array_sizes'] = calculator.minimum_array_sizes() context['seismic_anchors'] = calculator.subarray_summary() context['summary_values'] = calculator.summary_values() 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, calculator.subarrays, max_y) context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing, max_y) context['override_form'] = True context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data' context['dxf_file_name'] = session_manager.site.dxf_file_name or 'Upload System DXF' context['is_drawing_inaccurate'] = session_manager.user_values().is_panel_drawing_inaccurate() context['inaccurate_drawing_warning'] = 'The subarrays in this design are not parallel to each other, \ and the graphical representation on this page may not be accurate.' except FileValidationException as error: # when calculator is about to enter infinte loop # it throws an exception - it is supplied wrong data context['site_data_available'] = False context['csv_available'] = False context['no_proceed'] = True context['cad_file_name'] = '' context['dxf_file_name'] = '' context['infinite_loop_detection_message'] = error.message elif not context['site_data_available']: context['no_proceed'] = True if is_sfdc_session() and 'dxf_link_loaded' not in session: context['javascripts'].append('auto_dxf_load') db_session.close() return render_template('array_summary.html.jinja', context=context, form=array_form) @app.route("/load_dxf/", methods=['GET', 'POST']) def load_dxf_file(): if 'dxf_link' not in session: errors = ['DXF link not found'] response = jsonify({'errors': errors}) response.status_code = 404 return response dxf_link = session['dxf_link'] response = requests.get(dxf_link) if response.status_code == 200: file_contents = response.content.decode('utf-8') filename = urlparse(dxf_link).path.strip('/') db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) success, errors = handle_dxf_file(session_manager, file_contents, filename=filename, save_file=False) session['dxf_link_loaded'] = True if success: return jsonify({'status': 'success'}) else: errors = ['Unable to download DXF file from Sales Force ({})'.format(response.status_code)] response = jsonify({'errors': errors}) response.status_code = 400 return response @app.route("/power_station_configuration/", methods=['GET', 'POST']) def power_station_configuration(): db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) context = session_manager.context() if session_manager.site: system_type = session_manager.user_values().system_type() else: system_type = None inverter_brand_form = InverterBrandForm() if request.method != 'POST' or request.form['form_id'] != 'inverter_brand_form': inverter_brand_form.populate_choices(context['inverter_brands']) is_delta = inverter_brand_form.is_delta() if is_delta: ebom_form = EbomForm() standalone_inverter_form = StandAloneInverterFormDelta() standalone_inverter_form.populate_choices() supervisor_form = SupervisorForm() else: ebom_form = EbomForm() ebom_form.update_inverter_strings_choices(system_type) standalone_inverter_form = StandAloneInverterFormSMA() standalone_inverter_form.update_inverter_strings_choices(system_type) standalone_inverter_form.populate_choices(context['power_stations'], context['standalone_inverters']) supervisor_form = SupervisorFormSMA() supervisor_form.populate_choices(context['power_stations'], context['power_monitors']) if request.method == 'POST': if request.form['form_id'] == 'inverter_brand_form' and inverter_brand_form.validate_on_submit(): session_manager.delete_power_station_config_data() session_manager.save_or_update_inverter_brands(request.form) return redirect("/power_station_configuration/") elif request.form['form_id'] == 'power_station_form' and ebom_form.validate_on_submit(): session_manager.save_or_update_power_station(request.form) return redirect("/power_station_configuration/") elif request.form['form_id'] == 'standalone_inverter_form' and standalone_inverter_form.validate_on_submit(): session_manager.save_or_update_standalone_inverter(request.form) return redirect("/power_station_configuration/") elif request.form['form_id'] == 'supervisor_form' and supervisor_form.validate_on_submit(): session_manager.save_or_update_supervisor_monitor(request.form) return redirect("/power_station_configuration") ebom_form.power_station_description.data = "Power Station " + str(len(context['power_stations']) + 1) inverter_enum = InverterType.DELTA if is_delta else InverterType.SMA string_limits = {} string_defaults = {} for i_e in inverter_enum.all(): string_limits[i_e.value] = list(i_e.valid_string_ranges) if i_e.valid_string_ranges is not None else [] string_defaults[i_e.value] = i_e.default_string context['standalone_inverter_string_limits'] = string_limits context['standalone_inverter_string_defaults'] = string_defaults context['current_step'] = 4 context['javascripts'] = ['power_station_configuration'] db_session.close() return render_template('power_station_configuration.html.jinja', context=context, ebom_form=ebom_form, is_delta=is_delta, inverter_brand_form=inverter_brand_form, standalone_inverter_form=standalone_inverter_form, supervisor_monitor_form=supervisor_form) @app.route("/download/") def download(): db_session = sql_constant.sql_session_maker() 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 db_session.close() return render_template('download.html.jinja', context=context) @app.route("/delete_power_station/") def delete_power_station(uuid): db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) session_manager.delete_power_station(uuid) db_session.close() return redirect(url_for('power_station_configuration')) @app.route('/delete_standalone_inverter/') def delete_standalone_inverter(uuid): db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) session_manager.delete_standalone_inverter(uuid) db_session.close() return redirect(url_for('power_station_configuration')) @app.route('/delete_supervisor_monitor/') def delete_supervisor_monitor(uuid): db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) session_manager.delete_supervisor_monitor(uuid) db_session.close() return redirect(url_for('power_station_configuration')) @app.route("/result") def result(): 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) csv = CsvBuilder().build_cad_output(calculator.get_computed_csv_columns()) response = make_response(csv) response.headers["Content-Disposition"] = "attachment; filename=%s_result.txt" % user_values.project_name_no_spaces() db_session.close() return response @app.route("/documentation") def documentation(): 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) 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() response = make_response(document) response.headers["Content-Disposition"] = "attachment; filename=%s_documentation.pdf" % user_values.project_name_no_spaces() db_session.close() return response @app.route("/bom") def bom(): 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) csv = CsvBuilder().build_bom_output(calculator.compute_bom()) response = make_response(csv) response.headers["Content-Disposition"] = "attachment; filename=%s_bom.txt" % user_values.project_name_no_spaces() db_session.close() return response @app.route("/exposure_categories") def exposure_categories(): db_session = sql_constant.sql_session_maker() context = SessionManager(session, redis_constant.redis_store, db_session).context() db_session.close() return render_template('exposure_categories.html.jinja', context=context) @app.route("/helix_documentation") def helix_documentation(): db_session = sql_constant.sql_session_maker() context = SessionManager(session, redis_constant.redis_store, db_session).context() db_session.close() return render_template('helix_documentation.jinja', context=context) # Sales Force Integration @app.route('/sales_force_login') def sales_force_login(): # To test it locally: https://localhost:8443/sales_force_login?SFID=a3cL00000004QsQIAU sfid = request.args.get('SFID') if sfid: session.clear() session['SFID'] = sfid return sales_force.authorize(callback=url_for('sales_force_authorized', _external=True, SFID=sfid), SFID=sfid) else: return redirect('/') @app.route('/sales_force_authorized') def sales_force_authorized(): next_url = url_for('summary') resp = sales_force.authorized_response() if resp is None: print('Unable to authenticate to SFDC.') return redirect(next_url) print('New Sales Force - OAuth2 login') db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) session['sales_force_token'] = resp['access_token'] data = sf_tasks.get_site_characterization_from_sales_force(session) if data: session['dxf_link'] = data['dxf_link'] session_manager.save_form_submission(data) return redirect(next_url) else: return sales_force_logout() @app.route("/export-sfdc") def export_sfdc(): if not is_sfdc_session(): return redirect('/') db_session = sql_constant.sql_session_maker() session_manager = SessionManager(session, redis_constant.redis_store, db_session) 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) data.pop('bom', None) data['dxfUrlFromSF'] = session['dxf_link'] session['sfdc_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('sales_force_token') @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() return redirect('/') # End of Sales Force Integration @app.template_filter('format_number') def format_number(number): return "{:,g}".format(number) @app.template_filter('is_dual_tilt') def is_dual_tilt(system_type): return system_type == SystemType.dualTilt @app.context_processor def power_station_has_monitor(): def _power_station_has_monitor(power_station, monitors): for monitor in monitors: if monitor['power_source'][1] == power_station['power_station_id']: return True return False return dict(power_station_has_monitor=_power_station_has_monitor) @app.context_processor def enum(): def _enum(item): return enumerate(item) return dict(enum=_enum) def main(): host = '0.0.0.0' debug = bool(os.getenv("FLASK_DEBUG", False)) if os.getenv('FLASK_DEBUG_SSL', None): port = int(os.getenv('PORT', 8443)) app.run(host=host, port=port, debug=debug, ssl_context='adhoc') else: port = int(os.getenv('PORT', 5000)) app.run(host=host, port=port, debug=debug) @app.route("/fail-test") def fail_test(): raise RuntimeError("This is a test failure, ignore it") if __name__ == "__main__": main()