Files
old-krovovi-kalkulator/helix/main.py
2017-12-27 16:24:50 +01:00

642 lines
25 KiB
Python

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/<uuid>")
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/<uuid>')
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/<uuid>')
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()