import os import requests import rollbar import rollbar.contrib.flask from flask import Flask, request, make_response, session, render_template, \ redirect, url_for from flask import got_request_exception from flask.ext import assets from webassets.filter import get_filter 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.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 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) @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(): 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']: 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 db_session.close() return render_template('site_summary.html.jinja', context=context) @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, file, FileType.Csv) 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() calculator = Calculator(user_values, calculate_panel_data=False) validator = FileValidator(user_values) file_contents = validator.obtain_stream(file) validation_error = validator.validate(file_contents, file, FileType.AuroraDxf) if validation_error: error_msg = validation_error.format_error_message() array_form.dxf_upload.errors.append(error_msg) else: 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']: 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() 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() context['panel_array'] = project_presenter.get_panel_data(panels, calculator.subarrays, project_presenter.get_max_y( calculator.buildings_for_drawing, panels)) context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing) 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.' elif not context['site_data_available']: context['no_proceed'] = True db_session.close() return render_template('array_summary.html.jinja', context=context, form=array_form) @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 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) @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' port = int(os.getenv('PORT', 5000)) app.run(host=host, port=port, debug=bool(os.getenv("FLASK_DEBUG", False))) @app.route("/fail-test") def fail_test(): raise RuntimeError("This is a test failure, ignore it") if __name__ == "__main__": main()