diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..72e294c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +__pycache__ +*.pyc +*.pyo +*.pyd \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..20299ee --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log + +## CodeBase version: 2stable.0.1 / 2022-01-15 +### Improvements + +- Dependencies update (all packages) + - Flask==2.0.2 (latest stable version) + - flask_wtf==1.0.0 + - jinja2==3.0.3 + - flask-restx==0.5.1 + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..33221e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9 + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +COPY . . + +# gunicorn +CMD [".", "venv/bin/activate", ";" , "gunicorn", "--config", "gunicorn-cfg.py", "run:app"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5012dd9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,32 @@ +# MIT License + +Copyright (c) 2019 - present [AppSeed](http://appseed.us/) + +
+ +## Licensing Information + +
+ +| Item | - | +| ---------------------------------- | --- | +| License Type | MIT | +| Use for print | **YES** | +| Create single personal website/app | **YES** | +| Create single website/app for client | **YES** | +| Create multiple website/apps for clients | **YES** | +| Create multiple SaaS applications | **YES** | +| End-product paying users | **YES** | +| Product sale | **YES** | +| Remove footer credits | **YES** | +| --- | --- | +| Remove copyright mentions from source code | NO | +| Production deployment assistance | NO | +| Create HTML/CSS template for sale | NO | +| Create Theme/Template for CMS for sale | NO | +| Separate sale of our UI Elements | NO | + +
+ +--- +For more information regarding licensing, please contact the AppSeed Service < *support@appseed.us* > diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f00e35 --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +# [Material Kit](https://appseed.us/generator/material-kit/) Flask + +`Open-Source` seed project generated by AppSeed in **Flask** Framework on top of **[Material Kit](https://appseed.us/generator/material-kit/)** design. Designed for those who like bold elements and beautiful websites, **Material Kit 2** is ready to help you create stunning websites and web apps. `Material Kit 2` is built with over 60 frontend individual elements, like buttons, inputs, navbars, nav tabs, cards, or alerts, giving you the freedom of choosing and combining. + +- 👉 [Flask Material Kit](https://appseed.us/product/material-kit/flask/) - product page +- 👉 [Complete documentation](https://docs.appseed.us/products/flask-apps/material-kit) - `Learn how to use and update the product` +- ✅ [PRO Version Available](#pro-version) - `enhanced UI` and more `features` + +
+ +> Built with [Material Kit Generator](https://appseed.us/generator/material-kit/) + +- Timestamp: `2022-06-23 20:44` +- Build ID: `90d2d084-8a7e-457c-b9ea-045f590accfd` +- **Free [Support](https://appseed.us/support/)** (registered users) via `Email` and `Discord` + +
+ +> Features + +- `Up-to-date dependencies` +- Database: `sqlite` +- `DB Tools`: SQLAlchemy ORM, Flask-Migrate (schema migrations) +- Session-Based authentication (via **flask_login**), Forms validation + +
+ +![Material Kit - Starter generated by AppSeed.](https://user-images.githubusercontent.com/51070104/167396765-c88b7a95-155f-4236-8691-7b80fa2d9cd9.png) + +
+ + +## ✨ Start the app in Docker + +> **Step 1** - Download the code from the GH repository (using `GIT`) + +```bash +$ # Get the code +$ git clone https://github.com/appseed-projects/.git +$ cd +``` + +
+ +> **Step 2** - Edit `.env` and set `DEBUG=True`. This will activate the `SQLite` persistance. + +```txt +DEBUG=True +``` + +
+ +> **Step 3** - Start the APP in `Docker` + +```bash +$ docker-compose up --build +``` + +Visit `http://localhost:5085` in your browser. The app should be up & running. + +
+ + + + +## ✨ How to use it + +> Download the code + +```bash +$ # Get the code +$ git clone https://github.com/appseed-projects/90d2d084-8a7e-457c-b9ea-045f590accfd.git +$ cd 90d2d084-8a7e-457c-b9ea-045f590accfd +``` + +
+ +### 👉 Set Up for `Unix`, `MacOS` + +> Install modules via `VENV` + +```bash +$ virtualenv env +$ source env/bin/activate +$ pip3 install -r requirements.txt +``` + +
+ +> Set Up Flask Environment + +```bash +$ export FLASK_APP=run.py +$ export FLASK_ENV=development +``` + +
+ +> Start the app + +```bash +$ flask run +``` + +At this point, the app runs at `http://127.0.0.1:5000/`. + +
+ +### 👉 Set Up for `Windows` + +> Install modules via `VENV` (windows) + +``` +$ virtualenv env +$ .\env\Scripts\activate +$ pip3 install -r requirements.txt +``` + +
+ +> Set Up Flask Environment + +```bash +$ # CMD +$ set FLASK_APP=run.py +$ set FLASK_ENV=development +$ +$ # Powershell +$ $env:FLASK_APP = ".\run.py" +$ $env:FLASK_ENV = "development" +``` + +
+ +> Start the app + +```bash +$ flask run +``` + +At this point, the app runs at `http://127.0.0.1:5000/`. + +
+ +### 👉 Create Users + +By default, the app redirects guest users to authenticate. In order to access the private pages, follow this set up: + +- Start the app via `flask run` +- Access the `registration` page and create a new user: + - `http://127.0.0.1:5000/register` +- Access the `sign in` page and authenticate + - `http://127.0.0.1:5000/login` + +
+ +## ✨ Code-base structure + +The project is coded using blueprints, app factory pattern, dual configuration profile (development and production) and an intuitive structure presented bellow: + +```bash +< PROJECT ROOT > + | + |-- apps/ + | | + | |-- home/ # A simple app that serve HTML files + | | |-- routes.py # Define app routes + | | + | |-- authentication/ # Handles auth routes (login and register) + | | |-- routes.py # Define authentication routes + | | |-- models.py # Defines models + | | |-- forms.py # Define auth forms (login and register) + | | + | |-- static/ + | | |-- # CSS files, Javascripts files + | | + | |-- templates/ # Templates used to render pages + | | |-- includes/ # HTML chunks and components + | | | |-- navigation.html # Top menu component + | | | |-- sidebar.html # Sidebar component + | | | |-- footer.html # App Footer + | | | |-- scripts.html # Scripts common to all pages + | | | + | | |-- layouts/ # Master pages + | | | |-- base-fullscreen.html # Used by Authentication pages + | | | |-- base.html # Used by common pages + | | | + | | |-- accounts/ # Authentication pages + | | | |-- login.html # Login page + | | | |-- register.html # Register page + | | | + | | |-- home/ # UI Kit Pages + | | |-- index.html # Index page + | | |-- 404-page.html # 404 page + | | |-- *.html # All other pages + | | + | config.py # Set up the app + | __init__.py # Initialize the app + | + |-- requirements.txt # App Dependencies + | + |-- .env # Inject Configuration via Environment + |-- run.py # Start the app - WSGI gateway + | + |-- ************************************************************************ +``` + +
+ + + +## PRO Version + +> For more components, pages and priority on support, feel free to take a look at this amazing starter: + +**Material Kit 2** is a premium design crafted by the `Creative-Tim` agency on top of Bootstrap 5 Framework. Designed for those who like bold elements and beautiful websites, Material Kit 2 is made of hundreds of elements, designed blocks, and fully coded pages built with an impressive level of quality. + +- 👉 [Flask Material Kit2 PRO](https://appseed.us/product/material-kit2-pro/flask/) - product page + - ✅ `Enhanced UI` - more pages and components + - ✅ `Priority` on support + +
+ +![Mk2 PRO - Premium Seed project by AppSeed.](https://user-images.githubusercontent.com/51070104/168224733-b054bb46-d454-4aea-bb94-2d01bf4760d2.png) + +
+ +--- +[Material Kit](https://appseed.us/generator/material-kit/) Flask - Open-source starter generated by **[AppSeed Generator](https://appseed.us/generator/)**. diff --git a/apps/MRE/__init__.py b/apps/MRE/__init__.py new file mode 100644 index 0000000..fa28339 --- /dev/null +++ b/apps/MRE/__init__.py @@ -0,0 +1,117 @@ +import numpy as np +import matplotlib.pyplot as plt +from multipolyfit import multipolyfit as mpf + + +class ConsumerNotEligibleError(Exception): + def __init__(self, reason, *args): + super().__init__(args) + self.reason = reason + + def __str__(self): + return f'Consumer is ineligible due to {self.reason}' + + +def fico_default_risk_modifier_by_lend_percent(credit_score, loan_percent): + # The initial data set is organized as [{credit_score}, {loan_percent}] + initial_data = [[620, 1], + [620, 70], + [620, 85], + [620, 90], + [620, 95], + [660, 1], + [660, 70], + [660, 85], + [660, 90], + [660, 95], + [700, 1], + [700, 70], + [700, 85], + [700, 90], + [700, 95], + [740, 1], + [740, 70], + [740, 85], + [740, 90], + [740, 95]] + # the default rates to use + def_rate_620 = 3.03 + def_rate_660 = 2.01 + def_rate_700 = 1.50 + def_rate_740 = 0.94 + # the risk factor multiples to use + risk_factor_620 = [0.20, 0.61, 1.22, 1.48, 1.80] + risk_factor_660 = [0.20, 0.62, 1.22, 1.48, 1.82] + risk_factor_700 = [0.20, 0.62, 1.22, 1.49, 1.83] + risk_factor_740 = [0.20, 0.63, 1.21, 1.47, 1.81] + + net_def_risks_620 = np.multiply(def_rate_620, risk_factor_620) + net_def_risks_660 = np.multiply(def_rate_660, risk_factor_660) + net_def_risks_700 = np.multiply(def_rate_700, risk_factor_700) + net_def_risks_740 = np.multiply(def_rate_740, risk_factor_740) + + initial_risk_mults = [r for r in net_def_risks_620] + [r for r in net_def_risks_660] + [r for r in net_def_risks_700] + [r for r in net_def_risks_740] + + x, y = zip(*initial_data) + ax = plt.axes(projection='3d') + ax.scatter(x, y, initial_risk_mults) + + coefficients = mpf(initial_data, initial_risk_mults, 1) + result = np.multiply(coefficients[1], x) + np.multiply(coefficients[2], y) + coefficients[0] + ax.plot3D(x, y, result) + ax.set_title('Credit Score/Loan % Line of Best Fit') + plt.savefig('graph.png') + + return coefficients[0] + coefficients[1] * credit_score + coefficients[2] * loan_percent + + +def get_loss_severity(): + return 0.19 + + +def get_recovery_rate(): + return 0.9 + + +def get_risk_pool_allocation_percent(): + return 0.01 + + +def get_gross_rent_yield(): + return 3.686 + + +def compute_mre(home_value, down_payment_percent, consumer_fico): + if not 0 <= down_payment_percent < 100: + raise ValueError('Down payment percent must be contained in [0, 100)') + + if consumer_fico < 620: + raise ConsumerNotEligibleError('Credit score too low (must be 620 or Greater)') + + monthly_payment = home_value * get_gross_rent_yield()/100/12 + + income_interruption_home_percentage = 100 * (monthly_payment * 4 / home_value) + risk_pool_percent = get_risk_pool_allocation_percent() + + loss_severity = get_loss_severity() + recovery_rate = get_recovery_rate() + default_rate = fico_default_risk_modifier_by_lend_percent(consumer_fico, 100 - down_payment_percent) + + at_risk_value = home_value * loss_severity + + def_rate_mult = 100/default_rate + risk_pool_allocation = risk_pool_percent * home_value + denom = recovery_rate + num = at_risk_value - (def_rate_mult * risk_pool_allocation) + result = num/denom + mre = result/home_value + income_interruption_home_percentage + mre += income_interruption_home_percentage + + return mre + + +if __name__ == '__main__': + print(compute_mre(742500, 2.5, 680)) + # print(fico_default_risk_modifier_by_lend_percent(620, 85)) + + diff --git a/apps/MRE/graph.png b/apps/MRE/graph.png new file mode 100644 index 0000000..4136888 Binary files /dev/null and b/apps/MRE/graph.png differ diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..f23857d --- /dev/null +++ b/apps/__init__.py @@ -0,0 +1,44 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import Flask +from flask_login import LoginManager +from flask_sqlalchemy import SQLAlchemy +from importlib import import_module + + +db = SQLAlchemy() +login_manager = LoginManager() + + +def register_extensions(app): + db.init_app(app) + login_manager.init_app(app) + + +def register_blueprints(app): + for module_name in ('authentication', 'home'): + module = import_module('apps.{}.routes'.format(module_name)) + app.register_blueprint(module.blueprint) + + +def configure_database(app): + + @app.before_first_request + def initialize_database(): + db.create_all() + + @app.teardown_request + def shutdown_session(exception=None): + db.session.remove() + + +def create_app(config): + app = Flask(__name__) + app.config.from_object(config) + register_extensions(app) + register_blueprints(app) + configure_database(app) + return app diff --git a/apps/authentication/__init__.py b/apps/authentication/__init__.py new file mode 100644 index 0000000..25243ec --- /dev/null +++ b/apps/authentication/__init__.py @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import Blueprint + +blueprint = Blueprint( + 'authentication_blueprint', + __name__, + url_prefix='' +) diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py new file mode 100644 index 0000000..89f43da --- /dev/null +++ b/apps/authentication/forms.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField +from wtforms.validators import Email, DataRequired + +# login and registration + + +class LoginForm(FlaskForm): + username = StringField('Username', + id='username_login', + validators=[DataRequired()]) + password = PasswordField('Password', + id='pwd_login', + validators=[DataRequired()]) + + +class CreateAccountForm(FlaskForm): + username = StringField('Username', + id='username_create', + validators=[DataRequired()]) + email = StringField('Email', + id='email_create', + validators=[DataRequired(), Email()]) + password = PasswordField('Password', + id='pwd_create', + validators=[DataRequired()]) diff --git a/apps/authentication/models.py b/apps/authentication/models.py new file mode 100644 index 0000000..265faf6 --- /dev/null +++ b/apps/authentication/models.py @@ -0,0 +1,49 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask_login import UserMixin + +from apps import db, login_manager + +from apps.authentication.util import hash_pass + + +class Users(db.Model, UserMixin): + + __tablename__ = 'Users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True) + email = db.Column(db.String(64), unique=True) + password = db.Column(db.LargeBinary) + + def __init__(self, **kwargs): + for property, value in kwargs.items(): + # depending on whether value is an iterable or not, we must + # unpack it's value (when **kwargs is request.form, some values + # will be a 1-element list) + if hasattr(value, '__iter__') and not isinstance(value, str): + # the ,= unpack of a singleton fails PEP8 (travis flake8 test) + value = value[0] + + if property == 'password': + value = hash_pass(value) # we need bytes here (not plain str) + + setattr(self, property, value) + + def __repr__(self): + return str(self.username) + + +@login_manager.user_loader +def user_loader(id): + return Users.query.filter_by(id=id).first() + + +@login_manager.request_loader +def request_loader(request): + username = request.form.get('username') + user = Users.query.filter_by(username=username).first() + return user if user else None diff --git a/apps/authentication/routes.py b/apps/authentication/routes.py new file mode 100644 index 0000000..490b2d2 --- /dev/null +++ b/apps/authentication/routes.py @@ -0,0 +1,122 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import render_template, redirect, request, url_for +from flask_login import ( + current_user, + login_user, + logout_user +) + +from apps import db, login_manager +from apps.authentication import blueprint +from apps.authentication.forms import LoginForm, CreateAccountForm +from apps.authentication.models import Users + +from apps.authentication.util import verify_pass + + +@blueprint.route('/') +def route_default(): + return redirect(url_for('authentication_blueprint.login')) + +# Login & Registration + + +@blueprint.route('/login', methods=['GET', 'POST']) +def login(): + login_form = LoginForm(request.form) + if 'login' in request.form: + + # read form data + username = request.form['username'] + password = request.form['password'] + + # Locate user + user = Users.query.filter_by(username=username).first() + + # Check the password + if user and verify_pass(password, user.password): + + login_user(user) + return redirect(url_for('authentication_blueprint.route_default')) + + # Something (user or pass) is not ok + return render_template('accounts/login.html', + msg='Wrong user or password', + form=login_form) + + if not current_user.is_authenticated: + return render_template('accounts/login.html', + form=login_form) + return redirect(url_for('home_blueprint.index')) + + +@blueprint.route('/register', methods=['GET', 'POST']) +def register(): + create_account_form = CreateAccountForm(request.form) + if 'register' in request.form: + + username = request.form['username'] + email = request.form['email'] + + # Check usename exists + user = Users.query.filter_by(username=username).first() + if user: + return render_template('accounts/register.html', + msg='Username already registered', + success=False, + form=create_account_form) + + # Check email exists + user = Users.query.filter_by(email=email).first() + if user: + return render_template('accounts/register.html', + msg='Email already registered', + success=False, + form=create_account_form) + + # else we can create the user + user = Users(**request.form) + db.session.add(user) + db.session.commit() + + # Delete user from session + logout_user() + + return render_template('accounts/register.html', + msg='User created successfully.', + success=True, + form=create_account_form) + + else: + return render_template('accounts/register.html', form=create_account_form) + + +@blueprint.route('/logout') +def logout(): + logout_user() + return redirect(url_for('authentication_blueprint.login')) + +# Errors + +@login_manager.unauthorized_handler +def unauthorized_handler(): + return render_template('home/page-403.html'), 403 + + +@blueprint.errorhandler(403) +def access_forbidden(error): + return render_template('home/page-403.html'), 403 + + +@blueprint.errorhandler(404) +def not_found_error(error): + return render_template('home/page-404.html'), 404 + + +@blueprint.errorhandler(500) +def internal_error(error): + return render_template('home/page-500.html'), 500 diff --git a/apps/authentication/util.py b/apps/authentication/util.py new file mode 100644 index 0000000..9130da8 --- /dev/null +++ b/apps/authentication/util.py @@ -0,0 +1,34 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +import os +import hashlib +import binascii + +# Inspiration -> https://www.vitoshacademy.com/hashing-passwords-in-python/ + + +def hash_pass(password): + """Hash a password for storing.""" + + salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii') + pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), + salt, 100000) + pwdhash = binascii.hexlify(pwdhash) + return (salt + pwdhash) # return bytes + + +def verify_pass(provided_password, stored_password): + """Verify a stored password against one provided by user""" + + stored_password = stored_password.decode('ascii') + salt = stored_password[:64] + stored_password = stored_password[64:] + pwdhash = hashlib.pbkdf2_hmac('sha512', + provided_password.encode('utf-8'), + salt.encode('ascii'), + 100000) + pwdhash = binascii.hexlify(pwdhash).decode('ascii') + return pwdhash == stored_password diff --git a/apps/config.py b/apps/config.py new file mode 100644 index 0000000..5f0e1e3 --- /dev/null +++ b/apps/config.py @@ -0,0 +1,52 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +import os + + +class Config(object): + + basedir = os.path.abspath(os.path.dirname(__file__)) + + # Set up the App SECRET_KEY + # SECRET_KEY = config('SECRET_KEY' , default='S#perS3crEt_007') + SECRET_KEY = os.getenv('SECRET_KEY', 'S#perS3crEt_007') + + # This will create a file in FOLDER + SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'db.sqlite3') + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Assets Management + ASSETS_ROOT = os.getenv('ASSETS_ROOT', '/static/assets') + + +class ProductionConfig(Config): + DEBUG = False + + # Security + SESSION_COOKIE_HTTPONLY = True + REMEMBER_COOKIE_HTTPONLY = True + REMEMBER_COOKIE_DURATION = 3600 + + # PostgreSQL database + SQLALCHEMY_DATABASE_URI = '{}://{}:{}@{}:{}/{}'.format( + os.getenv('DB_ENGINE' , 'mysql'), + os.getenv('DB_USERNAME' , 'appseed_db_usr'), + os.getenv('DB_PASS' , 'pass'), + os.getenv('DB_HOST' , 'localhost'), + os.getenv('DB_PORT' , 3306), + os.getenv('DB_NAME' , 'appseed_db') + ) + + +class DebugConfig(Config): + DEBUG = True + + +# Load all possible configurations +config_dict = { + 'Production': ProductionConfig, + 'Debug' : DebugConfig +} diff --git a/apps/home/__init__.py b/apps/home/__init__.py new file mode 100644 index 0000000..dec76db --- /dev/null +++ b/apps/home/__init__.py @@ -0,0 +1,12 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from flask import Blueprint + +blueprint = Blueprint( + 'home_blueprint', + __name__, + url_prefix='' +) diff --git a/apps/home/forms.py b/apps/home/forms.py new file mode 100644 index 0000000..906133f --- /dev/null +++ b/apps/home/forms.py @@ -0,0 +1,14 @@ +from flask_wtf import FlaskForm +from wtforms import FloatField, IntegerField +from wtforms.validators import NumberRange, DataRequired + + +class MREForm(FlaskForm): + consumer_fico = IntegerField('FICO Score', id='consumer_fico', + validators=[DataRequired(), + NumberRange(300, 850, message='Fico scores must be between 300 and 850')]) + down_payment_percent = FloatField('Down Payment %', id='down_payment_percent', + validators=[DataRequired(), + NumberRange(0.0, 99.99, message='Down Payment must be between 0 and 99.99')]) + home_value = FloatField('Home Price', id='home_value', validators=[DataRequired()]) + diff --git a/apps/home/routes.py b/apps/home/routes.py new file mode 100644 index 0000000..3fc69ff --- /dev/null +++ b/apps/home/routes.py @@ -0,0 +1,79 @@ +# -*- encoding: utf-8 -*- +""" +Copyright (c) 2019 - present AppSeed.us +""" + +from apps.home import blueprint +from apps.home.forms import MREForm +from apps.MRE import compute_mre, ConsumerNotEligibleError +from flask import render_template, request, flash, jsonify +from flask_login import login_required +from jinja2 import TemplateNotFound + + +@blueprint.route('/') +@blueprint.route('/index') +def index(): + mre_form = MREForm(request.form) + # if request.method == 'POST' and mre_form.validate_on_submit(): + # try: + # mre = compute_mre(mre_form.home_value, mre_form.down_payment_percent, mre_form.consumer_fico) + # return render_template('home/mre.html', form=mre_form) + # + # except ConsumerNotEligibleError as ex: + # flash(f'Error: {ex.reason}') + + return render_template('home/mre.html', form=mre_form) + + +@blueprint.route('/compute_mre', methods=['GET']) +def ajax_compute_mre(): + home_value = float(request.args.get('home_value').replace(',', '')) + consumer_fico = int(request.args.get('consumer_fico')) + down_payment_percent = float(request.args.get('down_payment_percent')) + try: + result = compute_mre(home_value, down_payment_percent, consumer_fico) + except ConsumerNotEligibleError as ex: + return jsonify({'error': ex.reason}) + + print(f'Computed MRE: {result}') + + return jsonify({'computed_mre': result}) + + +@blueprint.route('/