commit e95bfe9fc4e128c25c409525fab0390d1f14ba78 Author: ChristopherDiesch Date: Wed Mar 12 20:43:26 2025 -0600 Initial Commit diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..254c3a2 Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..dd420cf --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[report] +omit="*/venv/*" \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b7e9ce9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "UI"] + path = UI + url = https://github.com/QuarterHomes/Quarterforms.git diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/ApplicantPortal.iml b/.idea/ApplicantPortal.iml new file mode 100644 index 0000000..d8b3f6c --- /dev/null +++ b/.idea/ApplicantPortal.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..23231ce --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..531ceef --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..ccf030d --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,86 @@ +from flask import Flask, g, request, flash, render_template +from flask_jwt_extended import current_user +import os +import time +from .auth import blueprint as users_blueprint, jwt +from .routes import blueprint as main_blueprint +from .util.log import set_up_logging +from .api import blueprint as api_blueprint +from .applicant import blueprint as apply_blueprint +from .cors import cors +from app.model import db +from app import config +from flask_mongoengine import MongoEngineSessionInterface + + +def config_from_environ(app, prefix=None): + if not prefix: + prefix = app.config.get('APP_NAME') + + for name, value in os.environ.items(): + if name.startswith(prefix): + if isinstance(value, str): + value = value.strip() + + if value: + app.config[name[len(prefix) + 1:]] = value + + +def create_app(app_name=None, config_override={}, config_objs=[], permanent_session_ttl=None, add_ping=True, ping_url='/api/ping'): + + if not config_objs: + config_objs = [config] + + if not app_name: + app_name = 'QUARTER_WEB' + + app_root_path = os.path.abspath(os.path.dirname(__file__)) + + app = Flask(app_name, template_folder=os.path.join(app_root_path, 'templates'), static_folder=os.path.join(app_root_path, 'static')) + + for obj in config_objs: + app.config.from_object(obj) + # configure any environment variables + config_from_environ(app) + + for key, value in config_override.items(): + app.config[key] = value + + # init all the app components + db.init_app(app) + jwt.init_app(app) + cors.init_app(app) + + app.session_interface = MongoEngineSessionInterface(db) + + app.secret_key = config.SECRET_KEY + app.permanent_session_lifetime = permanent_session_ttl if permanent_session_ttl else app.config.get('PERMANENT_SESSION_TTL') + + app.register_blueprint(api_blueprint, url_prefix='/api') + + for blueprint in [main_blueprint, users_blueprint, apply_blueprint]: + app.register_blueprint(blueprint) + + set_up_logging(app, app_name) + + @app.errorhandler(401) + def unauthorized(e): + flash('You are not authorized to access this page, please log in') + return render_template('auth/unauthorized.html') + + @app.before_request + def before_request(): + g.start = time.time() + + @app.after_request + def after_request(response): + request_time = time.time() - g.start + app.logger.info(f'HTTP request completed (method={request.method}, path={request.path}, status_code={response.status_code}, request_time={request_time}s, user={current_user.user_id if current_user else "None"})') + return response + + if add_ping: + @app.route(ping_url) + def ping(): + return {'ready': True}, 200 + + return app diff --git a/app/admin/__init__.py b/app/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/admin/api.py b/app/admin/api.py new file mode 100644 index 0000000..14752be --- /dev/null +++ b/app/admin/api.py @@ -0,0 +1,32 @@ +from flask import jsonify, make_response +from flask_jwt_extended import create_access_token, get_jti +from flask_restx import Resource, Namespace +from flask_restx.reqparse import RequestParser +from datetime import datetime +from app.auth import role_required +from app.auth.request_parsers import user_id_parser +from app import model + +namespace = Namespace('admin', description='API endpoint for admin users.', decorators=[role_required([model.Roles.ADMIN])]) + +delete_access_token_parser = RequestParser() +delete_access_token_parser.add_argument('access_token', type=str, required=True, help='The access token to revoke.') + + +@namespace.route('/ajax/access_token') +@namespace.doc(params={'user_id': 'The ID of the user to get an access token for'}) +class AccessToken(Resource): + @namespace.doc(descrption='Gets an access token for a user') + @namespace.expect(user_id_parser) + def get(self): + args = user_id_parser.parse_args() + user = model.User.objects(user_id=args.user_id).first_or_404() + return make_response(jsonify(access_token=create_access_token(user)), 200) + + @namespace.doc('Revokes the given access token.') + @namespace.expect(delete_access_token_parser) + def delete(self): + args = delete_access_token_parser.parse_args() + jti = get_jti(args.access_token) + to_blacklist = model.BlackListedJWT(jti=jti, created_at=datetime.utcnow()) + to_blacklist.save() diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..fa4c9a3 --- /dev/null +++ b/app/api.py @@ -0,0 +1,22 @@ +from flask import Blueprint +from flask_restx import Api +from .auth.api import auth as auth_namespace +from .applicant.api import applicant_namespace +from .network_admin.api import namespace as network_admin_namespace +from .admin.api import namespace as admin_namespace + +authorizations = {'Bearer': {'type': 'apiKey', 'in': 'header', 'name': 'Auth-Header'}} + +blueprint = Blueprint('api', __name__) + +api = Api( + blueprint, + doc='/doc/', + title='Quarter backend API documentation', + description='Welcome to the API documentation for the Quarter backend!', + authorizations=authorizations +) +api.add_namespace(auth_namespace, '/auth') +api.add_namespace(applicant_namespace, '') +api.add_namespace(network_admin_namespace, '/network_admin') +api.add_namespace(admin_namespace, '/admin') diff --git a/app/applicant/__init__.py b/app/applicant/__init__.py new file mode 100644 index 0000000..c6b3de8 --- /dev/null +++ b/app/applicant/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +blueprint = Blueprint('apply', __name__, url_prefix='/apply') + +from app.applicant import routes diff --git a/app/applicant/api.py b/app/applicant/api.py new file mode 100644 index 0000000..a9517b5 --- /dev/null +++ b/app/applicant/api.py @@ -0,0 +1,95 @@ +import os +import json +from flask import jsonify, request, make_response, current_app +from jsonschema import ValidationError +from flask_restx import Resource, Namespace +from flask_jwt_extended import jwt_required, current_user +from app import model +from app.util import emtpy_string_to_none, dict_to_snake +from app.util.validators import validate_schema + +applicant_namespace = Namespace('applicant', description='The API endpoints for applicants', path='/') + + +@applicant_namespace.route('/ajax/application/') +@applicant_namespace.route('/ajax/application') +class SubmitOccupantApplication(Resource): + + @jwt_required() + @applicant_namespace.doc(responses={ + 200: 'The Application was retrieved successfully.', + 401: 'The user is not authorized to access the application.' + }) + def get(self, finalize='False'): + if not current_user: + return make_response(jsonify({'message': f'No user found'}), 404) + + application = model.Application.objects(applicants__match={'email': current_user.email}).first() + user = model.User.objects(user_id=current_user.user_id).first() + # if there is no application, create one + if not application: + if model.Roles.APPLICANT not in current_user.roles: + current_user.roles.append(model.Roles.APPLICANT) + current_user.save() + + application = model.Application(user_id=current_user.user_id, + applicants=[model.Applicant(user=user, user_id=current_user.user_id)], + property_info=model.Property(), + deposit_held_by=None) + application.save() + return make_response(jsonify(application.to_camel_case()), 200) + + @jwt_required() + @applicant_namespace.doc(responses={ + 200: 'The application was saved to the user successfully.', + 402: 'The application was finalized and the application is no longer allowed to be edited.', + 401: 'The user is not authorized to access the application.', + 403: 'The given user the co-applicant, and it only able to view the application, not modify it' + }, + params={ + 'finalize': '"true" if the application should be finalized, "false" otherwise.' + }) + def post(self, finalize='False'): + data = request.get_data() + if isinstance(data, (str, bytes)): + data = json.loads(data) + data = emtpy_string_to_none(data) + try: + validate_schema(data, os.path.join(os.path.abspath(os.path.dirname(__file__)), 'schemas', 'Application.json')) + current_app.logger.info(f'Data: {data}') + except ValidationError as err: + field = ".".join([f'[{p}]' if isinstance(p, int) else str(p) for p in err.path]) + return make_response(jsonify(message='The given request does not follow the correct schema', error=f'Field "{field}": {err.message}'), 400) + + # should the application be finalized? + finalize = finalize.lower() == 'true' + application = model.Application.objects(applicants__match={'email': current_user.email}).first() + # if this user is NOT the "main" applicant + if application and not application.user_id == current_user.user_id: + return make_response(jsonify({'message': 'The given application cannot be modified by this user'}), 403) + + # if an application exists for the user + if application: + # was that application finalized? + if application.finalized: + return make_response(jsonify({'message': f'The application for the user "{current_user.user_id}" is finalized.'}), 402) + + application.applicants = [model.Applicant.from_camel_case(a) for a in data.get('applicants', [])] + + prop_info = data.get('propertyInfo') + if prop_info: + application.property_info = model.Property.from_camel_case(prop_info) + + kwargs = dict_to_snake(data) + for ignore in application.ignore_from_json() + ['applicants', 'property_info']: + kwargs.pop(ignore, None) + + application.update(**kwargs) + + else: + application = model.Application.from_camel_case(data) + + application.finalized = finalize + application.save() + + return make_response(jsonify({'message': 'Occupant application saved successfully'}), 200) diff --git a/app/applicant/model.py b/app/applicant/model.py new file mode 100644 index 0000000..aa2b9d0 --- /dev/null +++ b/app/applicant/model.py @@ -0,0 +1,296 @@ +from datetime import datetime +from dateutil.relativedelta import relativedelta +from app.util.datatypes.enum import BaseEnum +from bson import json_util +from app.auth.model import User, UserIDField +from ..database import db, EmbeddedDocumentBase, DocumentBase +from app.database.fields import QuarterDateField, QuarterYearField + + +class MarriageStatus(BaseEnum): + SINGLE = 'single' + MARRIED = 'married' + DIVORCED = 'divorced' + WIDOWED = 'windowed' + SEPARATED = 'separated' + + +class OwnOrRent(BaseEnum): + OWN = 'own' + RENT = 'rent' + + +class FilingTypes(BaseEnum): + SINGLE = 'single' + JOINT = 'joint' + + +class Property(EmbeddedDocumentBase): + original_cost = db.FloatField(required=False) + home_price = db.FloatField(required=False) + address1 = db.StringField(required=False) + address2 = db.StringField(required=False) + city = db.StringField(required=False) + state = db.StringField(required=False) + zip = db.StringField(required=False) + number_of_units = db.IntField(required=False) + funding_purpose = db.StringField(required=False, default='purchase') + property_purpose = db.StringField(required=False) + year_acquired = QuarterYearField(required=False) + year_built = QuarterYearField(required=False) + existing_liens = db.EmbeddedDocumentListField('AssetExpense', required=False) + title_holders = db.StringField(required=False) + tax_amount = db.FloatField(required=False) + hoa_amount = db.FloatField(required=False) + downpayment_source = db.StringField(required=False) + property_type = db.StringField(required=False) + + +class Job(EmbeddedDocumentBase): + employer_info = db.StringField(required=False) + self_employed = db.BooleanField(required=False) + date_from = QuarterDateField(required=False) + date_to = QuarterDateField(required=False, default=datetime.today()) + monthly_income = db.FloatField(required=False) + position_title = db.StringField(required=False) + bus_phone = db.StringField(required=False) + naics = db.StringField(required=False) + + def _get_date_delta_on_job(self, time_scale='years'): + if self.date_to and self.date_from: + date_from = QuarterDateField.make_datetime(self.date_to) + date_to = QuarterDateField.make_datetime(self.date_from) + return getattr(relativedelta(date_from, date_to), time_scale) + return 0 + + @classmethod + def ignore_from_json(cls): + result = super().ignore_from_json() + result.append('years_moths_on_job') + return result + + @property + def years_on_job(self): + return self._get_date_delta_on_job('years') + + @property + def months_on_job(self): + return self._get_date_delta_on_job('months') + + @property + def years_months_on_job(self): + years = self.years_on_job + extra_months = self.months_on_job + return f'{years} Year(s), {extra_months} Month(s)' + + @classmethod + def additional_fields(cls): + res = super().additional_fields() + res.append('years_months_on_job') + return res + + +class PrevAddress(EmbeddedDocumentBase): + address1 = db.StringField(required=False) + address2 = db.StringField(required=False) + city = db.StringField(required=False) + state = db.StringField(required=False) + zip = db.StringField(required=False) + own_or_rent = db.EnumField(OwnOrRent, required=False) + years = db.IntField(required=False) + + +class AssetBase(EmbeddedDocumentBase): + meta = {'allow_inheritance': True} + value = db.FloatField(required=False, default=0) + + +class AssetExpense(AssetBase): + meta = {'allow_inheritance': True} + description = db.StringField(required=False, default='') + + +class CheckingSavings(AssetBase): + bank_sl_cu = db.StringField(required=False) + acct_number = db.StringField(required=False) + + +class LifeInsurance(AssetBase): + face_amount = db.FloatField(required=False, default=None) + + +class Expense(EmbeddedDocumentBase): + base_emp_inc = db.FloatField(required=False) + over_time = db.FloatField(required=False) + bonuses = db.FloatField(required=False) + commissions = db.FloatField(required=False) + dividends_interest = db.FloatField(required=False) + net_rental_income = db.FloatField(required=False) + other = db.EmbeddedDocumentField('AssetExpense', required=False) + + +class ScheduleOfRealEstate(EmbeddedDocumentBase): + address1 = db.StringField(required=False) + address2 = db.StringField(required=False) + city = db.StringField(required=False) + state = db.StringField(required=False) + zip = db.StringField(required=False) + status = db.StringField(required=False) + type = db.StringField(required=False) + present_market_value = db.FloatField(required=False) + amount_of_mortgages = db.FloatField(required=False) + gross_rental_income = db.FloatField(required=False) + mortgage_payments = db.FloatField(required=False) + insurance_main_taxes_misc = db.FloatField(required=False) + net_rental_income = db.FloatField(required=False) + + +class Declarations(EmbeddedDocumentBase): + any_judgements = db.BooleanField(required=False) + declared_bankruptcy = db.BooleanField(required=False) + property_foreclosed = db.BooleanField(required=False) + party_to_lawsuit = db.BooleanField(required=False) + obligated_loan_foreclosure = db.BooleanField(required=False) + delinquent_or_default = db.BooleanField(required=False) + alimony_child_support_maintenance = db.BooleanField(required=False) + down_payment_borrowed = db.BooleanField(required=False) + comaker_or_endorser = db.BooleanField(required=False) + intend_primary_residence = db.BooleanField(Required=False) + + +class AcknowledgeAgree(EmbeddedDocumentBase): + signature = db.StringField(required=False) + date = QuarterDateField(required=False) + + +class GovernmentInfo(EmbeddedDocumentBase): + will_not_furnish = db.BooleanField(required=False) + ethnicity = db.BooleanField(required=False) + sex = db.StringField(required=False) + race = db.StringField(required=False) + + +class GeneralAssets(AssetExpense): + deposit_held_by = db.StringField(required=False, default='') + + +class Assets(EmbeddedDocumentBase): + assets = db.EmbeddedDocumentListField('GeneralAssets', required=False, default=[GeneralAssets()]) + checking_savings = db.EmbeddedDocumentListField('CheckingSavings', required=False, default=[]) + stocks_bonds = db.EmbeddedDocumentListField('AssetExpense', required=False, default=[]) + life_insurance = db.EmbeddedDocumentListField('LifeInsurance', required=False, default=[]) + retirement_funds = db.EmbeddedDocumentListField('AssetExpense', required=False, default=[]) + vehicles_owned = db.EmbeddedDocumentListField('AssetExpense', required=False, default=[]) + property_owned = db.EmbeddedDocumentListField('ScheduleOfRealEstate', required=False, default=[]) + owned_real_estate_value = db.FloatField(required=False, default=None) + vested_interest_retirement_fund = db.FloatField(required=False, default=None) + net_worth_bus_owned = db.FloatField(required=False, default=None) + autos_owned_value = db.FloatField(required=False, default=None) + other_assets = db.StringField(required=False, default='') + + +class Applicant(EmbeddedDocumentBase): + + def __init__(self, *args, **kwargs): + user = None + if 'email' in kwargs: + user = User.objects(email=kwargs['email']).first() + elif 'user_id' in kwargs: + user = User.objects(user_id=kwargs['user_id']).first() + + if user: + kwargs['user'] = user + kwargs['user_id'] = user.user_id + + user.date_of_birth = kwargs.get('date_of_birth') or user.date_of_birth + user.first_name = kwargs.get('first_name') or user.first_name + user.middle_init_or_name = kwargs.get('middle_init_or_name') or user.middle_init_or_name + user.last_name = kwargs.get('last_name') or user.last_name + user.address1 = kwargs.get('address1') or user.address1 + user.address2 = kwargs.get('address2') or user.address2 + user.city = kwargs.get('city') or user.city + user.state = kwargs.get('state') or user.state + user.zip = kwargs.get('zip') or user.zip + user.phone_number = kwargs.get('phone_number') or user.phone_number + user.save() + + if 'user' in kwargs: + kwargs['email'] = kwargs['user'].email + kwargs['first_name'] = kwargs['user'].first_name + kwargs['middle_init_or_name'] = kwargs['user'].middle_init_or_name + kwargs['last_name'] = kwargs['user'].last_name + kwargs['address1'] = kwargs['user'].address1 + kwargs['address2'] = kwargs['user'].address2 + kwargs['city'] = kwargs['user'].city + kwargs['state'] = kwargs['user'].state + kwargs['zip'] = kwargs['user'].zip + kwargs['phone_number'] = kwargs['user'].phone_number + kwargs['date_of_birth'] = kwargs['user'].date_of_birth + kwargs.pop('user') + + super().__init__(*args, **kwargs) + + user = db.ReferenceField('User', required=False) + user_id = UserIDField(required=False) + # these properties will be loaded from the user if there is one, if there isn't one, they will be saved here + email = db.StringField(required=False) + first_name = db.StringField(required=False) + middle_init_or_name = db.StringField(required=False, db_field='middle_initial') + last_name = db.StringField(required=False) + address1 = db.StringField(required=False) + address2 = db.StringField(required=False) + city = db.StringField(required=False) + state = db.StringField(required=False) + zip = db.StringField(required=False) + date_of_birth = QuarterDateField(required=False) + phone_number = db.StringField(required=False, db_field='home_phone') + # the applicant specific fields + role = db.StringField(required=False) + pin = db.StringField(required=False) + social_security_number = db.StringField(required=False) + years_school = db.IntField(required=False) + degree_earned = db.StringField(required=False) + school_name = db.StringField(required=False) + marriage_status = db.EnumField(MarriageStatus, required=False, default=MarriageStatus.SINGLE) + number_dependents = db.IntField(required=False, default=0) + ages_dependents = db.ListField(db.IntField(required=False)) + previous_jobs = db.EmbeddedDocumentListField('Job', required=False) + previous_addresses = db.EmbeddedDocumentListField('PrevAddress', required=False) + expense_info = db.EmbeddedDocumentField('Expense', required=False) + other_incomes = db.EmbeddedDocumentListField('AssetExpense', required=False) + assets = db.EmbeddedDocumentField('Assets', required=False, default=Assets()) + declarations = db.EmbeddedDocumentField('Declarations', required=False) + liabilities = db.EmbeddedDocumentListField('AssetExpense', required=False) + acknowledge_and_agree = db.EmbeddedDocumentField('AcknowledgeAgree', required=False) + auth_borrower = db.EmbeddedDocumentField('AcknowledgeAgree', required=False) + auth_fcra = db.EmbeddedDocumentField('AcknowledgeAgree', required=False) + government_info = db.EmbeddedDocumentField('GovernmentInfo') + + @classmethod + def ignore_to_json(cls): + result = super().ignore_to_json() + result.append('user') + result.append('user_id') + return result + + @classmethod + def ignore_from_json(cls): + result = super().ignore_from_json() + result.append('user_name') + return result + + +class Application(DocumentBase): + filing_type = db.EnumField(FilingTypes, required=False, default=FilingTypes.SINGLE) + user_id = UserIDField(required=True) + applicants = db.EmbeddedDocumentListField('Applicant', required=True) + deposit_held_by = db.StringField(required=False) + property_info = db.EmbeddedDocumentField('Property', required=False) + finalized = db.BooleanField(required=True, default=False) + + def to_json(self, *args, **kwargs): + use_db_field = kwargs.pop('use_db_field', True) + result = super().to_mongo(use_db_field=use_db_field) + for applicant in result.get('applicants', [{}]): + applicant.pop('user', None) + return json_util.dumps(result, *args, **kwargs) diff --git a/app/applicant/routes.py b/app/applicant/routes.py new file mode 100644 index 0000000..7ddd8b7 --- /dev/null +++ b/app/applicant/routes.py @@ -0,0 +1,9 @@ +import os +from flask import render_template, current_app +from . import blueprint + + +@blueprint.route('/home') +def apply_home(): + rendered = render_template('apply/index.html') + return rendered diff --git a/app/applicant/schemas/Address.json b/app/applicant/schemas/Address.json new file mode 100644 index 0000000..9bcb227 --- /dev/null +++ b/app/applicant/schemas/Address.json @@ -0,0 +1,69 @@ +{ + "$defs": { + "PropertyBase": { + "type": "object", + "properties": { + "address1": { + "type": "string" + }, + "address2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + } + } + }, + "PreviousAddress": { + "type": "object", + "allOf": [{"$ref": "#/$defs/PropertyBase"}], + "properties": { + "ownOrRent": { + "type": "string", + "enum": ["own", "rent"] + }, + "years": { + "type": "number" + } + } + }, + "ScheduleOfRealEstate": { + "title": "ScheduleOfRealEstate", + "type": "object", + "allOf": [{"$ref": "#/$defs/PropertyBase"}], + "properties": { + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "presentMarketValue": { + "type": "number" + }, + "amountOfMortgages": { + "type": "number" + }, + "grossRentalIncome": { + "type": "number" + }, + "mortgagePayments": { + "type": "number" + }, + "insuranceMainTaxesMisc": { + "type": "number" + }, + "netRentalIncome": { + "type": "number" + } + } + } + }, + "$schema": "https://json-schema.org/draft-07/schema" +} \ No newline at end of file diff --git a/app/applicant/schemas/Applicant.json b/app/applicant/schemas/Applicant.json new file mode 100644 index 0000000..8387175 --- /dev/null +++ b/app/applicant/schemas/Applicant.json @@ -0,0 +1,200 @@ +{ + "$defs": { + "AcknowledgeAgree": { + "title": "AcknowledgeAgree", + "type": "object", + "properties": { + "signature": { + "type": "string" + }, + "date": { + "type": "string" + } + } + }, + "GovernmentInfo": { + "title": "GovernmentInfo", + "type": "object", + "properties": { + "willNotFurnish": { + "type": "boolean" + }, + "ethnicity": { + "type": "boolean" + }, + "sex": { + "type": "string" + }, + "race": { + "type": "string" + } + } + }, + "Assets": { + "title": "Assets", + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "allOf": [{ + "$ref": "Assets.json#/$defs/AssetExpense" + }], + "type": "object", + "properties": { + "depositHeldBy": { + "type": "string" + } + } + } + }, + "checkingSavings": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/CheckingSavings" + } + }, + "stocksBonds": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + }, + "lifeInsurance": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/LifeInsurance" + } + }, + "retirementFunds": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + }, + "vehiclesOwned": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + }, + "propertyOwned": { + "type": "array", + "items": { + "$ref": "Address.json#/$defs/ScheduleOfRealEstate" + } + }, + "liabilities": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + } + } + } + }, + "$schema": "https://json-schema.org/draft-07/schema", + "Applicant": { + "title": "Applicant", + "type": "object", + "required": [ + "email" + ], + "properties": { + "pin": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "middleInitial": { + "type": "string" + }, + "email": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "socialSecurityNumber": { + "type": "string" + }, + "currentAddress": { + "type": "string" + }, + "homePhone": { + "type": "string" + }, + "dateOfBirth": { + "type": "string" + }, + "yearsSchool": { + "type": "integer" + }, + "degreeEarned": { + "type": "string" + }, + "schoolName": { + "type": "string" + }, + "marriageStatus": { + "type": "string", + "enum": [ + "single", + "married", + "divorced", + "widowed", + "separated" + ] + }, + "numberDependants": { + "type": "integer" + }, + "agesDependants": { + "type": "array", + "items": { + "type": "integer" + } + }, + "previousJobs": { + "type": "array", + "items": { + "$ref": "Job.json" + } + }, + "previousAddresses": { + "type": "array", + "items": { + "$ref": "Address.json#/$defs/PreviousAddress" + } + }, + "expenseInfo": { + "$ref": "Expense.json" + }, + "otherIncomes": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + }, + "assets": { + "$ref": "#/$defs/Assets" + }, + "declarations": { + "$ref": "Declarations.json" + }, + "acknowledgeAndAgree": { + "$ref": "#/$defs/AcknowledgeAgree" + }, + "authBorrower": { + "$ref": "#/$defs/AcknowledgeAgree" + }, + "AuthFcra": { + "$ref": "#/$defs/AcknowledgeAgree" + }, + "governmentInfo": { + "$ref": "#/$defs/GovernmentInfo" + } + } + } +} diff --git a/app/applicant/schemas/Application.json b/app/applicant/schemas/Application.json new file mode 100644 index 0000000..c02a575 --- /dev/null +++ b/app/applicant/schemas/Application.json @@ -0,0 +1,91 @@ +{ + "$defs": { + "PropertyInfo": { + "title": "PropertyInfo", + "type": "object", + "properties": { + "homePrice": { + "type": "number" + }, + "address1": { + "type": "string" + }, + "address2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "numberOfUnits": { + "type": "integer" + }, + "fundingPurpose": { + "type": "string" + }, + "propertyPurpose": { + "type": "string" + }, + "yearAcquired": { + "type": "string" + }, + "yearBuilt": { + "type": "string" + }, + "originalCost": { + "type": "number" + }, + "existingLiens": { + "type": "array", + "items": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + }, + "titleHolders": { + "type": "string" + }, + "taxAmount": { + "type": "number" + }, + "hoaAmount": { + "type": "number" + }, + "downPaymentSource": { + "type": "string" + } + } + } + }, + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Application", + "type": "object", + "properties": { + "userID": { + "type": "string" + }, + "applicants": { + "type": "array", + "items": { + "$ref": "Applicant.json#Applicant" + } + }, + "filingType": { + "type": "string", + "enum": [ + "single", + "joint" + ] + }, + "propertyInfo": { + "$ref": "#/$defs/PropertyInfo" + }, + "depositHeldBy": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/app/applicant/schemas/Assets.json b/app/applicant/schemas/Assets.json new file mode 100644 index 0000000..6042e83 --- /dev/null +++ b/app/applicant/schemas/Assets.json @@ -0,0 +1,44 @@ +{ + "$defs": { + "AssetExpenseBase": { + "type": "object", + "properties": { + "value": { + "type": "number" + } + } + }, + "AssetExpense": { + "title": "AssetExpense", + "type": "object", + "allOf": [{"$ref": "#/$defs/AssetExpenseBase"}], + "properties": { + "description": { + "type": "string" + } + } + }, + "CheckingSavings": { + "type": "object", + "allOf": [{"$ref": "#/$defs/AssetExpenseBase"}], + "properties": { + "bankSlCu": { + "type": "string" + }, + "acctNumber": { + "type": "string" + } + } + }, + "LifeInsurance": { + "type": "object", + "allOf": [{"$ref": "#/$defs/AssetExpenseBase"}], + "properties": { + "faceAmount": { + "type": "number" + } + } + } + }, + "$schema": "https://json-schema.org/draft-07/schema" +} \ No newline at end of file diff --git a/app/applicant/schemas/Declarations.json b/app/applicant/schemas/Declarations.json new file mode 100644 index 0000000..8ebe4ab --- /dev/null +++ b/app/applicant/schemas/Declarations.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Declarations", + "type": "object", + "properties": { + "anyJudgements": { + "type": "boolean" + }, + "declaredBankruptcy": { + "type": "boolean" + }, + "propertyForeclosed": { + "type": "boolean" + }, + "partyToLawSuit": { + "type": "boolean" + }, + "obligatedLoanForeclosure": { + "type": "boolean" + }, + "delinquentOrDefault": { + "type": "boolean" + }, + "alimonyChildSupportMaintenance": { + "type": "boolean" + }, + "downPaymentBorrowed": { + "type": "boolean" + }, + "comakerOrEndorser": { + "type": "boolean" + }, + "intendedPrimaryResidence": { + "type": "boolean" + } + } +} \ No newline at end of file diff --git a/app/applicant/schemas/Expense.json b/app/applicant/schemas/Expense.json new file mode 100644 index 0000000..dfbd7e7 --- /dev/null +++ b/app/applicant/schemas/Expense.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Expense", + "type": "object", + "properties": { + "baseEmpInc": { + "type": "number" + }, + "overTime": { + "type": "number" + }, + "bonuses": { + "type": "number" + }, + "commissions": { + "type": "number" + }, + "dividendsInterest": { + "type": "number" + }, + "netRentalIncome": { + "type": "number" + }, + "other": { + "$ref": "Assets.json#/$defs/AssetExpense" + } + } +} \ No newline at end of file diff --git a/app/applicant/schemas/Job.json b/app/applicant/schemas/Job.json new file mode 100644 index 0000000..ec627f3 --- /dev/null +++ b/app/applicant/schemas/Job.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Job", + "type": "object", + "properties": { + "employerInfo": { + "type": "string" + }, + "selfEmployed": { + "type": "boolean" + }, + "yearsOnJob": { + "type": "integer" + }, + "dateFrom": { + "type": "string" + }, + "dateTo": { + "type": "string" + }, + "monthlyIncome": { + "type": "number" + }, + "positionTitle": { + "type": "string" + }, + "busPhone": { + "type": "string" + } + } +} diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 0000000..f6e5403 --- /dev/null +++ b/app/auth/__init__.py @@ -0,0 +1,80 @@ +from flask import Blueprint, jsonify, current_app, make_response +from flask_restx import abort +from flask_jwt_extended import JWTManager, verify_jwt_in_request, get_jwt +from functools import wraps +from app_common.const import USER_NOT_ACTIVE_MESSAGE, USER_NOT_ACTIVE_STATUS_CODE +from .model import Roles, BlackListedJWT + + +jwt = JWTManager() +blueprint = Blueprint('auth', __name__) + +from app.auth import routes +from app.auth.model import User, BlackListedJWT + + +NO_USER_FOUND_MESSAGE = 'No user found' + + +@jwt.user_identity_loader +def _user_identity_lookup(user): + return user.user_id + + +@jwt.additional_claims_loader +def add_claims_to_token(identity): + return {'roles': ','.join([str(r) for r in identity.roles])} + + +@jwt.user_lookup_loader +def _user_lookup_callback(_jwt_header, jwt_data): + user_id = jwt_data['sub'] + return User.objects(user_id=user_id, active=True).first() + + +@jwt.user_lookup_error_loader +def _user_lookup_error_callback(_jwt_header, jwt_data): + user_id = jwt_data['sub'] + user = User.objects(user_id=user_id).first() + if not user: + return abort(404, NO_USER_FOUND_MESSAGE) + elif not user.active: + return abort(USER_NOT_ACTIVE_STATUS_CODE, USER_NOT_ACTIVE_MESSAGE) + else: + return make_response(jsonify(message='An error occurred trying to load the given user.', user_id=user_id), 400) + + +@jwt.token_in_blocklist_loader +def _is_token_revoked(_jwt_header, jwt_payload): + return BlackListedJWT.is_blacklisted(jwt_payload['jti']) + + +@jwt.expired_token_loader +def _expired_token_callback(jwt_header, jwt_payload): + return jsonify(message='The given JWT token has expired'), 405 + + +def role_required(roles): + """A wrapper for an endpoint that will require the calling user has any of the given roles (implies `jwt_required()`). + + Arguments: + roles (`list(Roles)`): The allowed role for this endpoint. + """ + def wrapper(func): + @wraps(func) + def decorator(*args, **kwargs): + for role in roles: + if role not in Roles: + msg = f'The given role is not a valid (role="{role}", valid_roles="{Roles.values()}").' + current_app.logger.warn(msg) + return abort(401, msg) + verify_jwt_in_request() + claims = get_jwt() + user_has_role = any([role in roles for role in claims['roles'].split(',')]) + if user_has_role: + return func(*args, **kwargs) + return abort(401, f'The endpoint requires a user with the following roles "{roles}".') + + return decorator + + return wrapper diff --git a/app/auth/api.py b/app/auth/api.py new file mode 100644 index 0000000..9bcc8c6 --- /dev/null +++ b/app/auth/api.py @@ -0,0 +1,226 @@ +from datetime import datetime +from flask import jsonify, current_app, make_response, request +from flask_restx import Resource, Namespace, abort +from mongoengine.errors import NotUniqueError, ValidationError +from flask_jwt_extended import create_access_token, jwt_required, get_jwt, create_refresh_token, current_user +from app_common.const import NOT_UNIQUE, MALFORMED_DATA, USER_NOT_ACTIVE_MESSAGE, USER_NOT_ACTIVE_STATUS_CODE +from .model import User, BlackListedJWT, Roles, UserPrefs +from .email import send_password_reset_email, send_activate_account_email, send_forgot_user_id_email +from .request_parsers import post_user_login_schema, create_user_parser, reset_password_parser, update_password_parser, request_password_reset_parser, activate_account_parser, \ + resend_activation_email +from . import role_required + +auth = Namespace('auth', description='The API endpoints for user authentication.') + + +def _get_auth_payload(user, include_refresh_token=True): + kwargs = user.to_dict() + kwargs['access_token'] = create_access_token(user) + if include_refresh_token: + kwargs['refresh_token'] = create_refresh_token(user) + + return jsonify(**kwargs) + + +@auth.route('/ajax/register') +class Register(Resource): + @auth.expect(create_user_parser) + @jwt_required(optional=True) + def post(self): + current_app.logger.info(f'\n\nRequest Headers: {request.headers}\nType:{type(request.headers)}\n') + if current_user: + return abort(401, f'Already logged in as {current_user.user_id}') + + redirect_url = '' + + if request.headers.get('Referer'): + redirect_url = request.headers.get('Referer') + + current_app.logger.info(f'Redirect URL: {redirect_url}') + + redirect_url = f'{redirect_url}#home?activated=true' + + current_app.logger.info(f'Redirect URL: {redirect_url}') + + args = create_user_parser.parse_args() + + try: + user = User(user_id=args.user_id, + email=args.email, + first_name=args.first_name, + middle_init_or_name=args.middle_init_or_name, + last_name=args.last_name, + last_login=datetime.utcnow(), + join_date=datetime.utcnow(), + date_of_birth=args.date_of_birth, + phone_number=args.phone_number, + roles=[Roles.APPLICANT]) + user.set_password(args.password) + # save the new user + user.save() + except NotUniqueError: + bad_fields = [] + unique_fields = ['user_id', 'email'] + for field in unique_fields: + if User.objects(**{field: args.get(field)}).first(): + bad_fields.append(field) + return abort(400, 'A user already exists with the following fields', fields=bad_fields, error_type=NOT_UNIQUE) + except ValidationError as ex: + return abort(400, 'Invalid data given', error=str(ex), error_type=MALFORMED_DATA) + + # send the account activation email + token = user.get_activation_token(expire_secs=current_app.config['CONFIRM_EMAIL_EXP_SEC'], secret_key=current_app.config['SECRET_KEY'], redirect_url=redirect_url) + send_activate_account_email(user, token) + return {'message': 'Account activation email sent'}, 200 + + +@auth.route('/ajax/activate') +@auth.doc(description='Activates a user from a JWT token') +@auth.response(401, 'The activation token has expired') +@auth.response(402, 'The user has already activated their account') +class ActivateUser(Resource): + @auth.expect(activate_account_parser) + def post(self): + args = activate_account_parser.parse_args() + user, _ = User.from_activation_token(args.token, current_app.config['SECRET_KEY']) + if not user: + return {'message': 'The account activation token has expired.'}, 401 + if user.active: + return {'message': 'The user has already activated their account.'}, 402 + user.active = True + user.save() + + +@auth.route('/ajax/send_confirm_email') +@auth.response(404, 'No user with the given email address was found.') +@auth.doc(description='Sends the account activation email to a user.') +class SendAccountActivation(Resource): + @auth.expect(resend_activation_email) + def post(self): + args = resend_activation_email.parse_args() + user = User.objects(email=args.email).first_or_404(message=f'No user with email {args.email} found') + token = user.get_activation_token(expire_secs=current_app.config['CONFIRM_EMAIL_EXP_SEC'], secret_key=current_app.config['SECRET_KEY']) + send_activate_account_email(user, token) + + +@auth.route('/ajax/login') +class AjaxLogin(Resource): + @auth.expect(post_user_login_schema) + @jwt_required(optional=True) + def post(self): + if current_user: + return abort(401, f'A user is already logged in {current_user.user_id}') + + args = post_user_login_schema.parse_args() + user = User.objects(user_id=args.user_id).first_or_404(message=f'No user "{args.user_id}" exists.') + + if not user.active: + return make_response(jsonify(message=USER_NOT_ACTIVE_MESSAGE), USER_NOT_ACTIVE_STATUS_CODE) + + if user.check_password(args.password): + user.last_login = datetime.utcnow() + user.save() + return _get_auth_payload(user, include_refresh_token=args.remember) + + return abort(401, 'Incorrect Password') + + +@auth.route('/ajax/refresh_access_token') +class RefreshAccess(Resource): + @jwt_required(refresh=True) + def get(self): + return _get_auth_payload(current_user, include_refresh_token=False) + + +@auth.route('/ajax/test_access_token') +class TestAccessToken(Resource): + @jwt_required() + def post(self): + return {'user_id': current_user.user_id}, 200 + + +@auth.route('/ajax/logout') +class AjaxLogout(Resource): + @jwt_required() + @role_required([Roles.APPLICANT, Roles.OCCUPANT]) + def post(self): + to_blacklist = BlackListedJWT(jti=get_jwt()['jti'], created_at=datetime.utcnow()) + to_blacklist.save() + return jsonify(message='Access token revoked') + + +@auth.route('/ajax/update_password') +class UpdatePassword(Resource): + @jwt_required() + @auth.expect(update_password_parser) + def post(self): + args = update_password_parser.parse_args() + if not current_user.check_password(args.old_password): + return abort(401, 'The password is incorrect') + current_user.set_password(args.new_password) + current_user.save() + + +@auth.route('/ajax/request_password_reset') +@auth.response(404, 'No user was found') +class RequestPasswordReset(Resource): + @auth.expect(request_password_reset_parser) + def post(self): + args = request_password_reset_parser.parse_args() + # were we given an email? + if args.email: + user = User.objects(email=args.email).first_or_404() + # were we given a user id? + elif args.user_id: + user = User.objects(user_id=args.user_id).first_or_404() + # were we not given either? + else: + return abort(400, 'Either a user ID or email MUST be provided.', error_type=MALFORMED_DATA) + + token = user.get_password_reset_token(expire_secs=current_app.config['PASSWORD_RESET_EXP_SEC'], secret_key=current_app.config['SECRET_KEY']) + send_password_reset_email(user, token) + + return {'message': 'Password reset email sent successfully'}, 200 + + +@auth.route('/ajax/reset_password') +class ResetPassword(Resource): + @auth.expect(reset_password_parser) + def post(self): + args = reset_password_parser.parse_args() + user = User.from_password_reset_token(args.token, secret_key=current_app.config['SECRET_KEY']) + # if the user is none, the password reset token has expired + if not user: + return abort(401, 'The password reset token has expired') + + if user.check_password(args.new_password): + token = user.get_password_reset_token(secret_key=current_app.config['SECRET_KEY'], expire_secs=current_app.config['PASSWORD_RESET_EXP_SEC']) + return {'password_reset_token': token, 'error_type': MALFORMED_DATA}, 400 + + user.set_password(args.new_password) + user.save() + return jsonify() + + +@auth.route('/ajax/forgot_user_id') +class ForgotUsername(Resource): + @auth.expect(resend_activation_email) + def post(self): + args = resend_activation_email.parse_args() + user = User.objects(email=args.email).first_or_404() + send_forgot_user_id_email(user) + + +@auth.route('/ajax/user_prefs/') +class GetUserPrefs(Resource): + def get(self, user_id): + user = User.objects(user_id=user_id).first_or_404() + return jsonify(user.prefs) + + +@auth.route('/ajax/user_prefs') +class PostUserPrefs(Resource): + @jwt_required() + def post(self): + current_user.prefs = UserPrefs(**request.args) + current_user.save() diff --git a/app/auth/email.py b/app/auth/email.py new file mode 100644 index 0000000..f60e1b2 --- /dev/null +++ b/app/auth/email.py @@ -0,0 +1,27 @@ +from flask import render_template, current_app +from app.email import send_email + + +def send_email_to_user(user, subject, txt_body, html_body): + send_email(subject, sender=current_app.config['MAIL_USERNAME'], recipient=user.email, txt_body=txt_body, html_body=html_body) + + +def send_password_reset_email(user, token): + send_email_to_user(user, + 'Quarter Password Reset', + txt_body=render_template('email/reset_password.txt', user=user, token=token, exp_mins=int(current_app.config['PASSWORD_RESET_EXP_SEC'] / 60)), + html_body=render_template('email/reset_password.html', user=user, token=token, exp_mins=int(current_app.config['PASSWORD_RESET_EXP_SEC'] / 60))) + + +def send_activate_account_email(user, token): + send_email_to_user(user, + 'Confirm Quarter Email', + txt_body=render_template('email/confirm_email.txt', user=user, token=token, exp_mins=int(current_app.config['CONFIRM_EMAIL_EXP_SEC'] / 60)), + html_body=render_template('email/confirm_email.html', user=user, token=token, exp_mins=int(current_app.config['CONFIRM_EMAIL_EXP_SEC'] / 60))) + + +def send_forgot_user_id_email(user): + send_email_to_user(user, + 'Forgot Quarter Username', + txt_body=render_template('email/forgot_user_id.txt', user=user), + html_body=render_template('email/forgot_user_id.html', user=user)) diff --git a/app/auth/forms.py b/app/auth/forms.py new file mode 100644 index 0000000..511262c --- /dev/null +++ b/app/auth/forms.py @@ -0,0 +1,93 @@ +from wtforms.form import Form +from wtforms.fields import StringField, PasswordField, BooleanField, DateField +from wtforms import validators as wtf_validators +from wtforms import ValidationError +from datetime import datetime +from .model import User +from app.util import validators + + +class LoginForm(Form): + user_id = StringField('Username', [wtf_validators.required()]) + password = PasswordField('Password', [wtf_validators.required(), wtf_validators.length(min=8, message='Password MUST be at least 8 characters long.')]) + remember_me = BooleanField('Remember Me?') + + @staticmethod + def validate_user_id(form, field): + user = User.objects(user_id=field.data).first() + if not user: + raise ValidationError('The given user does not exist') + + @staticmethod + def validate_password(form, field): + user = User.objects(user_id=form.user_id.data).first() + if user: + if not user.check_password(field.data): + raise ValidationError('The given password is incorrect') + + +class ResetPasswordForm(Form): + password = PasswordField('Password', [wtf_validators.required()]) + confirm_password = PasswordField('Confirm Password', [wtf_validators.required(), wtf_validators.equal_to('password', message='Passwords are not equal.')]) + + +class RequestPasswordResetForm(Form): + email = StringField('Email', [wtf_validators.email()]) + user_id = StringField('Username') + + @staticmethod + def validate_email(form, field): + if not field.data: + if not form.user_id.data: + raise ValidationError('Either a username or email MUST be provided.') + + else: + user = User.objects(email=field.data).first() + if not user: + raise ValidationError('The given email is not associated with any account.') + + @staticmethod + def validate_user_id(form, field): + if not field.data: + if not form.email.data: + raise ValidationError('Either a username or email MUST be provided.') + + else: + user = User.objects(user_id=field.data).first() + if not user: + raise ValidationError('There is no account with the given username') + + +class RegisterForm(Form): + user_id = StringField('Username', [wtf_validators.required()]) + email = StringField('Email', [wtf_validators.email(), wtf_validators.required()]) + confirm_email = StringField('Confirm Email', [wtf_validators.email(), wtf_validators.equal_to('email')]) + phone_number = StringField('Phone Number', [wtf_validators.required(), validators.phone_number()]) + password = PasswordField('Password', [wtf_validators.required(), wtf_validators.length(min=8, message='Password MUST be at least 8 characters long.')]) + confirm_pass = PasswordField('Confirm Password', [wtf_validators.required(), wtf_validators.equal_to('password', message='Passwords are not equal.')]) + first_name = StringField('First Name', [wtf_validators.required()]) + last_name = StringField('Last Name', [wtf_validators.required()]) + date_of_birth = StringField('Date of Birth', [validators.date_of_birth()]) + is_applicant = BooleanField('Applying to be a homeowner?'), + + @staticmethod + def validate_user_id(form, field): + user = User.objects(user_id=field.data).first() + if user: + raise ValidationError('The given username is taken, please pick a new one') + + @staticmethod + def validate_email(form, field): + user = User.objects(email=field.data).first() + if user: + raise ValidationError('The given email is already in use. Please choose a different one.') + + @property + def get_date_of_birth(self): + return datetime.strptime(self.date_of_birth.raw_data[0], '%b %d, %Y') + + +class SendConfirmEmailFrom(Form): + """A form for sending a confirmation email to activate a user account.""" + email = StringField('Email', [wtf_validators.required(), wtf_validators.email()]) + """The email address for the user account to activate.""" diff --git a/app/auth/model.py b/app/auth/model.py new file mode 100644 index 0000000..2e59d3a --- /dev/null +++ b/app/auth/model.py @@ -0,0 +1,180 @@ +import jwt +from werkzeug.security import generate_password_hash, check_password_hash +from time import time +from datetime import datetime +from uuid import uuid4 +from mongoengine.fields import StringField +from app.database import db, DocumentBase, EmbeddedDocumentBase +from app.database.fields import QuarterDateField +from app.util.datatypes import enum +from app_common.const import USER_ID_REGEX + + +class UserIDField(StringField): + def validate(self, value): + value = value.lower() + if not USER_ID_REGEX.fullmatch(value): + self.error(f'The given user ID "{value}" is invalid, can only contain alphanumeric characters and "_, -".') + if value in Roles: + self.error(f'A user ID cannot be a user role (invalid values: {Roles})') + + +class Roles(enum.BaseEnum): + APPLICANT = 'applicant' + OCCUPANT = 'occupant' + INVESTOR = 'investor' + IMPACT_INVESTOR = 'impact_investor' + NETWORK_ADMIN = 'network_admin' + ADMIN = 'admin' + + +class BlackListedJWT(DocumentBase): + """A mongo document to represent expired JWT access tokens.""" + jti = db.StringField(required=True) + created_at = db.DateTimeField(required=True) + + @staticmethod + def blacklist_jwt(jti): + to_blacklist = BlackListedJWT(jti=jti, created_at=datetime.utcnow()) + to_blacklist.save() + + @staticmethod + def is_blacklisted(jti): + return BlackListedJWT.objects(jti=jti).first() is not None + + +class UserPrefs(EmbeddedDocumentBase): + data = db.DictField(required=False) + + +class User(DocumentBase): + """A mongo document to represent users.""" + user_id = UserIDField(required=True, unique=True) + """The user's ID (unique, required)""" + email = db.EmailField(required=True, unique=True) + """The user's email (unique, required).""" + password_hash = db.StringField(required=False) + """The hashed password for the user.""" + first_name = db.StringField(required=True) + """The user's first name (required)""" + middle_init_or_name = db.StringField(required=False, default='') + """The middle initial or middle name for the user.""" + last_name = db.StringField(required=True) + """The user's last name (required).""" + last_login = db.DateTimeField(required=False) + """The last time the user logged in.""" + address1 = db.StringField(required=False) + address2 = db.StringField(required=False) + city = db.StringField(required=False) + state = db.StringField(required=False) + zip = db.StringField(required=False) + """The user's address.""" + join_date = QuarterDateField(required=True) + """THe date the user joined (required).""" + date_of_birth = QuarterDateField(required=False) + """The user's date of birth.""" + phone_number = db.StringField(requied=False, sparse=True) + """The user's phone number (unique but not required).""" + roles = db.ListField(db.EnumField(Roles), default=[Roles.APPLICANT]) + """The roles the user has.""" + active = db.BooleanField(required=True, default=False) + """Weather or not the user account is active""" + prefs = db.EmbeddedDocumentField('UserPrefs') + misc_data = db.DictField(required=False) + + @classmethod + def ignore_to_json(cls): + result = super().ignore_to_json() + result.append('password_hash') + result.append('active') + return result + + @classmethod + def from_request_args(cls, **kwargs): + password = kwargs.pop('password') + kwargs['join_date'] = datetime.utcnow().date() + if isinstance(kwargs.get('roles'), str): + kwargs['roles'] = kwargs['roles'].split(',') + user = User(**kwargs) + user.set_password(password) + user.save() + + def set_password(self, password): + """Hashes and stores the password for the user. + + Arguments: + password (`str`): The password to store the hash of for the user. + """ + self.password_hash = generate_password_hash(password, salt_length=15) + + def check_password(self, password): + """Checks if the password is correct for the user. + + Arguments: + password (`str`): The password to check against the user's stored password hash. + + Returns: + `bool`: `True` if the password is correct, `False` otherwise. + """ + return check_password_hash(self.password_hash, password) + + def get_password_reset_token(self, expire_secs=600, secret_key=None): + """Generates a JWT token allowing the user to reset their password that will expire after the given time. + + Arguments: + expire_secs (`int`): The number of seconds the password reset token should be valid for. + secret_key (`str`): The secret key to use when generating the JWT token. + + Returns: + `str`: A JWT token which allows the user to reset their password. + """ + payload = { + 'exp': time() + expire_secs, + 'reset_password': self.user_id, + 'jti': str(uuid4()) + } + return jwt.encode(payload, secret_key, algorithm='HS256') + + def get_activation_token(self, expire_secs=1200, secret_key=None, redirect_url=''): + payload = { + 'exp': time() + expire_secs, + 'activate': self.user_id, + 'redirect_url': redirect_url + } + return jwt.encode(payload, secret_key, algorithm='HS256') + + @staticmethod + def from_activation_token(token, secret_key=None): + try: + jwt_data = jwt.decode(token, secret_key, algorithms='HS256') + user_id = jwt_data['activate'] + redirect_url = jwt_data['redirect_url'] + except jwt.ExpiredSignatureError: + return None + return User.objects(user_id=user_id).first(), redirect_url + + @staticmethod + def from_password_reset_token(token, secret_key=None): + """Gets a `User` from the given JWT token that allows them to reset their password. + + Arguments: + token (`str`): The encoded JWT token to load the user from. + secret_key (`str`): The secret key used to generate the JWT token. + + Returns: + `User|None`: The user object encoded if the token is still valid, `None` if the token has expired. + """ + try: + jwt_data = jwt.decode(token, secret_key, algorithms=['HS256']) + # Has this token been blacklisted? + if BlackListedJWT.is_blacklisted(jwt_data['jti']): + return None + # get the user id + user_id = jwt_data['reset_password'] + # black list this token after it's been used + BlackListedJWT.blacklist_jwt(jwt_data['jti']) + except jwt.ExpiredSignatureError: + return None + # get the user + return User.objects(user_id=user_id).first() + diff --git a/app/auth/oauth.py b/app/auth/oauth.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/auth/oauth.py @@ -0,0 +1 @@ + diff --git a/app/auth/request_parsers.py b/app/auth/request_parsers.py new file mode 100644 index 0000000..807a123 --- /dev/null +++ b/app/auth/request_parsers.py @@ -0,0 +1,76 @@ +from flask_restx import inputs +from .model import Roles +from app_common.parser import QuarterRequestParser as RequestParser +from app_common.inputs import quarter_date_time, email, user_id + + +def roles(value): + if isinstance(value, str): + value = value.split(',') + + def invalid_role_filter(v): + return v not in Roles + + if isinstance(value, list): + invalid = list(filter(invalid_role_filter, value)) + if invalid: + raise ValueError(f'The given role(s) are invalid: {invalid}') + + return value + + +post_user_login_schema = RequestParser() +post_user_login_schema.add_argument('user_id', required=True, type=user_id(), help='The ID of the user to log in.') +post_user_login_schema.add_argument('password', required=True, type=str, help='The password to use when logging the user in.') +post_user_login_schema.add_argument('remember', required=False, type=inputs.boolean, default=False, + help='If a refresh token should be included when logging in.') + + +def _get_create_user_parser(require_user_id=True): + result = RequestParser() + result.add_argument('user_id', required=require_user_id, type=user_id(), help='The user ID to create an account for.') + result.add_argument('password', required=True, type=str, help='The password to create for the user account.') + result.add_argument('email', required=True, type=email(), help='The user\'s email address.') + result.add_argument('first_name', required=True, type=str, help='The user\'s first name.') + result.add_argument('middle_init_or_name', required=False, type=str, help='The user\'s middle intital or middle name') + result.add_argument('last_name', required=True, type=str, help='The user\'s last name.') + result.add_argument('date_of_birth', required=False, type=quarter_date_time(), help='The user\'s date of birth (expected format: %b %d, %Y ex: "Apr 17, 1993").') + result.add_argument('phone_number', required=False, type=str, help='The user\'s phone number.') + result.add_argument('roles', required=False, type=roles, default=str(Roles.APPLICANT), help=f'A comma-separated list of the user\'s roles (valid roles are: {Roles.values()}).') + return result + + +create_user_parser = _get_create_user_parser() + +admin_crate_user_parser = _get_create_user_parser(False) + +user_id_parser = RequestParser() +user_id_parser.add_argument('user_id', required=True, type=user_id(), help='The user ID of the user to operate this request on.') + +edit_user_parser = RequestParser() +edit_user_parser.add_argument('email', required=False, type=email(), help='The new email address to set for the user.') +edit_user_parser.add_argument('first_name', required=False, type=str, help='The new first name for the user.') +edit_user_parser.add_argument('middle_init_or_name', required=False, type=str, help='The new middle initial or middle name for the user.') +edit_user_parser.add_argument('last_name', required=False, type=str, help='The new last name for the user.') +edit_user_parser.add_argument('date_of_birth', required=False, type=quarter_date_time(), help='The new date of birth for the user (expected format: %b %d, %Y ex: "Apr 17, 1993").') +edit_user_parser.add_argument('phone_number', required=False, type=str, help='The new phone number for the user.') +create_user_parser.add_argument('roles', required=False, type=roles, default=str(Roles.APPLICANT), + help=f'A comma-separated list of the user\'s roles (valid roles are: {Roles.values()}).') + +update_password_parser = RequestParser() +update_password_parser.add_argument('old_password', required=True) +update_password_parser.add_argument('new_password', required=True) + +reset_password_parser = RequestParser() +reset_password_parser.add_argument('new_password', required=True, type=str, help='The new password to set for the user.') +reset_password_parser.add_argument('token', required=True, type=str, help='The JWT password reset token used to allow a password to be reset.') + +activate_account_parser = RequestParser() +activate_account_parser.add_argument('token', required=True, type=str, help='The JWT account activation token used to activate the account.') + +resend_activation_email = RequestParser() +resend_activation_email.add_argument('email', required=True, type=email(), help='The email address to resend the account activation link to.') + +request_password_reset_parser = RequestParser() +request_password_reset_parser.add_argument('email', required=False, type=email(), help='The email address of the user to reset the password for.') +request_password_reset_parser.add_argument('user_id', required=False, type=user_id(), help='The User ID of the user to reset the password for.') diff --git a/app/auth/routes.py b/app/auth/routes.py new file mode 100644 index 0000000..c61729a --- /dev/null +++ b/app/auth/routes.py @@ -0,0 +1,126 @@ +from flask import request, url_for, render_template, redirect, flash, jsonify, make_response, current_app +from flask_jwt_extended import current_user, jwt_required, create_access_token, get_jwt, create_refresh_token, set_access_cookies, unset_jwt_cookies +from datetime import datetime +from . import blueprint +from .model import User, BlackListedJWT +from .forms import LoginForm, RegisterForm, RequestPasswordResetForm, ResetPasswordForm, SendConfirmEmailFrom +from app.auth.email import send_password_reset_email + + +@blueprint.route('/login', methods=['GET', 'POST']) +@jwt_required(optional=True, locations=['cookies']) +def login(): + if current_user: + flash(f'Already logged in as {current_user.user_id}. Incorrect? logout') + + form = LoginForm(request.form) + if request.method == 'POST': + if form.validate(): + user = User.objects(user_id=form.user_id.data).first() + auth_token = create_access_token(user) + user.last_login = datetime.utcnow() + user.save() + response = make_response(jsonify(redirect=url_for('main.index'))) + set_access_cookies(response, auth_token) + return response + + return render_template('auth/login_modal.html', form=form) + + +@blueprint.route('/register', methods=['POST', 'GET']) +@jwt_required(optional=True) +def register(): + if current_user: + flash(f'Already logged in as {current_user.user_id}. Incorrect? logout') + return redirect(url_for('main.index')) + + form = RegisterForm(request.form) + + if request.method == 'POST': + if form.validate(): + user = User(user_id=form.user_id.data, + email=form.email.data, + first_name=form.first_name.data, + last_name=form.last_name.data, + last_login=datetime.now(), + join_date=datetime.now().date(), + date_of_birth=form.get_date_of_birth, + phone_number=form.phone_number.data, + type='Occupant') + user.set_password(form.password.data) + user.last_login = datetime.utcnow() + user.save() + auth_token = create_access_token(user) + response = make_response(jsonify(redirect=url_for('main.index'))) + set_access_cookies(response, auth_token) + return response + + return render_template('auth/register.html', form=form) + + +@blueprint.route('/logout') +@jwt_required(locations=['cookies']) +def logout(): + to_blacklist = BlackListedJWT(jti=get_jwt()['jti'], created_at=datetime.utcnow()) + to_blacklist.save() + + response = redirect(url_for('main.index')) + flash('Logged out successfully!') + unset_jwt_cookies(response) + return redirect(url_for('main.index')) + + +@blueprint.route('/request_password_reset', methods=['GET', 'POST']) +@jwt_required(optional=True) +def request_password_reset(): + if current_user: + flash(f'You are already logged in as {current_user.user_id}. Incorrect? logout') + return redirect(url_for('main.index')) + + form = RequestPasswordResetForm(request.form) + + if request.method == 'POST': + if form.validate(): + if form.user_id.data: + user = User.objects(user_id=form.user_id.data).first() + else: + user = User.objects(email=form.email.data).first() + send_password_reset_email(user) + flash('Check your email for the password reset link') + return jsonify(redirect=url_for('main.index')) + + return render_template('auth/request_password_reset.html', form=form) + + +@blueprint.route('/reset_password/', methods=['GET', 'POST']) +def reset_password(token): + if current_user: + flash(f'You are already logged in as {current_user.user_id}. Incorrect? logout') + return redirect(url_for('main.index')) + + user = User.from_password_reset_token(token, secret_key=current_app.config['SECRET_KEY']) + if not user: + flash(f'The password reset link may have expired. Please reset your password again.') + return redirect(url_for('main.index')) + form = ResetPasswordForm(request.form) + + if request.method == 'POST': + if form.validate(): + flash('Password reset successfully!') + user.set_password(form.password.data) + return redirect(url_for('main.index')) + + return render_template('auth/reset_password.html', form=form) + + +@blueprint.route('/account') +@jwt_required() +def account(): + return render_template('auth/account.html', current_user=current_user) + + +@blueprint.route('/confirm_email/') +def confirm_email(token): + form = SendConfirmEmailFrom() + user, redirect_url = User.from_activation_token(token, secret_key=current_app.config['SECRET_KEY']) + return render_template('auth/email_confirmation.html', token=token, form=form, redirect_url=redirect_url) diff --git a/app/auth/util.py b/app/auth/util.py new file mode 100644 index 0000000..1c9bdf6 --- /dev/null +++ b/app/auth/util.py @@ -0,0 +1,6 @@ +import os +from base64 import b64encode + + +def secure_random_string(num_bytes=24): + return b64encode(os.urandom(num_bytes)).decode('utf-8') diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..45693f8 --- /dev/null +++ b/app/config.py @@ -0,0 +1,64 @@ +import logging +import datetime +import os + +# General app configuration options +APP_NAME = 'QUARTER_WEB' +VERSION = '0.1.0' +LOCAL_HOSTNAME = 'localhost' +# TODO: This should be retrieved from the AWS instance name +PUBLIC_HOSTNAME = 'localhost' +SECRET_KEY = os.environ.get('SECRET_KEY', 'ThisIsASecret') + +# Mongo configuration options +MONGODB_DB = os.environ.get('MONGODB_DATABASE', 'quarter') +MONGODB_HOST = os.environ.get('MONGODB_HOST', 'quarter_mongodb') +MONGODB_PORT = int(os.environ.get('MONGODB_PORT', 27017)) +MONGODB_USERNAME = os.environ.get('MONGODB_USERNAME', 'quarter') +MONGODB_PASSWORD = os.environ.get('MONGODB_PASSWORD', 'quarter').replace('"', '') +MONGODB_CONNECT = False + +# Flask-Static-Digest config +FLASK_STATIC_DIGEST_URL = 'apply/' + +# JWT configuration options +JWT_SECRET_KEY = SECRET_KEY +JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(hours=1) +JWT_REFRESH_TOKEN_EXPIRES = datetime.timedelta(days=1) +JWT_TOKEN_LOCATION = ['headers', 'cookies'] +JWT_HEADER_NAME = 'Auth-Token' +JWT_COOKIE_CSRF_PROTECT = False +JWT_CLEANUP_MULE_SLEEP_SECS = datetime.timedelta(minutes=30).seconds + +# CORS configuration options +CORS_SUPPORTS_CREDENTIALS = True +CORS_ORIGINS = [ + 'http://127.0.0.1:5000', + 'http://127.0.0.1', + 'http://localhost:5000', + 'http://localhost', + 'https://www.apply.quarterhomes.com', + 'https://apply.qaurterhomes.com', + 'http://apply.qaurterhomes.com', + 'http://www.quarterhomes.com', + 'https://www.quarterhomes.com', + 'http://qhome2.strassner.com' +] +CORS_RESOURCES = r'/api/*' + +# Swagger config options +SWAGGER_UI_DOC_EXPANSION = 'list' +RESTX_MASK_SWAGGER = False + +# logging configuration options +LOG_LEVEL = logging.INFO +LOG_PATH = os.path.join('/var/log/www/quarter.log') + +# Mail configuration options +MAIL_USERNAME = os.environ.get('MAIL_USERNAME', 'noreply@quarterhomes.com') + +# Password reset config options +PASSWORD_RESET_EXP_SEC = datetime.timedelta(minutes=10).seconds + +# Email confirmation options +CONFIRM_EMAIL_EXP_SEC = datetime.timedelta(minutes=15).seconds diff --git a/app/cors.py b/app/cors.py new file mode 100644 index 0000000..e716230 --- /dev/null +++ b/app/cors.py @@ -0,0 +1,3 @@ +from flask_cors import CORS + +cors = CORS() diff --git a/app/database/__init__.py b/app/database/__init__.py new file mode 100644 index 0000000..3f9c863 --- /dev/null +++ b/app/database/__init__.py @@ -0,0 +1,77 @@ +import json +from bson import json_util +from flask_mongoengine import MongoEngine +from mongoengine.document import BaseDocument +from mongoengine.fields import ListField, EmbeddedDocumentField +from app.util import dict_to_camel, dict_to_snake, camel_to_snake + +db = MongoEngine() + + +class QuarterDocumentBase(BaseDocument): + meta = {'abstract': True} + + @classmethod + def ignore_to_json(cls): + return ['_id', '_cls'] + + @classmethod + def ignore_from_json(cls): + return ['_cls'] + + @classmethod + def json_mapped_fields(cls): + return {} + + @classmethod + def additional_fields(cls): + return [] + + def to_dict(self): + result = json_util.loads(self.to_json()) + for ignore in self.ignore_to_json(): + result.pop(ignore, None) + for additional in self.additional_fields(): + result[additional] = getattr(self, additional, None) + + for prop_name in result: + if hasattr(getattr(self, prop_name, None), 'to_dict'): + result[prop_name] = getattr(self, prop_name).to_dict() + elif isinstance(getattr(self, prop_name, None), list): + result[prop_name] = [k.to_dict() if hasattr(k, 'to_dict') else k for k in getattr(self, prop_name)] + + for json_name, prop_name in self.json_mapped_fields().items(): + result[json_name] = result.pop(prop_name, None) + + return result + + def to_camel_case(self): + return dict_to_camel(self.to_dict()) + + @classmethod + def from_camel_case(cls, json_data): + if isinstance(json_data, str): + json_data = json_util.loads(json_data) + + kwargs = dict_to_snake(json_data) + + result = cls._from_son(kwargs) + return result + + @classmethod + def _from_son(cls, son, _auto_dereference=True, created=False): + for ignore in cls.ignore_from_json(): + son.pop(ignore, None) + + for json_name, prop_name in cls.json_mapped_fields().items(): + son[prop_name] = son.pop(json_name, None) + + return super()._from_son(son, _auto_dereference=_auto_dereference, created=created) + + +class DocumentBase(db.Document, QuarterDocumentBase): + meta = {'abstract': True} + + +class EmbeddedDocumentBase(db.EmbeddedDocument, QuarterDocumentBase): + meta = {'abstract': True} diff --git a/app/database/fields.py b/app/database/fields.py new file mode 100644 index 0000000..7aa09ae --- /dev/null +++ b/app/database/fields.py @@ -0,0 +1,60 @@ +from mongoengine.fields import DateField +from datetime import datetime + + +DEFAULT_DATE_FORMAT = '%b %d, %Y' + + +class QuarterDateField(DateField): + def __init__(self, *args, date_format='%b %d, %Y', **kwargs): + self.date_format = date_format + super().__init__(*args, **kwargs) + + def validate(self, value): + if isinstance(value, str): + try: + datetime.strptime(value, self.date_format) + except Exception as ex: + self.error(f'Date time is not the correct format: (expected {self.date_format}). Error: {str(ex)}') + + def to_mongo(self, value): + value = super().to_mongo(value) + if isinstance(value, datetime): + value = value.strftime(self.date_format) + return value + + def to_python(self, value): + value = super().to_python(value) + if isinstance(value, datetime): + value = value.strftime(self.date_format) + return value + + def get_dt_format(self): + return self.date_format + + @staticmethod + def make_datetime(value, dt_format='%b %d, %Y'): + if isinstance(value, str): + return datetime.strptime(value, dt_format) + return value + + +class QuarterYearField(QuarterDateField): + def __init__(self, *args, **kwargs): + kwargs['date_format'] = '%Y' + super().__init__(*args, **kwargs) + + def validate(self, value): + if isinstance(value, int): + value = str(value) + return super().validate(value) + + def to_mongo(self, value): + if isinstance(value, int): + value = str(value) + return super().to_mongo(value) + + def to_python(self, value): + if isinstance(value, int): + value = str(value) + return super().to_python(value) diff --git a/app/database/utils.py b/app/database/utils.py new file mode 100644 index 0000000..8e4e43b --- /dev/null +++ b/app/database/utils.py @@ -0,0 +1,56 @@ +# from mongoengine import fields +# from app.util.schema import load_json_schema +# from . import DocumentBase + +# +# def obj_from_schema(schema_file, base_classes=(DocumentBase, )): +# schema = load_json_schema(schema_file) +# type = schema['type'] +# title = schema['title'] +# required = schema.get('required', []) +# unique = schema.get('unique', []) +# +# obj_fields = {} +# +# def load_properties(obj_schema): +# for name, _schema in obj_schema['properties']: +# _required = name in required +# _unique = name in unique +# _sparse = not _required and _unique +# field_class = _get_field_class(_schema) +# +# # is this a list field? +# if field_class == fields.ListField: +# list_class = _get_field_class(_schema['items']) +# # is this an embedded document field? +# elif field_class == fields.EmbeddedDocumentField: +# +# +# print(schema) +# +# +# def _get_field_class(field_schema): +# _type = field_schema.get('type') +# if _type == 'string': +# str_format = field_schema.get('format', '').lower() +# if str_format == 'date': +# return fields.DateField +# if str_format == 'datetime': +# return fields.DateTimeField +# elif str_format == 'email': +# return fields.EmailField +# elif str_format == 'multiline': +# return fields.MultiLineStringField +# return fields.StringField +# +# elif _type == 'float': +# return fields.FloatField +# elif _type == 'integer': +# return fields.IntField +# elif _type == 'boolean': +# return fields.BooleanField +# elif _type == 'array': +# return fields.ListField +# elif _type == 'object': +# return fields.EmbeddedDocumentField + diff --git a/app/docker-entrypoint.sh b/app/docker-entrypoint.sh new file mode 100644 index 0000000..f0eb8c4 --- /dev/null +++ b/app/docker-entrypoint.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +uwsgi --ini /var/www/docker/quarter-web.ini --touch-reload /var/www/uwsgi.reload >> /var/log/www/quarter.log 2>&1 diff --git a/app/email/__init__.py b/app/email/__init__.py new file mode 100644 index 0000000..c7cd5ef --- /dev/null +++ b/app/email/__init__.py @@ -0,0 +1,43 @@ +from __future__ import print_function +from flask import current_app +from googleapiclient.discovery import build +from apiclient import errors +import os +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import base64 +from google.oauth2 import service_account + + +def login(): + scopes = ['https://www.googleapis.com/auth/gmail.send'] + service_account_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'service-key.json')) + creds = service_account.Credentials.from_service_account_file(service_account_file, scopes=scopes) + delegated_creds = creds.with_subject(current_app.config['MAIL_USERNAME']) + service = build('gmail', 'v1', credentials=delegated_creds) + return service + + +def create_message(subject, sender, recipient, txt_body, html_body): + message = MIMEMultipart('alternative') + message['To'] = recipient + message['From'] = sender + message['Subject'] = subject + message.attach(MIMEText(txt_body, 'plain')) + message.attach(MIMEText(html_body, 'html')) + return {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')} + + +def send_email(subject, sender, recipient, txt_body, html_body): + service = login() + message = create_message(subject, sender, recipient, txt_body, html_body) + + try: + msg = (service.users().messages().send(userId='me', body=message).execute()) + print(f'Message ID: {msg["id"]}') + return msg + except errors.HttpError as error: + print(f'Failed to send message: {str(error)}') + + + diff --git a/app/email/service-key.json b/app/email/service-key.json new file mode 100644 index 0000000..4886a56 Binary files /dev/null and b/app/email/service-key.json differ diff --git a/app/model.py b/app/model.py new file mode 100644 index 0000000..b969677 --- /dev/null +++ b/app/model.py @@ -0,0 +1,3 @@ +from .database import db +from .auth.model import User, Roles, BlackListedJWT +from .applicant.model import Application, Applicant, Property, LifeInsurance, Assets, Job diff --git a/app/mules/JWTCleanMule.py b/app/mules/JWTCleanMule.py new file mode 100644 index 0000000..2322ed0 --- /dev/null +++ b/app/mules/JWTCleanMule.py @@ -0,0 +1,28 @@ +from app import create_app +from app import model +from datetime import datetime +from app.mules.mule import MuleBase + + +class JWTCleanupMule(MuleBase): + def __init__(self): + app = create_app() + self.jwt_exp_time = app.config.get('JWT_ACCESS_TOKEN_EXPIRES') + super().__init__('JWTCleanupMule', app.logger, app.config.get('JWT_CLEANUP_MULE_SLEEP_SECS', 10)) + + def loop(self): + oldest_allowed = datetime.utcnow() - self.jwt_exp_time + self.logger.info(f'Searching for JWT access tokens revoked at or before {oldest_allowed.strftime("%Y %m %d %H:%M:%S")}...') + to_remove = model.BlackListedJWT.objects(created_at__lt=oldest_allowed).all() + self.logger.info(f'Found {len(to_remove)} expired JWT access tokens to remove') + for idx, jwt in enumerate(to_remove): + self.logger.debug(f'Removing expired JWT {idx + 1}/{len(to_remove)}') + try: + jwt.delete() + except Exception as ex: + self.logger.error(f'Failed to remove JWT (exception={str(ex)}).') + + +if __name__ == '__main__': + mule = JWTCleanupMule() + mule() diff --git a/app/mules/mule.py b/app/mules/mule.py new file mode 100644 index 0000000..7a7bb96 --- /dev/null +++ b/app/mules/mule.py @@ -0,0 +1,24 @@ +from time import sleep +import traceback +from abc import abstractmethod, ABC + + +class MuleBase(ABC): + def __init__(self, mule_name, logger, sleep_secs): + self.name = mule_name + self.logger = logger + self.sleep_secs = sleep_secs + + @abstractmethod + def loop(self): + pass + + def __call__(self): + while True: + try: + self.loop() + self.logger.info(f'[{self.name}] Completed work, sleeping (sleep_secs={self.sleep_secs}).') + except Exception as ex: + self.logger.error(f'[{self.name}] Task failed, sleeping before restarting (sleep_secs={self.sleep_secs}, exception={str(ex)}).\n{traceback.format_exc()}') + finally: + sleep(self.sleep_secs) diff --git a/app/network_admin/__init__.py b/app/network_admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/network_admin/api.py b/app/network_admin/api.py new file mode 100644 index 0000000..2ca6282 --- /dev/null +++ b/app/network_admin/api.py @@ -0,0 +1,108 @@ +from app.auth import role_required, NO_USER_FOUND_MESSAGE +from app import model +from app_common.const import BAD_USER_ROLES +from app_common.parser import QuarterRequestParser +from app.auth.request_parsers import edit_user_parser, admin_crate_user_parser, roles +from flask_restx import Namespace, Resource, abort, inputs +from flask import jsonify, make_response + +namespace = Namespace('network_admin', description='API endpoints available for network admins', decorators=[role_required([model.Roles.ADMIN, model.Roles.NETWORK_ADMIN])]) + + +@namespace.route('/ajax/user/') +@namespace.response(404, NO_USER_FOUND_MESSAGE) +class User(Resource): + + @namespace.doc(description='Gets the data for a user.') + def get(self, user_id): + user = model.User.objects(user_id=user_id).first_or_404() + return make_response(jsonify(user.to_dict())) + + @namespace.expect(admin_crate_user_parser) + @namespace.doc(description='Crates a new user.') + def put(self, user_id): + args = admin_crate_user_parser.parse_args() + args.user_id = user_id + model.User.from_request_args(**dict(args)) + + @namespace.expect(edit_user_parser) + @namespace.doc(description='Update the data for a user.') + def post(self, user_id): + args = edit_user_parser.parse_args() + args.user_id = user_id + user = model.User.objects(user_id=args.user_id).first_or_404() + # try to update the user + for key in dict(args): + value = getattr(args, key) + if value: + setattr(user, key, value) + user.save() + + @namespace.doc(description='Deletes a user.') + def delete(self, user_id): + user = model.User.objects(user_id=user_id).first_or_404() + user.delete() + + # see if there is an application to delete as well... + application = model.Application.objects(user_id=user_id).first() + if application: + application.delete() + + +get_users_parser = QuarterRequestParser() +get_users_parser.add_argument('roles', required=False, type=roles, help='A comma separated list of user roles to return.', default=None) + + +@namespace.route('/users') +class GetUsers(Resource): + @namespace.expect(get_users_parser) + def get(self): + args = get_users_parser.parse_args() + if args.roles: + users = model.User.objects(roles__in=args.roles).all() + else: + users = model.User.objects() + return jsonify([u.to_dict() for u in users]) + + +@namespace.route('/ajax/user//application') +class UserApplication(Resource): + @namespace.response(404, 'No user was found or no application was found for the given user') + def get(self, user_id): + user = model.User.objects(user_id=user_id).first_or_404(message='No user found') + application = model.Application.objects(applicants__match={'email': user.email}).first_or_404(message='No application found for user.') + return make_response(jsonify(user=user.to_dict(), application=application.to_dict())) + + +user_roles_parser = QuarterRequestParser() +user_roles_parser.add_argument('roles', required=True, default='', type=roles, help='A comma separated list of roles to assign to/delete from a user') +user_roles_parser.add_argument('force_match', required=False, default=False, type=inputs.boolean, help='Weather or not to force a users roles to exactly match what is given.') + + +@namespace.route('/ajax/user//roles') +class UserRoles(Resource): + @namespace.response(404, 'No user found with the given ID') + def get(self, user_id): + user = model.User.objects(user_id=user_id).first_or_404(message='No user found') + return jsonify(user.roles) + + @namespace.expect(user_roles_parser) + @namespace.response(404, 'No user found with the given ID') + def post(self, user_id): + args = user_roles_parser.parse_args() + user = model.User.objects(user_id=user_id).first_or_404(message='No user found') + # if we are supposed to force the user roles to match what is given, clear out the existing roles + if args.force_match: + user.roles = [] + # add the new roles + user.roles.extend(args.roles) + user.save() + + @namespace.expect(user_roles_parser) + @namespace.response(404, 'No user found with the given ID') + def delete(self, user_id): + args = user_roles_parser.parse_args() + user = model.User.objects(user_id=user_id).first_or_404(message='No user found') + user.roles = [r for r in user.roles if r not in args.roles] + user.save() + diff --git a/app/network_admin/views.py b/app/network_admin/views.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..0aafbcf --- /dev/null +++ b/app/routes.py @@ -0,0 +1,18 @@ +from flask import Blueprint, render_template +from flask_jwt_extended import current_user, jwt_required, get_jwt_identity + +blueprint = Blueprint('main', __name__) + + +@blueprint.route('/') +@blueprint.route('/index') +@jwt_required(optional=True) +def index(): + try: + user_id = get_jwt_identity() + except: + user_id = None + + if current_user: + user_id = current_user.user_id + return render_template('index.html', user_id=user_id) diff --git a/app/static/css/materialize.min.css b/app/static/css/materialize.min.css new file mode 100644 index 0000000..74b1741 --- /dev/null +++ b/app/static/css/materialize.min.css @@ -0,0 +1,13 @@ +/*! + * Materialize v1.0.0 (http://materializecss.com) + * Copyright 2014-2017 Materialize + * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) + */ +.materialize-red{background-color:#e51c23 !important}.materialize-red-text{color:#e51c23 !important}.materialize-red.lighten-5{background-color:#fdeaeb !important}.materialize-red-text.text-lighten-5{color:#fdeaeb !important}.materialize-red.lighten-4{background-color:#f8c1c3 !important}.materialize-red-text.text-lighten-4{color:#f8c1c3 !important}.materialize-red.lighten-3{background-color:#f3989b !important}.materialize-red-text.text-lighten-3{color:#f3989b !important}.materialize-red.lighten-2{background-color:#ee6e73 !important}.materialize-red-text.text-lighten-2{color:#ee6e73 !important}.materialize-red.lighten-1{background-color:#ea454b !important}.materialize-red-text.text-lighten-1{color:#ea454b !important}.materialize-red.darken-1{background-color:#d0181e !important}.materialize-red-text.text-darken-1{color:#d0181e !important}.materialize-red.darken-2{background-color:#b9151b !important}.materialize-red-text.text-darken-2{color:#b9151b !important}.materialize-red.darken-3{background-color:#a21318 !important}.materialize-red-text.text-darken-3{color:#a21318 !important}.materialize-red.darken-4{background-color:#8b1014 !important}.materialize-red-text.text-darken-4{color:#8b1014 !important}.red{background-color:#F44336 !important}.red-text{color:#F44336 !important}.red.lighten-5{background-color:#FFEBEE !important}.red-text.text-lighten-5{color:#FFEBEE !important}.red.lighten-4{background-color:#FFCDD2 !important}.red-text.text-lighten-4{color:#FFCDD2 !important}.red.lighten-3{background-color:#EF9A9A !important}.red-text.text-lighten-3{color:#EF9A9A !important}.red.lighten-2{background-color:#E57373 !important}.red-text.text-lighten-2{color:#E57373 !important}.red.lighten-1{background-color:#EF5350 !important}.red-text.text-lighten-1{color:#EF5350 !important}.red.darken-1{background-color:#E53935 !important}.red-text.text-darken-1{color:#E53935 !important}.red.darken-2{background-color:#D32F2F !important}.red-text.text-darken-2{color:#D32F2F !important}.red.darken-3{background-color:#C62828 !important}.red-text.text-darken-3{color:#C62828 !important}.red.darken-4{background-color:#B71C1C !important}.red-text.text-darken-4{color:#B71C1C !important}.red.accent-1{background-color:#FF8A80 !important}.red-text.text-accent-1{color:#FF8A80 !important}.red.accent-2{background-color:#FF5252 !important}.red-text.text-accent-2{color:#FF5252 !important}.red.accent-3{background-color:#FF1744 !important}.red-text.text-accent-3{color:#FF1744 !important}.red.accent-4{background-color:#D50000 !important}.red-text.text-accent-4{color:#D50000 !important}.pink{background-color:#e91e63 !important}.pink-text{color:#e91e63 !important}.pink.lighten-5{background-color:#fce4ec !important}.pink-text.text-lighten-5{color:#fce4ec !important}.pink.lighten-4{background-color:#f8bbd0 !important}.pink-text.text-lighten-4{color:#f8bbd0 !important}.pink.lighten-3{background-color:#f48fb1 !important}.pink-text.text-lighten-3{color:#f48fb1 !important}.pink.lighten-2{background-color:#f06292 !important}.pink-text.text-lighten-2{color:#f06292 !important}.pink.lighten-1{background-color:#ec407a !important}.pink-text.text-lighten-1{color:#ec407a !important}.pink.darken-1{background-color:#d81b60 !important}.pink-text.text-darken-1{color:#d81b60 !important}.pink.darken-2{background-color:#c2185b !important}.pink-text.text-darken-2{color:#c2185b !important}.pink.darken-3{background-color:#ad1457 !important}.pink-text.text-darken-3{color:#ad1457 !important}.pink.darken-4{background-color:#880e4f !important}.pink-text.text-darken-4{color:#880e4f !important}.pink.accent-1{background-color:#ff80ab !important}.pink-text.text-accent-1{color:#ff80ab !important}.pink.accent-2{background-color:#ff4081 !important}.pink-text.text-accent-2{color:#ff4081 !important}.pink.accent-3{background-color:#f50057 !important}.pink-text.text-accent-3{color:#f50057 !important}.pink.accent-4{background-color:#c51162 !important}.pink-text.text-accent-4{color:#c51162 !important}.purple{background-color:#9c27b0 !important}.purple-text{color:#9c27b0 !important}.purple.lighten-5{background-color:#f3e5f5 !important}.purple-text.text-lighten-5{color:#f3e5f5 !important}.purple.lighten-4{background-color:#e1bee7 !important}.purple-text.text-lighten-4{color:#e1bee7 !important}.purple.lighten-3{background-color:#ce93d8 !important}.purple-text.text-lighten-3{color:#ce93d8 !important}.purple.lighten-2{background-color:#ba68c8 !important}.purple-text.text-lighten-2{color:#ba68c8 !important}.purple.lighten-1{background-color:#ab47bc !important}.purple-text.text-lighten-1{color:#ab47bc !important}.purple.darken-1{background-color:#8e24aa !important}.purple-text.text-darken-1{color:#8e24aa !important}.purple.darken-2{background-color:#7b1fa2 !important}.purple-text.text-darken-2{color:#7b1fa2 !important}.purple.darken-3{background-color:#6a1b9a !important}.purple-text.text-darken-3{color:#6a1b9a !important}.purple.darken-4{background-color:#4a148c !important}.purple-text.text-darken-4{color:#4a148c !important}.purple.accent-1{background-color:#ea80fc !important}.purple-text.text-accent-1{color:#ea80fc !important}.purple.accent-2{background-color:#e040fb !important}.purple-text.text-accent-2{color:#e040fb !important}.purple.accent-3{background-color:#d500f9 !important}.purple-text.text-accent-3{color:#d500f9 !important}.purple.accent-4{background-color:#a0f !important}.purple-text.text-accent-4{color:#a0f !important}.deep-purple{background-color:#673ab7 !important}.deep-purple-text{color:#673ab7 !important}.deep-purple.lighten-5{background-color:#ede7f6 !important}.deep-purple-text.text-lighten-5{color:#ede7f6 !important}.deep-purple.lighten-4{background-color:#d1c4e9 !important}.deep-purple-text.text-lighten-4{color:#d1c4e9 !important}.deep-purple.lighten-3{background-color:#b39ddb !important}.deep-purple-text.text-lighten-3{color:#b39ddb !important}.deep-purple.lighten-2{background-color:#9575cd !important}.deep-purple-text.text-lighten-2{color:#9575cd !important}.deep-purple.lighten-1{background-color:#7e57c2 !important}.deep-purple-text.text-lighten-1{color:#7e57c2 !important}.deep-purple.darken-1{background-color:#5e35b1 !important}.deep-purple-text.text-darken-1{color:#5e35b1 !important}.deep-purple.darken-2{background-color:#512da8 !important}.deep-purple-text.text-darken-2{color:#512da8 !important}.deep-purple.darken-3{background-color:#4527a0 !important}.deep-purple-text.text-darken-3{color:#4527a0 !important}.deep-purple.darken-4{background-color:#311b92 !important}.deep-purple-text.text-darken-4{color:#311b92 !important}.deep-purple.accent-1{background-color:#b388ff !important}.deep-purple-text.text-accent-1{color:#b388ff !important}.deep-purple.accent-2{background-color:#7c4dff !important}.deep-purple-text.text-accent-2{color:#7c4dff !important}.deep-purple.accent-3{background-color:#651fff !important}.deep-purple-text.text-accent-3{color:#651fff !important}.deep-purple.accent-4{background-color:#6200ea !important}.deep-purple-text.text-accent-4{color:#6200ea !important}.indigo{background-color:#3f51b5 !important}.indigo-text{color:#3f51b5 !important}.indigo.lighten-5{background-color:#e8eaf6 !important}.indigo-text.text-lighten-5{color:#e8eaf6 !important}.indigo.lighten-4{background-color:#c5cae9 !important}.indigo-text.text-lighten-4{color:#c5cae9 !important}.indigo.lighten-3{background-color:#9fa8da !important}.indigo-text.text-lighten-3{color:#9fa8da !important}.indigo.lighten-2{background-color:#7986cb !important}.indigo-text.text-lighten-2{color:#7986cb !important}.indigo.lighten-1{background-color:#5c6bc0 !important}.indigo-text.text-lighten-1{color:#5c6bc0 !important}.indigo.darken-1{background-color:#3949ab !important}.indigo-text.text-darken-1{color:#3949ab !important}.indigo.darken-2{background-color:#303f9f !important}.indigo-text.text-darken-2{color:#303f9f !important}.indigo.darken-3{background-color:#283593 !important}.indigo-text.text-darken-3{color:#283593 !important}.indigo.darken-4{background-color:#1a237e !important}.indigo-text.text-darken-4{color:#1a237e !important}.indigo.accent-1{background-color:#8c9eff !important}.indigo-text.text-accent-1{color:#8c9eff !important}.indigo.accent-2{background-color:#536dfe !important}.indigo-text.text-accent-2{color:#536dfe !important}.indigo.accent-3{background-color:#3d5afe !important}.indigo-text.text-accent-3{color:#3d5afe !important}.indigo.accent-4{background-color:#304ffe !important}.indigo-text.text-accent-4{color:#304ffe !important}.blue{background-color:#2196F3 !important}.blue-text{color:#2196F3 !important}.blue.lighten-5{background-color:#E3F2FD !important}.blue-text.text-lighten-5{color:#E3F2FD !important}.blue.lighten-4{background-color:#BBDEFB !important}.blue-text.text-lighten-4{color:#BBDEFB !important}.blue.lighten-3{background-color:#90CAF9 !important}.blue-text.text-lighten-3{color:#90CAF9 !important}.blue.lighten-2{background-color:#64B5F6 !important}.blue-text.text-lighten-2{color:#64B5F6 !important}.blue.lighten-1{background-color:#42A5F5 !important}.blue-text.text-lighten-1{color:#42A5F5 !important}.blue.darken-1{background-color:#1E88E5 !important}.blue-text.text-darken-1{color:#1E88E5 !important}.blue.darken-2{background-color:#1976D2 !important}.blue-text.text-darken-2{color:#1976D2 !important}.blue.darken-3{background-color:#1565C0 !important}.blue-text.text-darken-3{color:#1565C0 !important}.blue.darken-4{background-color:#0D47A1 !important}.blue-text.text-darken-4{color:#0D47A1 !important}.blue.accent-1{background-color:#82B1FF !important}.blue-text.text-accent-1{color:#82B1FF !important}.blue.accent-2{background-color:#448AFF !important}.blue-text.text-accent-2{color:#448AFF !important}.blue.accent-3{background-color:#2979FF !important}.blue-text.text-accent-3{color:#2979FF !important}.blue.accent-4{background-color:#2962FF !important}.blue-text.text-accent-4{color:#2962FF !important}.light-blue{background-color:#03a9f4 !important}.light-blue-text{color:#03a9f4 !important}.light-blue.lighten-5{background-color:#e1f5fe !important}.light-blue-text.text-lighten-5{color:#e1f5fe !important}.light-blue.lighten-4{background-color:#b3e5fc !important}.light-blue-text.text-lighten-4{color:#b3e5fc !important}.light-blue.lighten-3{background-color:#81d4fa !important}.light-blue-text.text-lighten-3{color:#81d4fa !important}.light-blue.lighten-2{background-color:#4fc3f7 !important}.light-blue-text.text-lighten-2{color:#4fc3f7 !important}.light-blue.lighten-1{background-color:#29b6f6 !important}.light-blue-text.text-lighten-1{color:#29b6f6 !important}.light-blue.darken-1{background-color:#039be5 !important}.light-blue-text.text-darken-1{color:#039be5 !important}.light-blue.darken-2{background-color:#0288d1 !important}.light-blue-text.text-darken-2{color:#0288d1 !important}.light-blue.darken-3{background-color:#0277bd !important}.light-blue-text.text-darken-3{color:#0277bd !important}.light-blue.darken-4{background-color:#01579b !important}.light-blue-text.text-darken-4{color:#01579b !important}.light-blue.accent-1{background-color:#80d8ff !important}.light-blue-text.text-accent-1{color:#80d8ff !important}.light-blue.accent-2{background-color:#40c4ff !important}.light-blue-text.text-accent-2{color:#40c4ff !important}.light-blue.accent-3{background-color:#00b0ff !important}.light-blue-text.text-accent-3{color:#00b0ff !important}.light-blue.accent-4{background-color:#0091ea !important}.light-blue-text.text-accent-4{color:#0091ea !important}.cyan{background-color:#00bcd4 !important}.cyan-text{color:#00bcd4 !important}.cyan.lighten-5{background-color:#e0f7fa !important}.cyan-text.text-lighten-5{color:#e0f7fa !important}.cyan.lighten-4{background-color:#b2ebf2 !important}.cyan-text.text-lighten-4{color:#b2ebf2 !important}.cyan.lighten-3{background-color:#80deea !important}.cyan-text.text-lighten-3{color:#80deea !important}.cyan.lighten-2{background-color:#4dd0e1 !important}.cyan-text.text-lighten-2{color:#4dd0e1 !important}.cyan.lighten-1{background-color:#26c6da !important}.cyan-text.text-lighten-1{color:#26c6da !important}.cyan.darken-1{background-color:#00acc1 !important}.cyan-text.text-darken-1{color:#00acc1 !important}.cyan.darken-2{background-color:#0097a7 !important}.cyan-text.text-darken-2{color:#0097a7 !important}.cyan.darken-3{background-color:#00838f !important}.cyan-text.text-darken-3{color:#00838f !important}.cyan.darken-4{background-color:#006064 !important}.cyan-text.text-darken-4{color:#006064 !important}.cyan.accent-1{background-color:#84ffff !important}.cyan-text.text-accent-1{color:#84ffff !important}.cyan.accent-2{background-color:#18ffff !important}.cyan-text.text-accent-2{color:#18ffff !important}.cyan.accent-3{background-color:#00e5ff !important}.cyan-text.text-accent-3{color:#00e5ff !important}.cyan.accent-4{background-color:#00b8d4 !important}.cyan-text.text-accent-4{color:#00b8d4 !important}.teal{background-color:#009688 !important}.teal-text{color:#009688 !important}.teal.lighten-5{background-color:#e0f2f1 !important}.teal-text.text-lighten-5{color:#e0f2f1 !important}.teal.lighten-4{background-color:#b2dfdb !important}.teal-text.text-lighten-4{color:#b2dfdb !important}.teal.lighten-3{background-color:#80cbc4 !important}.teal-text.text-lighten-3{color:#80cbc4 !important}.teal.lighten-2{background-color:#4db6ac !important}.teal-text.text-lighten-2{color:#4db6ac !important}.teal.lighten-1{background-color:#26a69a !important}.teal-text.text-lighten-1{color:#26a69a !important}.teal.darken-1{background-color:#00897b !important}.teal-text.text-darken-1{color:#00897b !important}.teal.darken-2{background-color:#00796b !important}.teal-text.text-darken-2{color:#00796b !important}.teal.darken-3{background-color:#00695c !important}.teal-text.text-darken-3{color:#00695c !important}.teal.darken-4{background-color:#004d40 !important}.teal-text.text-darken-4{color:#004d40 !important}.teal.accent-1{background-color:#a7ffeb !important}.teal-text.text-accent-1{color:#a7ffeb !important}.teal.accent-2{background-color:#64ffda !important}.teal-text.text-accent-2{color:#64ffda !important}.teal.accent-3{background-color:#1de9b6 !important}.teal-text.text-accent-3{color:#1de9b6 !important}.teal.accent-4{background-color:#00bfa5 !important}.teal-text.text-accent-4{color:#00bfa5 !important}.green{background-color:#4CAF50 !important}.green-text{color:#4CAF50 !important}.green.lighten-5{background-color:#E8F5E9 !important}.green-text.text-lighten-5{color:#E8F5E9 !important}.green.lighten-4{background-color:#C8E6C9 !important}.green-text.text-lighten-4{color:#C8E6C9 !important}.green.lighten-3{background-color:#A5D6A7 !important}.green-text.text-lighten-3{color:#A5D6A7 !important}.green.lighten-2{background-color:#81C784 !important}.green-text.text-lighten-2{color:#81C784 !important}.green.lighten-1{background-color:#66BB6A !important}.green-text.text-lighten-1{color:#66BB6A !important}.green.darken-1{background-color:#43A047 !important}.green-text.text-darken-1{color:#43A047 !important}.green.darken-2{background-color:#388E3C !important}.green-text.text-darken-2{color:#388E3C !important}.green.darken-3{background-color:#2E7D32 !important}.green-text.text-darken-3{color:#2E7D32 !important}.green.darken-4{background-color:#1B5E20 !important}.green-text.text-darken-4{color:#1B5E20 !important}.green.accent-1{background-color:#B9F6CA !important}.green-text.text-accent-1{color:#B9F6CA !important}.green.accent-2{background-color:#69F0AE !important}.green-text.text-accent-2{color:#69F0AE !important}.green.accent-3{background-color:#00E676 !important}.green-text.text-accent-3{color:#00E676 !important}.green.accent-4{background-color:#00C853 !important}.green-text.text-accent-4{color:#00C853 !important}.light-green{background-color:#8bc34a !important}.light-green-text{color:#8bc34a !important}.light-green.lighten-5{background-color:#f1f8e9 !important}.light-green-text.text-lighten-5{color:#f1f8e9 !important}.light-green.lighten-4{background-color:#dcedc8 !important}.light-green-text.text-lighten-4{color:#dcedc8 !important}.light-green.lighten-3{background-color:#c5e1a5 !important}.light-green-text.text-lighten-3{color:#c5e1a5 !important}.light-green.lighten-2{background-color:#aed581 !important}.light-green-text.text-lighten-2{color:#aed581 !important}.light-green.lighten-1{background-color:#9ccc65 !important}.light-green-text.text-lighten-1{color:#9ccc65 !important}.light-green.darken-1{background-color:#7cb342 !important}.light-green-text.text-darken-1{color:#7cb342 !important}.light-green.darken-2{background-color:#689f38 !important}.light-green-text.text-darken-2{color:#689f38 !important}.light-green.darken-3{background-color:#558b2f !important}.light-green-text.text-darken-3{color:#558b2f !important}.light-green.darken-4{background-color:#33691e !important}.light-green-text.text-darken-4{color:#33691e !important}.light-green.accent-1{background-color:#ccff90 !important}.light-green-text.text-accent-1{color:#ccff90 !important}.light-green.accent-2{background-color:#b2ff59 !important}.light-green-text.text-accent-2{color:#b2ff59 !important}.light-green.accent-3{background-color:#76ff03 !important}.light-green-text.text-accent-3{color:#76ff03 !important}.light-green.accent-4{background-color:#64dd17 !important}.light-green-text.text-accent-4{color:#64dd17 !important}.lime{background-color:#cddc39 !important}.lime-text{color:#cddc39 !important}.lime.lighten-5{background-color:#f9fbe7 !important}.lime-text.text-lighten-5{color:#f9fbe7 !important}.lime.lighten-4{background-color:#f0f4c3 !important}.lime-text.text-lighten-4{color:#f0f4c3 !important}.lime.lighten-3{background-color:#e6ee9c !important}.lime-text.text-lighten-3{color:#e6ee9c !important}.lime.lighten-2{background-color:#dce775 !important}.lime-text.text-lighten-2{color:#dce775 !important}.lime.lighten-1{background-color:#d4e157 !important}.lime-text.text-lighten-1{color:#d4e157 !important}.lime.darken-1{background-color:#c0ca33 !important}.lime-text.text-darken-1{color:#c0ca33 !important}.lime.darken-2{background-color:#afb42b !important}.lime-text.text-darken-2{color:#afb42b !important}.lime.darken-3{background-color:#9e9d24 !important}.lime-text.text-darken-3{color:#9e9d24 !important}.lime.darken-4{background-color:#827717 !important}.lime-text.text-darken-4{color:#827717 !important}.lime.accent-1{background-color:#f4ff81 !important}.lime-text.text-accent-1{color:#f4ff81 !important}.lime.accent-2{background-color:#eeff41 !important}.lime-text.text-accent-2{color:#eeff41 !important}.lime.accent-3{background-color:#c6ff00 !important}.lime-text.text-accent-3{color:#c6ff00 !important}.lime.accent-4{background-color:#aeea00 !important}.lime-text.text-accent-4{color:#aeea00 !important}.yellow{background-color:#ffeb3b !important}.yellow-text{color:#ffeb3b !important}.yellow.lighten-5{background-color:#fffde7 !important}.yellow-text.text-lighten-5{color:#fffde7 !important}.yellow.lighten-4{background-color:#fff9c4 !important}.yellow-text.text-lighten-4{color:#fff9c4 !important}.yellow.lighten-3{background-color:#fff59d !important}.yellow-text.text-lighten-3{color:#fff59d !important}.yellow.lighten-2{background-color:#fff176 !important}.yellow-text.text-lighten-2{color:#fff176 !important}.yellow.lighten-1{background-color:#ffee58 !important}.yellow-text.text-lighten-1{color:#ffee58 !important}.yellow.darken-1{background-color:#fdd835 !important}.yellow-text.text-darken-1{color:#fdd835 !important}.yellow.darken-2{background-color:#fbc02d !important}.yellow-text.text-darken-2{color:#fbc02d !important}.yellow.darken-3{background-color:#f9a825 !important}.yellow-text.text-darken-3{color:#f9a825 !important}.yellow.darken-4{background-color:#f57f17 !important}.yellow-text.text-darken-4{color:#f57f17 !important}.yellow.accent-1{background-color:#ffff8d !important}.yellow-text.text-accent-1{color:#ffff8d !important}.yellow.accent-2{background-color:#ff0 !important}.yellow-text.text-accent-2{color:#ff0 !important}.yellow.accent-3{background-color:#ffea00 !important}.yellow-text.text-accent-3{color:#ffea00 !important}.yellow.accent-4{background-color:#ffd600 !important}.yellow-text.text-accent-4{color:#ffd600 !important}.amber{background-color:#ffc107 !important}.amber-text{color:#ffc107 !important}.amber.lighten-5{background-color:#fff8e1 !important}.amber-text.text-lighten-5{color:#fff8e1 !important}.amber.lighten-4{background-color:#ffecb3 !important}.amber-text.text-lighten-4{color:#ffecb3 !important}.amber.lighten-3{background-color:#ffe082 !important}.amber-text.text-lighten-3{color:#ffe082 !important}.amber.lighten-2{background-color:#ffd54f !important}.amber-text.text-lighten-2{color:#ffd54f !important}.amber.lighten-1{background-color:#ffca28 !important}.amber-text.text-lighten-1{color:#ffca28 !important}.amber.darken-1{background-color:#ffb300 !important}.amber-text.text-darken-1{color:#ffb300 !important}.amber.darken-2{background-color:#ffa000 !important}.amber-text.text-darken-2{color:#ffa000 !important}.amber.darken-3{background-color:#ff8f00 !important}.amber-text.text-darken-3{color:#ff8f00 !important}.amber.darken-4{background-color:#ff6f00 !important}.amber-text.text-darken-4{color:#ff6f00 !important}.amber.accent-1{background-color:#ffe57f !important}.amber-text.text-accent-1{color:#ffe57f !important}.amber.accent-2{background-color:#ffd740 !important}.amber-text.text-accent-2{color:#ffd740 !important}.amber.accent-3{background-color:#ffc400 !important}.amber-text.text-accent-3{color:#ffc400 !important}.amber.accent-4{background-color:#ffab00 !important}.amber-text.text-accent-4{color:#ffab00 !important}.orange{background-color:#ff9800 !important}.orange-text{color:#ff9800 !important}.orange.lighten-5{background-color:#fff3e0 !important}.orange-text.text-lighten-5{color:#fff3e0 !important}.orange.lighten-4{background-color:#ffe0b2 !important}.orange-text.text-lighten-4{color:#ffe0b2 !important}.orange.lighten-3{background-color:#ffcc80 !important}.orange-text.text-lighten-3{color:#ffcc80 !important}.orange.lighten-2{background-color:#ffb74d !important}.orange-text.text-lighten-2{color:#ffb74d !important}.orange.lighten-1{background-color:#ffa726 !important}.orange-text.text-lighten-1{color:#ffa726 !important}.orange.darken-1{background-color:#fb8c00 !important}.orange-text.text-darken-1{color:#fb8c00 !important}.orange.darken-2{background-color:#f57c00 !important}.orange-text.text-darken-2{color:#f57c00 !important}.orange.darken-3{background-color:#ef6c00 !important}.orange-text.text-darken-3{color:#ef6c00 !important}.orange.darken-4{background-color:#e65100 !important}.orange-text.text-darken-4{color:#e65100 !important}.orange.accent-1{background-color:#ffd180 !important}.orange-text.text-accent-1{color:#ffd180 !important}.orange.accent-2{background-color:#ffab40 !important}.orange-text.text-accent-2{color:#ffab40 !important}.orange.accent-3{background-color:#ff9100 !important}.orange-text.text-accent-3{color:#ff9100 !important}.orange.accent-4{background-color:#ff6d00 !important}.orange-text.text-accent-4{color:#ff6d00 !important}.deep-orange{background-color:#ff5722 !important}.deep-orange-text{color:#ff5722 !important}.deep-orange.lighten-5{background-color:#fbe9e7 !important}.deep-orange-text.text-lighten-5{color:#fbe9e7 !important}.deep-orange.lighten-4{background-color:#ffccbc !important}.deep-orange-text.text-lighten-4{color:#ffccbc !important}.deep-orange.lighten-3{background-color:#ffab91 !important}.deep-orange-text.text-lighten-3{color:#ffab91 !important}.deep-orange.lighten-2{background-color:#ff8a65 !important}.deep-orange-text.text-lighten-2{color:#ff8a65 !important}.deep-orange.lighten-1{background-color:#ff7043 !important}.deep-orange-text.text-lighten-1{color:#ff7043 !important}.deep-orange.darken-1{background-color:#f4511e !important}.deep-orange-text.text-darken-1{color:#f4511e !important}.deep-orange.darken-2{background-color:#e64a19 !important}.deep-orange-text.text-darken-2{color:#e64a19 !important}.deep-orange.darken-3{background-color:#d84315 !important}.deep-orange-text.text-darken-3{color:#d84315 !important}.deep-orange.darken-4{background-color:#bf360c !important}.deep-orange-text.text-darken-4{color:#bf360c !important}.deep-orange.accent-1{background-color:#ff9e80 !important}.deep-orange-text.text-accent-1{color:#ff9e80 !important}.deep-orange.accent-2{background-color:#ff6e40 !important}.deep-orange-text.text-accent-2{color:#ff6e40 !important}.deep-orange.accent-3{background-color:#ff3d00 !important}.deep-orange-text.text-accent-3{color:#ff3d00 !important}.deep-orange.accent-4{background-color:#dd2c00 !important}.deep-orange-text.text-accent-4{color:#dd2c00 !important}.brown{background-color:#795548 !important}.brown-text{color:#795548 !important}.brown.lighten-5{background-color:#efebe9 !important}.brown-text.text-lighten-5{color:#efebe9 !important}.brown.lighten-4{background-color:#d7ccc8 !important}.brown-text.text-lighten-4{color:#d7ccc8 !important}.brown.lighten-3{background-color:#bcaaa4 !important}.brown-text.text-lighten-3{color:#bcaaa4 !important}.brown.lighten-2{background-color:#a1887f !important}.brown-text.text-lighten-2{color:#a1887f !important}.brown.lighten-1{background-color:#8d6e63 !important}.brown-text.text-lighten-1{color:#8d6e63 !important}.brown.darken-1{background-color:#6d4c41 !important}.brown-text.text-darken-1{color:#6d4c41 !important}.brown.darken-2{background-color:#5d4037 !important}.brown-text.text-darken-2{color:#5d4037 !important}.brown.darken-3{background-color:#4e342e !important}.brown-text.text-darken-3{color:#4e342e !important}.brown.darken-4{background-color:#3e2723 !important}.brown-text.text-darken-4{color:#3e2723 !important}.blue-grey{background-color:#607d8b !important}.blue-grey-text{color:#607d8b !important}.blue-grey.lighten-5{background-color:#eceff1 !important}.blue-grey-text.text-lighten-5{color:#eceff1 !important}.blue-grey.lighten-4{background-color:#cfd8dc !important}.blue-grey-text.text-lighten-4{color:#cfd8dc !important}.blue-grey.lighten-3{background-color:#b0bec5 !important}.blue-grey-text.text-lighten-3{color:#b0bec5 !important}.blue-grey.lighten-2{background-color:#90a4ae !important}.blue-grey-text.text-lighten-2{color:#90a4ae !important}.blue-grey.lighten-1{background-color:#78909c !important}.blue-grey-text.text-lighten-1{color:#78909c !important}.blue-grey.darken-1{background-color:#546e7a !important}.blue-grey-text.text-darken-1{color:#546e7a !important}.blue-grey.darken-2{background-color:#455a64 !important}.blue-grey-text.text-darken-2{color:#455a64 !important}.blue-grey.darken-3{background-color:#37474f !important}.blue-grey-text.text-darken-3{color:#37474f !important}.blue-grey.darken-4{background-color:#263238 !important}.blue-grey-text.text-darken-4{color:#263238 !important}.grey{background-color:#9e9e9e !important}.grey-text{color:#9e9e9e !important}.grey.lighten-5{background-color:#fafafa !important}.grey-text.text-lighten-5{color:#fafafa !important}.grey.lighten-4{background-color:#f5f5f5 !important}.grey-text.text-lighten-4{color:#f5f5f5 !important}.grey.lighten-3{background-color:#eee !important}.grey-text.text-lighten-3{color:#eee !important}.grey.lighten-2{background-color:#e0e0e0 !important}.grey-text.text-lighten-2{color:#e0e0e0 !important}.grey.lighten-1{background-color:#bdbdbd !important}.grey-text.text-lighten-1{color:#bdbdbd !important}.grey.darken-1{background-color:#757575 !important}.grey-text.text-darken-1{color:#757575 !important}.grey.darken-2{background-color:#616161 !important}.grey-text.text-darken-2{color:#616161 !important}.grey.darken-3{background-color:#424242 !important}.grey-text.text-darken-3{color:#424242 !important}.grey.darken-4{background-color:#212121 !important}.grey-text.text-darken-4{color:#212121 !important}.black{background-color:#000 !important}.black-text{color:#000 !important}.white{background-color:#fff !important}.white-text{color:#fff !important}.transparent{background-color:rgba(0,0,0,0) !important}.transparent-text{color:rgba(0,0,0,0) !important}/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:0.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;box-sizing:inherit}button,input,optgroup,select,textarea{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul:not(.browser-default){padding-left:0;list-style-type:none}ul:not(.browser-default)>li{list-style-type:none}a{color:#039be5;text-decoration:none;-webkit-tap-highlight-color:transparent}.valign-wrapper{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.clearfix{clear:both}.z-depth-0{-webkit-box-shadow:none !important;box-shadow:none !important}.z-depth-1,nav,.card-panel,.card,.toast,.btn,.btn-large,.btn-small,.btn-floating,.dropdown-content,.collapsible,.sidenav{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}.z-depth-1-half,.btn:hover,.btn-large:hover,.btn-small:hover,.btn-floating:hover{-webkit-box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2);box-shadow:0 3px 3px 0 rgba(0,0,0,0.14),0 1px 7px 0 rgba(0,0,0,0.12),0 3px 1px -1px rgba(0,0,0,0.2)}.z-depth-2{-webkit-box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3);box-shadow:0 4px 5px 0 rgba(0,0,0,0.14),0 1px 10px 0 rgba(0,0,0,0.12),0 2px 4px -1px rgba(0,0,0,0.3)}.z-depth-3{-webkit-box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2);box-shadow:0 8px 17px 2px rgba(0,0,0,0.14),0 3px 14px 2px rgba(0,0,0,0.12),0 5px 5px -3px rgba(0,0,0,0.2)}.z-depth-4{-webkit-box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2);box-shadow:0 16px 24px 2px rgba(0,0,0,0.14),0 6px 30px 5px rgba(0,0,0,0.12),0 8px 10px -7px rgba(0,0,0,0.2)}.z-depth-5,.modal{-webkit-box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2);box-shadow:0 24px 38px 3px rgba(0,0,0,0.14),0 9px 46px 8px rgba(0,0,0,0.12),0 11px 15px -7px rgba(0,0,0,0.2)}.hoverable{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s}.hoverable:hover{-webkit-box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);box-shadow:0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}.divider{height:1px;overflow:hidden;background-color:#e0e0e0}blockquote{margin:20px 0;padding-left:1.5rem;border-left:5px solid #ee6e73}i{line-height:inherit}i.left{float:left;margin-right:15px}i.right{float:right;margin-left:15px}i.tiny{font-size:1rem}i.small{font-size:2rem}i.medium{font-size:4rem}i.large{font-size:6rem}img.responsive-img,video.responsive-video{max-width:100%;height:auto}.pagination li{display:inline-block;border-radius:2px;text-align:center;vertical-align:top;height:30px}.pagination li a{color:#444;display:inline-block;font-size:1.2rem;padding:0 10px;line-height:30px}.pagination li.active a{color:#fff}.pagination li.active{background-color:#ee6e73}.pagination li.disabled a{cursor:default;color:#999}.pagination li i{font-size:2rem}.pagination li.pages ul li{display:inline-block;float:none}@media only screen and (max-width: 992px){.pagination{width:100%}.pagination li.prev,.pagination li.next{width:10%}.pagination li.pages{width:80%;overflow:hidden;white-space:nowrap}}.breadcrumb{font-size:18px;color:rgba(255,255,255,0.7)}.breadcrumb i,.breadcrumb [class^="mdi-"],.breadcrumb [class*="mdi-"],.breadcrumb i.material-icons{display:inline-block;float:left;font-size:24px}.breadcrumb:before{content:'\E5CC';color:rgba(255,255,255,0.7);vertical-align:top;display:inline-block;font-family:'Material Icons';font-weight:normal;font-style:normal;font-size:25px;margin:0 10px 0 8px;-webkit-font-smoothing:antialiased}.breadcrumb:first-child:before{display:none}.breadcrumb:last-child{color:#fff}.parallax-container{position:relative;overflow:hidden;height:500px}.parallax-container .parallax{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1}.parallax-container .parallax img{opacity:0;position:absolute;left:50%;bottom:0;min-width:100%;min-height:100%;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);-webkit-transform:translateX(-50%);transform:translateX(-50%)}.pin-top,.pin-bottom{position:relative}.pinned{position:fixed !important}ul.staggered-list li{opacity:0}.fade-in{opacity:0;-webkit-transform-origin:0 50%;transform-origin:0 50%}@media only screen and (max-width: 600px){.hide-on-small-only,.hide-on-small-and-down{display:none !important}}@media only screen and (max-width: 992px){.hide-on-med-and-down{display:none !important}}@media only screen and (min-width: 601px){.hide-on-med-and-up{display:none !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.hide-on-med-only{display:none !important}}@media only screen and (min-width: 993px){.hide-on-large-only{display:none !important}}@media only screen and (min-width: 1201px){.hide-on-extra-large-only{display:none !important}}@media only screen and (min-width: 1201px){.show-on-extra-large{display:block !important}}@media only screen and (min-width: 993px){.show-on-large{display:block !important}}@media only screen and (min-width: 600px) and (max-width: 992px){.show-on-medium{display:block !important}}@media only screen and (max-width: 600px){.show-on-small{display:block !important}}@media only screen and (min-width: 601px){.show-on-medium-and-up{display:block !important}}@media only screen and (max-width: 992px){.show-on-medium-and-down{display:block !important}}@media only screen and (max-width: 600px){.center-on-small-only{text-align:center}}.page-footer{padding-top:20px;color:#fff;background-color:#ee6e73}.page-footer .footer-copyright{overflow:hidden;min-height:50px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:10px 0px;color:rgba(255,255,255,0.8);background-color:rgba(51,51,51,0.08)}table,th,td{border:none}table{width:100%;display:table;border-collapse:collapse;border-spacing:0}table.striped tr{border-bottom:none}table.striped>tbody>tr:nth-child(odd){background-color:rgba(242,242,242,0.5)}table.striped>tbody>tr>td{border-radius:0}table.highlight>tbody>tr{-webkit-transition:background-color .25s ease;transition:background-color .25s ease}table.highlight>tbody>tr:hover{background-color:rgba(242,242,242,0.5)}table.centered thead tr th,table.centered tbody tr td{text-align:center}tr{border-bottom:1px solid rgba(0,0,0,0.12)}td,th{padding:15px 5px;display:table-cell;text-align:left;vertical-align:middle;border-radius:2px}@media only screen and (max-width: 992px){table.responsive-table{width:100%;border-collapse:collapse;border-spacing:0;display:block;position:relative}table.responsive-table td:empty:before{content:'\00a0'}table.responsive-table th,table.responsive-table td{margin:0;vertical-align:top}table.responsive-table th{text-align:left}table.responsive-table thead{display:block;float:left}table.responsive-table thead tr{display:block;padding:0 10px 0 0}table.responsive-table thead tr th::before{content:"\00a0"}table.responsive-table tbody{display:block;width:auto;position:relative;overflow-x:auto;white-space:nowrap}table.responsive-table tbody tr{display:inline-block;vertical-align:top}table.responsive-table th{display:block;text-align:right}table.responsive-table td{display:block;min-height:1.25em;text-align:left}table.responsive-table tr{border-bottom:none;padding:0 10px}table.responsive-table thead{border:0;border-right:1px solid rgba(0,0,0,0.12)}}.collection{margin:.5rem 0 1rem 0;border:1px solid #e0e0e0;border-radius:2px;overflow:hidden;position:relative}.collection .collection-item{background-color:#fff;line-height:1.5rem;padding:10px 20px;margin:0;border-bottom:1px solid #e0e0e0}.collection .collection-item.avatar{min-height:84px;padding-left:72px;position:relative}.collection .collection-item.avatar:not(.circle-clipper)>.circle,.collection .collection-item.avatar :not(.circle-clipper)>.circle{position:absolute;width:42px;height:42px;overflow:hidden;left:15px;display:inline-block;vertical-align:middle}.collection .collection-item.avatar i.circle{font-size:18px;line-height:42px;color:#fff;background-color:#999;text-align:center}.collection .collection-item.avatar .title{font-size:16px}.collection .collection-item.avatar p{margin:0}.collection .collection-item.avatar .secondary-content{position:absolute;top:16px;right:16px}.collection .collection-item:last-child{border-bottom:none}.collection .collection-item.active{background-color:#26a69a;color:#eafaf9}.collection .collection-item.active .secondary-content{color:#fff}.collection a.collection-item{display:block;-webkit-transition:.25s;transition:.25s;color:#26a69a}.collection a.collection-item:not(.active):hover{background-color:#ddd}.collection.with-header .collection-header{background-color:#fff;border-bottom:1px solid #e0e0e0;padding:10px 20px}.collection.with-header .collection-item{padding-left:30px}.collection.with-header .collection-item.avatar{padding-left:72px}.secondary-content{float:right;color:#26a69a}.collapsible .collection{margin:0;border:none}.video-container{position:relative;padding-bottom:56.25%;height:0;overflow:hidden}.video-container iframe,.video-container object,.video-container embed{position:absolute;top:0;left:0;width:100%;height:100%}.progress{position:relative;height:4px;display:block;width:100%;background-color:#acece6;border-radius:2px;margin:.5rem 0 1rem 0;overflow:hidden}.progress .determinate{position:absolute;top:0;left:0;bottom:0;background-color:#26a69a;-webkit-transition:width .3s linear;transition:width .3s linear}.progress .indeterminate{background-color:#26a69a}.progress .indeterminate:before{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;animation:indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite}.progress .indeterminate:after{content:'';position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left, right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;animation:indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}.hide{display:none !important}.left-align{text-align:left}.right-align{text-align:right}.center,.center-align{text-align:center}.left{float:left !important}.right{float:right !important}.no-select,input[type=range],input[type=range]+.thumb{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.circle{border-radius:50%}.center-block{display:block;margin-left:auto;margin-right:auto}.truncate{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-padding{padding:0 !important}span.badge{min-width:3rem;padding:0 6px;margin-left:14px;text-align:center;font-size:1rem;line-height:22px;height:22px;color:#757575;float:right;-webkit-box-sizing:border-box;box-sizing:border-box}span.badge.new{font-weight:300;font-size:0.8rem;color:#fff;background-color:#26a69a;border-radius:2px}span.badge.new:after{content:" new"}span.badge[data-badge-caption]::after{content:" " attr(data-badge-caption)}nav ul a span.badge{display:inline-block;float:none;margin-left:4px;line-height:22px;height:22px;-webkit-font-smoothing:auto}.collection-item span.badge{margin-top:calc(.75rem - 11px)}.collapsible span.badge{margin-left:auto}.sidenav span.badge{margin-top:calc(24px - 11px)}table span.badge{display:inline-block;float:none;margin-left:auto}.material-icons{text-rendering:optimizeLegibility;-webkit-font-feature-settings:'liga';-moz-font-feature-settings:'liga';font-feature-settings:'liga'}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width: 601px){.container{width:85%}}@media only screen and (min-width: 993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*="push-"],.row .col[class*="pull-"]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width: 601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width: 993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width: 1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}nav{color:#fff;background-color:#ee6e73;width:100%;height:56px;line-height:56px}nav.nav-extended{height:auto}nav.nav-extended .nav-wrapper{min-height:56px;height:auto}nav.nav-extended .nav-content{position:relative;line-height:normal}nav a{color:#fff}nav i,nav [class^="mdi-"],nav [class*="mdi-"],nav i.material-icons{display:block;font-size:24px;height:56px;line-height:56px}nav .nav-wrapper{position:relative;height:100%}@media only screen and (min-width: 993px){nav a.sidenav-trigger{display:none}}nav .sidenav-trigger{float:left;position:relative;z-index:1;height:56px;margin:0 18px}nav .sidenav-trigger i{height:56px;line-height:56px}nav .brand-logo{position:absolute;color:#fff;display:inline-block;font-size:2.1rem;padding:0}nav .brand-logo.center{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}@media only screen and (max-width: 992px){nav .brand-logo{left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%)}nav .brand-logo.left,nav .brand-logo.right{padding:0;-webkit-transform:none;transform:none}nav .brand-logo.left{left:0.5rem}nav .brand-logo.right{right:0.5rem;left:auto}}nav .brand-logo.right{right:0.5rem;padding:0}nav .brand-logo i,nav .brand-logo [class^="mdi-"],nav .brand-logo [class*="mdi-"],nav .brand-logo i.material-icons{float:left;margin-right:15px}nav .nav-title{display:inline-block;font-size:32px;padding:28px 0}nav ul{margin:0}nav ul li{-webkit-transition:background-color .3s;transition:background-color .3s;float:left;padding:0}nav ul li.active{background-color:rgba(0,0,0,0.1)}nav ul a{-webkit-transition:background-color .3s;transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}nav ul a.btn,nav ul a.btn-large,nav ul a.btn-small,nav ul a.btn-large,nav ul a.btn-flat,nav ul a.btn-floating{margin-top:-2px;margin-left:15px;margin-right:15px}nav ul a.btn>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-small>.material-icons,nav ul a.btn-large>.material-icons,nav ul a.btn-flat>.material-icons,nav ul a.btn-floating>.material-icons{height:inherit;line-height:inherit}nav ul a:hover{background-color:rgba(0,0,0,0.1)}nav ul.left{float:left}nav form{height:100%}nav .input-field{margin:0;height:100%}nav .input-field input{height:100%;font-size:1.2rem;border:none;padding-left:2rem}nav .input-field input:focus,nav .input-field input[type=text]:valid,nav .input-field input[type=password]:valid,nav .input-field input[type=email]:valid,nav .input-field input[type=url]:valid,nav .input-field input[type=date]:valid{border:none;-webkit-box-shadow:none;box-shadow:none}nav .input-field label{top:0;left:0}nav .input-field label i{color:rgba(255,255,255,0.7);-webkit-transition:color .3s;transition:color .3s}nav .input-field label.active i{color:#fff}.navbar-fixed{position:relative;height:56px;z-index:997}.navbar-fixed nav{position:fixed}@media only screen and (min-width: 601px){nav.nav-extended .nav-wrapper{min-height:64px}nav,nav .nav-wrapper i,nav a.sidenav-trigger,nav a.sidenav-trigger i{height:64px;line-height:64px}.navbar-fixed{height:64px}}a{text-decoration:none}html{line-height:1.5;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-weight:normal;color:rgba(0,0,0,0.87)}@media only screen and (min-width: 0){html{font-size:14px}}@media only screen and (min-width: 992px){html{font-size:14.5px}}@media only screen and (min-width: 1200px){html{font-size:15px}}h1,h2,h3,h4,h5,h6{font-weight:400;line-height:1.3}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{font-weight:inherit}h1{font-size:4.2rem;line-height:110%;margin:2.8rem 0 1.68rem 0}h2{font-size:3.56rem;line-height:110%;margin:2.3733333333rem 0 1.424rem 0}h3{font-size:2.92rem;line-height:110%;margin:1.9466666667rem 0 1.168rem 0}h4{font-size:2.28rem;line-height:110%;margin:1.52rem 0 .912rem 0}h5{font-size:1.64rem;line-height:110%;margin:1.0933333333rem 0 .656rem 0}h6{font-size:1.15rem;line-height:110%;margin:.7666666667rem 0 .46rem 0}em{font-style:italic}strong{font-weight:500}small{font-size:75%}.light{font-weight:300}.thin{font-weight:200}@media only screen and (min-width: 360px){.flow-text{font-size:1.2rem}}@media only screen and (min-width: 390px){.flow-text{font-size:1.224rem}}@media only screen and (min-width: 420px){.flow-text{font-size:1.248rem}}@media only screen and (min-width: 450px){.flow-text{font-size:1.272rem}}@media only screen and (min-width: 480px){.flow-text{font-size:1.296rem}}@media only screen and (min-width: 510px){.flow-text{font-size:1.32rem}}@media only screen and (min-width: 540px){.flow-text{font-size:1.344rem}}@media only screen and (min-width: 570px){.flow-text{font-size:1.368rem}}@media only screen and (min-width: 600px){.flow-text{font-size:1.392rem}}@media only screen and (min-width: 630px){.flow-text{font-size:1.416rem}}@media only screen and (min-width: 660px){.flow-text{font-size:1.44rem}}@media only screen and (min-width: 690px){.flow-text{font-size:1.464rem}}@media only screen and (min-width: 720px){.flow-text{font-size:1.488rem}}@media only screen and (min-width: 750px){.flow-text{font-size:1.512rem}}@media only screen and (min-width: 780px){.flow-text{font-size:1.536rem}}@media only screen and (min-width: 810px){.flow-text{font-size:1.56rem}}@media only screen and (min-width: 840px){.flow-text{font-size:1.584rem}}@media only screen and (min-width: 870px){.flow-text{font-size:1.608rem}}@media only screen and (min-width: 900px){.flow-text{font-size:1.632rem}}@media only screen and (min-width: 930px){.flow-text{font-size:1.656rem}}@media only screen and (min-width: 960px){.flow-text{font-size:1.68rem}}@media only screen and (max-width: 360px){.flow-text{font-size:1.2rem}}.scale-transition{-webkit-transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:-webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;transition:transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63), -webkit-transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important}.scale-transition.scale-out{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .2s !important;transition:-webkit-transform .2s !important;transition:transform .2s !important;transition:transform .2s, -webkit-transform .2s !important}.scale-transition.scale-in{-webkit-transform:scale(1);transform:scale(1)}.card-panel{-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;padding:24px;margin:.5rem 0 1rem 0;border-radius:2px;background-color:#fff}.card{position:relative;margin:.5rem 0 1rem 0;background-color:#fff;-webkit-transition:-webkit-box-shadow .25s;transition:-webkit-box-shadow .25s;transition:box-shadow .25s;transition:box-shadow .25s, -webkit-box-shadow .25s;border-radius:2px}.card .card-title{font-size:24px;font-weight:300}.card .card-title.activator{cursor:pointer}.card.small,.card.medium,.card.large{position:relative}.card.small .card-image,.card.medium .card-image,.card.large .card-image{max-height:60%;overflow:hidden}.card.small .card-image+.card-content,.card.medium .card-image+.card-content,.card.large .card-image+.card-content{max-height:40%}.card.small .card-content,.card.medium .card-content,.card.large .card-content{max-height:100%;overflow:hidden}.card.small .card-action,.card.medium .card-action,.card.large .card-action{position:absolute;bottom:0;left:0;right:0}.card.small{height:300px}.card.medium{height:400px}.card.large{height:500px}.card.horizontal{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.card.horizontal.small .card-image,.card.horizontal.medium .card-image,.card.horizontal.large .card-image{height:100%;max-height:none;overflow:visible}.card.horizontal.small .card-image img,.card.horizontal.medium .card-image img,.card.horizontal.large .card-image img{height:100%}.card.horizontal .card-image{max-width:50%}.card.horizontal .card-image img{border-radius:2px 0 0 2px;max-width:100%;width:auto}.card.horizontal .card-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;position:relative}.card.horizontal .card-stacked .card-content{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.card.sticky-action .card-action{z-index:2}.card.sticky-action .card-reveal{z-index:1;padding-bottom:64px}.card .card-image{position:relative}.card .card-image img{display:block;border-radius:2px 2px 0 0;position:relative;left:0;right:0;top:0;bottom:0;width:100%}.card .card-image .card-title{color:#fff;position:absolute;bottom:0;left:0;max-width:100%;padding:24px}.card .card-content{padding:24px;border-radius:0 0 2px 2px}.card .card-content p{margin:0}.card .card-content .card-title{display:block;line-height:32px;margin-bottom:8px}.card .card-content .card-title i{line-height:32px}.card .card-action{background-color:inherit;border-top:1px solid rgba(160,160,160,0.2);position:relative;padding:16px 24px}.card .card-action:last-child{border-radius:0 0 2px 2px}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating){color:#ffab40;margin-right:24px;-webkit-transition:color .3s ease;transition:color .3s ease;text-transform:uppercase}.card .card-action a:not(.btn):not(.btn-large):not(.btn-small):not(.btn-large):not(.btn-floating):hover{color:#ffd8a6}.card .card-reveal{padding:24px;position:absolute;background-color:#fff;width:100%;overflow-y:auto;left:0;top:100%;height:100%;z-index:3;display:none}.card .card-reveal .card-title{cursor:pointer;display:block}#toast-container{display:block;position:fixed;z-index:10000}@media only screen and (max-width: 600px){#toast-container{min-width:100%;bottom:0%}}@media only screen and (min-width: 601px) and (max-width: 992px){#toast-container{left:5%;bottom:7%;max-width:90%}}@media only screen and (min-width: 993px){#toast-container{top:10%;right:7%;max-width:86%}}.toast{border-radius:2px;top:35px;width:auto;margin-top:10px;position:relative;max-width:100%;height:auto;min-height:48px;line-height:1.5em;background-color:#323232;padding:10px 25px;font-size:1.1rem;font-weight:300;color:#fff;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;cursor:default}.toast .toast-action{color:#eeff41;font-weight:500;margin-right:-25px;margin-left:3rem}.toast.rounded{border-radius:24px}@media only screen and (max-width: 600px){.toast{width:100%;border-radius:0}}.tabs{position:relative;overflow-x:auto;overflow-y:hidden;height:48px;width:100%;background-color:#fff;margin:0 auto;white-space:nowrap}.tabs.tabs-transparent{background-color:transparent}.tabs.tabs-transparent .tab a,.tabs.tabs-transparent .tab.disabled a,.tabs.tabs-transparent .tab.disabled a:hover{color:rgba(255,255,255,0.7)}.tabs.tabs-transparent .tab a:hover,.tabs.tabs-transparent .tab a.active{color:#fff}.tabs.tabs-transparent .indicator{background-color:#fff}.tabs.tabs-fixed-width{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs.tabs-fixed-width .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab{display:inline-block;text-align:center;line-height:48px;height:48px;padding:0;margin:0;text-transform:uppercase}.tabs .tab a{color:rgba(238,110,115,0.7);display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease, background-color .28s ease;transition:color .28s ease, background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:rgba(246,178,181,0.2);outline:none}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ee6e73}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:rgba(238,110,115,0.4);cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#f6b2b5;will-change:left, right}@media only screen and (max-width: 992px){.tabs{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.tabs .tab{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.tabs .tab a{padding:0 12px}}.material-tooltip{padding:10px 8px;font-size:1rem;z-index:2000;background-color:transparent;border-radius:2px;color:#fff;min-height:36px;line-height:120%;opacity:0;position:absolute;text-align:center;max-width:calc(100% - 4px);overflow:hidden;left:0;top:0;pointer-events:none;visibility:hidden;background-color:#323232}.backdrop{position:absolute;opacity:0;height:7px;width:14px;border-radius:0 0 50% 50%;background-color:#323232;z-index:-1;-webkit-transform-origin:50% 0%;transform-origin:50% 0%;visibility:hidden}.btn,.btn-large,.btn-small,.btn-flat{border:none;border-radius:2px;display:inline-block;height:36px;line-height:36px;padding:0 16px;text-transform:uppercase;vertical-align:middle;-webkit-tap-highlight-color:transparent}.btn.disabled,.disabled.btn-large,.disabled.btn-small,.btn-floating.disabled,.btn-large.disabled,.btn-small.disabled,.btn-flat.disabled,.btn:disabled,.btn-large:disabled,.btn-small:disabled,.btn-floating:disabled,.btn-large:disabled,.btn-small:disabled,.btn-flat:disabled,.btn[disabled],.btn-large[disabled],.btn-small[disabled],.btn-floating[disabled],.btn-large[disabled],.btn-small[disabled],.btn-flat[disabled]{pointer-events:none;background-color:#DFDFDF !important;-webkit-box-shadow:none;box-shadow:none;color:#9F9F9F !important;cursor:default}.btn.disabled:hover,.disabled.btn-large:hover,.disabled.btn-small:hover,.btn-floating.disabled:hover,.btn-large.disabled:hover,.btn-small.disabled:hover,.btn-flat.disabled:hover,.btn:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-floating:disabled:hover,.btn-large:disabled:hover,.btn-small:disabled:hover,.btn-flat:disabled:hover,.btn[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-floating[disabled]:hover,.btn-large[disabled]:hover,.btn-small[disabled]:hover,.btn-flat[disabled]:hover{background-color:#DFDFDF !important;color:#9F9F9F !important}.btn,.btn-large,.btn-small,.btn-floating,.btn-large,.btn-small,.btn-flat{font-size:14px;outline:0}.btn i,.btn-large i,.btn-small i,.btn-floating i,.btn-large i,.btn-small i,.btn-flat i{font-size:1.3rem;line-height:inherit}.btn:focus,.btn-large:focus,.btn-small:focus,.btn-floating:focus{background-color:#1d7d74}.btn,.btn-large,.btn-small{text-decoration:none;color:#fff;background-color:#26a69a;text-align:center;letter-spacing:.5px;-webkit-transition:background-color .2s ease-out;transition:background-color .2s ease-out;cursor:pointer}.btn:hover,.btn-large:hover,.btn-small:hover{background-color:#2bbbad}.btn-floating{display:inline-block;color:#fff;position:relative;overflow:hidden;z-index:1;width:40px;height:40px;line-height:40px;padding:0;background-color:#26a69a;border-radius:50%;-webkit-transition:background-color .3s;transition:background-color .3s;cursor:pointer;vertical-align:middle}.btn-floating:hover{background-color:#26a69a}.btn-floating:before{border-radius:0}.btn-floating.btn-large{width:56px;height:56px;padding:0}.btn-floating.btn-large.halfway-fab{bottom:-28px}.btn-floating.btn-large i{line-height:56px}.btn-floating.btn-small{width:32.4px;height:32.4px}.btn-floating.btn-small.halfway-fab{bottom:-16.2px}.btn-floating.btn-small i{line-height:32.4px}.btn-floating.halfway-fab{position:absolute;right:24px;bottom:-20px}.btn-floating.halfway-fab.left{right:auto;left:24px}.btn-floating i{width:inherit;display:inline-block;text-align:center;color:#fff;font-size:1.6rem;line-height:40px}button.btn-floating{border:none}.fixed-action-btn{position:fixed;right:23px;bottom:23px;padding-top:15px;margin-bottom:0;z-index:997}.fixed-action-btn.active ul{visibility:visible}.fixed-action-btn.direction-left,.fixed-action-btn.direction-right{padding:0 0 0 15px}.fixed-action-btn.direction-left ul,.fixed-action-btn.direction-right ul{text-align:right;right:64px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);height:100%;left:auto;width:500px}.fixed-action-btn.direction-left ul li,.fixed-action-btn.direction-right ul li{display:inline-block;margin:7.5px 15px 0 0}.fixed-action-btn.direction-right{padding:0 15px 0 0}.fixed-action-btn.direction-right ul{text-align:left;direction:rtl;left:64px;right:auto}.fixed-action-btn.direction-right ul li{margin:7.5px 0 0 15px}.fixed-action-btn.direction-bottom{padding:0 0 15px 0}.fixed-action-btn.direction-bottom ul{top:64px;bottom:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.fixed-action-btn.direction-bottom ul li{margin:15px 0 0 0}.fixed-action-btn.toolbar{padding:0;height:56px}.fixed-action-btn.toolbar.active>a i{opacity:0}.fixed-action-btn.toolbar ul{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;top:0;bottom:0;z-index:1}.fixed-action-btn.toolbar ul li{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;display:inline-block;margin:0;height:100%;-webkit-transition:none;transition:none}.fixed-action-btn.toolbar ul li a{display:block;overflow:hidden;position:relative;width:100%;height:100%;background-color:transparent;-webkit-box-shadow:none;box-shadow:none;color:#fff;line-height:56px;z-index:1}.fixed-action-btn.toolbar ul li a i{line-height:inherit}.fixed-action-btn ul{left:0;right:0;text-align:center;position:absolute;bottom:64px;margin:0;visibility:hidden}.fixed-action-btn ul li{margin-bottom:15px}.fixed-action-btn ul a.btn-floating{opacity:0}.fixed-action-btn .fab-backdrop{position:absolute;top:0;left:0;z-index:-1;width:40px;height:40px;background-color:#26a69a;border-radius:50%;-webkit-transform:scale(0);transform:scale(0)}.btn-flat{-webkit-box-shadow:none;box-shadow:none;background-color:transparent;color:#343434;cursor:pointer;-webkit-transition:background-color .2s;transition:background-color .2s}.btn-flat:focus,.btn-flat:hover{-webkit-box-shadow:none;box-shadow:none}.btn-flat:focus{background-color:rgba(0,0,0,0.1)}.btn-flat.disabled,.btn-flat.btn-flat[disabled]{background-color:transparent !important;color:#b3b2b2 !important;cursor:default}.btn-large{height:54px;line-height:54px;font-size:15px;padding:0 28px}.btn-large i{font-size:1.6rem}.btn-small{height:32.4px;line-height:32.4px;font-size:13px}.btn-small i{font-size:1.2rem}.btn-block{display:block}.dropdown-content{background-color:#fff;margin:0;display:none;min-width:100px;overflow-y:auto;opacity:0;position:absolute;left:0;top:0;z-index:9999;-webkit-transform-origin:0 0;transform-origin:0 0}.dropdown-content:focus{outline:0}.dropdown-content li{clear:both;color:rgba(0,0,0,0.87);cursor:pointer;min-height:50px;line-height:1.5rem;width:100%;text-align:left}.dropdown-content li:hover,.dropdown-content li.active{background-color:#eee}.dropdown-content li:focus{outline:none}.dropdown-content li.divider{min-height:0;height:1px}.dropdown-content li>a,.dropdown-content li>span{font-size:16px;color:#26a69a;display:block;line-height:22px;padding:14px 16px}.dropdown-content li>span>label{top:1px;left:0;height:18px}.dropdown-content li>a>i{height:inherit;line-height:inherit;float:left;margin:0 24px 0 0;width:24px}body.keyboard-focused .dropdown-content li:focus{background-color:#dadada}.input-field.col .dropdown-content [type="checkbox"]+label{top:1px;left:0;height:18px;-webkit-transform:none;transform:none}.dropdown-trigger{cursor:pointer}/*! + * Waves v0.6.0 + * http://fian.my.id/Waves + * + * Copyright 2014 Alfiana E. Sibuea and other contributors + * Released under the MIT license + * https://github.com/fians/Waves/blob/master/LICENSE + */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;-webkit-transition:.3s ease-out;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);-webkit-transition:all 0.7s ease-out;transition:all 0.7s ease-out;-webkit-transition-property:opacity, -webkit-transform;transition-property:opacity, -webkit-transform;transition-property:transform, opacity;transition-property:transform, opacity, -webkit-transform;-webkit-transform:scale(0);transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{-webkit-transition:none !important;transition:none !important}.waves-circle{-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width: 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;-webkit-box-sizing:border-box;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;-webkit-box-shadow:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;-webkit-box-shadow:none;box-shadow:none}.collapsible.popout>li{-webkit-box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;-webkit-transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{-webkit-box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;-webkit-box-shadow:none;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;-webkit-transition:all .3s;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;-webkit-box-shadow:none !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix ~ .chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty ~ label{font-size:0.8rem;-webkit-transform:translateY(-140%);transform:translateY(-140%)}.materialboxed{display:block;cursor:-webkit-zoom-in;cursor:zoom-in;position:relative;-webkit-transition:opacity .4s;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:-webkit-zoom-out;cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#9e9e9e}::-webkit-input-placeholder{color:#d1d1d1}::-moz-placeholder{color:#d1d1d1}:-ms-input-placeholder{color:#d1d1d1}::-ms-input-placeholder{color:#d1d1d1}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-transition:border .3s, -webkit-box-shadow .3s;transition:border .3s, -webkit-box-shadow .3s;transition:box-shadow .3s, border .3s;transition:box-shadow .3s, border .3s, -webkit-box-shadow .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;-webkit-box-shadow:0 1px 0 0 #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid ~ label,input[type=text]:not(.browser-default):focus.valid ~ label,input[type=password]:not(.browser-default):focus.valid ~ label,input[type=email]:not(.browser-default):focus.valid ~ label,input[type=url]:not(.browser-default):focus.valid ~ label,input[type=time]:not(.browser-default):focus.valid ~ label,input[type=date]:not(.browser-default):focus.valid ~ label,input[type=datetime]:not(.browser-default):focus.valid ~ label,input[type=datetime-local]:not(.browser-default):focus.valid ~ label,input[type=tel]:not(.browser-default):focus.valid ~ label,input[type=number]:not(.browser-default):focus.valid ~ label,input[type=search]:not(.browser-default):focus.valid ~ label,textarea.materialize-textarea:focus.valid ~ label{color:#4CAF50}input:not([type]):focus.invalid ~ label,input[type=text]:not(.browser-default):focus.invalid ~ label,input[type=password]:not(.browser-default):focus.invalid ~ label,input[type=email]:not(.browser-default):focus.invalid ~ label,input[type=url]:not(.browser-default):focus.invalid ~ label,input[type=time]:not(.browser-default):focus.invalid ~ label,input[type=date]:not(.browser-default):focus.invalid ~ label,input[type=datetime]:not(.browser-default):focus.invalid ~ label,input[type=datetime-local]:not(.browser-default):focus.invalid ~ label,input[type=tel]:not(.browser-default):focus.invalid ~ label,input[type=number]:not(.browser-default):focus.invalid ~ label,input[type=search]:not(.browser-default):focus.invalid ~ label,textarea.materialize-textarea:focus.invalid ~ label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}input.valid:not([type]),input.valid:not([type]):focus,input.valid[type=text]:not(.browser-default),input.valid[type=text]:not(.browser-default):focus,input.valid[type=password]:not(.browser-default),input.valid[type=password]:not(.browser-default):focus,input.valid[type=email]:not(.browser-default),input.valid[type=email]:not(.browser-default):focus,input.valid[type=url]:not(.browser-default),input.valid[type=url]:not(.browser-default):focus,input.valid[type=time]:not(.browser-default),input.valid[type=time]:not(.browser-default):focus,input.valid[type=date]:not(.browser-default),input.valid[type=date]:not(.browser-default):focus,input.valid[type=datetime]:not(.browser-default),input.valid[type=datetime]:not(.browser-default):focus,input.valid[type=datetime-local]:not(.browser-default),input.valid[type=datetime-local]:not(.browser-default):focus,input.valid[type=tel]:not(.browser-default),input.valid[type=tel]:not(.browser-default):focus,input.valid[type=number]:not(.browser-default),input.valid[type=number]:not(.browser-default):focus,input.valid[type=search]:not(.browser-default),input.valid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus,.select-wrapper.valid>input.select-dropdown{border-bottom:1px solid #4CAF50;-webkit-box-shadow:0 1px 0 0 #4CAF50;box-shadow:0 1px 0 0 #4CAF50}input.invalid:not([type]),input.invalid:not([type]):focus,input.invalid[type=text]:not(.browser-default),input.invalid[type=text]:not(.browser-default):focus,input.invalid[type=password]:not(.browser-default),input.invalid[type=password]:not(.browser-default):focus,input.invalid[type=email]:not(.browser-default),input.invalid[type=email]:not(.browser-default):focus,input.invalid[type=url]:not(.browser-default),input.invalid[type=url]:not(.browser-default):focus,input.invalid[type=time]:not(.browser-default),input.invalid[type=time]:not(.browser-default):focus,input.invalid[type=date]:not(.browser-default),input.invalid[type=date]:not(.browser-default):focus,input.invalid[type=datetime]:not(.browser-default),input.invalid[type=datetime]:not(.browser-default):focus,input.invalid[type=datetime-local]:not(.browser-default),input.invalid[type=datetime-local]:not(.browser-default):focus,input.invalid[type=tel]:not(.browser-default),input.invalid[type=tel]:not(.browser-default):focus,input.invalid[type=number]:not(.browser-default),input.invalid[type=number]:not(.browser-default):focus,input.invalid[type=search]:not(.browser-default),input.invalid[type=search]:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus,.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus{border-bottom:1px solid #F44336;-webkit-box-shadow:0 1px 0 0 #F44336;box-shadow:0 1px 0 0 #F44336}input:not([type]).valid ~ .helper-text[data-success],input:not([type]):focus.valid ~ .helper-text[data-success],input:not([type]).invalid ~ .helper-text[data-error],input:not([type]):focus.invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default).valid ~ .helper-text[data-success],input[type=text]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=text]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=text]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default).valid ~ .helper-text[data-success],input[type=password]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=password]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=password]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default).valid ~ .helper-text[data-success],input[type=email]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=email]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=email]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default).valid ~ .helper-text[data-success],input[type=url]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=url]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=url]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default).valid ~ .helper-text[data-success],input[type=time]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=time]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=time]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default).valid ~ .helper-text[data-success],input[type=date]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=date]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=date]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default).valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default).valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=tel]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default).valid ~ .helper-text[data-success],input[type=number]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=number]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=number]:not(.browser-default):focus.invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default).valid ~ .helper-text[data-success],input[type=search]:not(.browser-default):focus.valid ~ .helper-text[data-success],input[type=search]:not(.browser-default).invalid ~ .helper-text[data-error],input[type=search]:not(.browser-default):focus.invalid ~ .helper-text[data-error],textarea.materialize-textarea.valid ~ .helper-text[data-success],textarea.materialize-textarea:focus.valid ~ .helper-text[data-success],textarea.materialize-textarea.invalid ~ .helper-text[data-error],textarea.materialize-textarea:focus.invalid ~ .helper-text[data-error],.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid ~ .helper-text[data-error]{color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}input:not([type]).valid ~ .helper-text:after,input:not([type]):focus.valid ~ .helper-text:after,input[type=text]:not(.browser-default).valid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=password]:not(.browser-default).valid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=email]:not(.browser-default).valid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=url]:not(.browser-default).valid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=time]:not(.browser-default).valid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=date]:not(.browser-default).valid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).valid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=tel]:not(.browser-default).valid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=number]:not(.browser-default).valid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.valid ~ .helper-text:after,input[type=search]:not(.browser-default).valid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.valid ~ .helper-text:after,textarea.materialize-textarea.valid ~ .helper-text:after,textarea.materialize-textarea:focus.valid ~ .helper-text:after,.select-wrapper.valid ~ .helper-text:after{content:attr(data-success);color:#4CAF50}input:not([type]).invalid ~ .helper-text:after,input:not([type]):focus.invalid ~ .helper-text:after,input[type=text]:not(.browser-default).invalid ~ .helper-text:after,input[type=text]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=password]:not(.browser-default).invalid ~ .helper-text:after,input[type=password]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=email]:not(.browser-default).invalid ~ .helper-text:after,input[type=email]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=url]:not(.browser-default).invalid ~ .helper-text:after,input[type=url]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=time]:not(.browser-default).invalid ~ .helper-text:after,input[type=time]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=date]:not(.browser-default).invalid ~ .helper-text:after,input[type=date]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default).invalid ~ .helper-text:after,input[type=datetime-local]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=tel]:not(.browser-default).invalid ~ .helper-text:after,input[type=tel]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=number]:not(.browser-default).invalid ~ .helper-text:after,input[type=number]:not(.browser-default):focus.invalid ~ .helper-text:after,input[type=search]:not(.browser-default).invalid ~ .helper-text:after,input[type=search]:not(.browser-default):focus.invalid ~ .helper-text:after,textarea.materialize-textarea.invalid ~ .helper-text:after,textarea.materialize-textarea:focus.invalid ~ .helper-text:after,.select-wrapper.invalid ~ .helper-text:after{content:attr(data-error);color:#F44336}input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after,.select-wrapper+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;-webkit-transition:.2s opacity ease-out, .2s color ease-out;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix ~ label,.input-field.col .prefix ~ .validate ~ label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#9e9e9e;position:absolute;top:0;left:0;font-size:1rem;cursor:text;-webkit-transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:color .2s ease-out, -webkit-transform .2s ease-out;transition:transform .2s ease-out, color .2s ease-out;transition:transform .2s ease-out, color .2s ease-out, -webkit-transform .2s ease-out;-webkit-transform-origin:0% 100%;transform-origin:0% 100%;text-align:initial;-webkit-transform:translateY(12px);transform:translateY(12px)}.input-field>label:not(.label-icon).active{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{-webkit-transform:translateY(-14px) scale(0.8);transform:translateY(-14px) scale(0.8);-webkit-transform-origin:0 0;transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;-webkit-transition:color .2s;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix ~ input,.input-field .prefix ~ textarea,.input-field .prefix ~ label,.input-field .prefix ~ .validate ~ label,.input-field .prefix ~ .helper-text,.input-field .prefix ~ .autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix ~ label{margin-left:3rem}@media only screen and (max-width: 992px){.input-field .prefix ~ input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width: 600px){.input-field .prefix ~ input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;-webkit-transition:.3s background-color;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;-webkit-box-shadow:none;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;-webkit-box-shadow:none;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default) ~ .mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default) ~ .material-icons{color:#444}.input-field input[type=search]+.label-icon{-webkit-transform:none;transform:none;left:1rem}.input-field input[type=search] ~ .mdi-navigation-close,.input-field input[type=search] ~ .material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;-webkit-transition:.3s color;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;-webkit-box-sizing:border-box;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-transition:.28s ease;transition:.28s ease;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;-webkit-transition:.28s ease;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{-webkit-transform:scale(0);transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{-webkit-transform:scale(1.02);transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{-webkit-transform:scale(0.5);transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;-webkit-transition:.2s;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;-webkit-transform:scale(0);transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{-webkit-transform:scale(1);transform:scale(1);border:0;border-radius:50%;-webkit-box-shadow:0 0 0 10px rgba(0,0,0,0.1);box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;-webkit-transform:rotate(40deg);transform:rotate(40deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;-webkit-transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;-webkit-transform:rotateZ(37deg);transform:rotateZ(37deg);-webkit-transform-origin:100% 100%;transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;-webkit-transition:background 0.3s ease;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;-webkit-transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;-webkit-box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12);box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled) ~ .lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before{-webkit-transform:scale(2.4);transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix ~ .select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix ~ label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup ~ li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;-webkit-transform:rotate(45deg);transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;-webkit-transform-origin:50% 50%;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 10px rgba(38,166,154,0.26);box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;-webkit-transition:-webkit-box-shadow .3s;transition:-webkit-box-shadow .3s;transition:box-shadow .3s;transition:box-shadow .3s, -webkit-box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;-webkit-transform:translateX(-100%);transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.right-aligned{right:0;-webkit-transform:translateX(105%);transform:translateX(105%);left:auto;-webkit-transform:translateX(100%);transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-large,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-large:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;-webkit-transform:translateX(0);transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width: 992px){.sidenav.sidenav-fixed{-webkit-transform:translateX(-105%);transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{-webkit-transform:translateX(105%);transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;-webkit-perspective:500px;perspective:500px;-webkit-transform-style:preserve-3d;transform-style:preserve-3d;-webkit-transform-origin:0% 50%;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:rgba(255,255,255,0.5);-webkit-transition:background-color .3s;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;-webkit-transition:visibility 0s .3s;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;-webkit-transition:visibility 0s;transition:visibility 0s}.tap-target-wrapper.open .tap-target{-webkit-transform:scale(1);transform:scale(1);opacity:.95;-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{-webkit-transform:scale(1);transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;-webkit-transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, visibility 0s 1s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s 1s;transition:opacity .3s, transform .3s, visibility 0s 1s, -webkit-transform .3s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;-webkit-box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1),-webkit-transform 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s, -webkit-transform .3s}.tap-target-wave::after{visibility:hidden;-webkit-transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, visibility 0s, -webkit-transform .3s;transition:opacity .3s, transform .3s, visibility 0s;transition:opacity .3s, transform .3s, visibility 0s, -webkit-transform .3s;z-index:-1}.tap-target-origin{top:50%;left:50%;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;-webkit-transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, -webkit-transform .3s;transition:opacity .3s, transform .3s;transition:opacity .3s, transform .3s, -webkit-transform .3s;-webkit-animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@-webkit-keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}@keyframes pulse-animation{0%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}50%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}100%{opacity:0;-webkit-transform:scale(1.5);transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.datepicker-controls{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width: 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.datepicker-date-display{-webkit-box-flex:0;-webkit-flex:0 1 270px;-ms-flex:0 1 270px;flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{-webkit-box-flex:1;-webkit-flex:1 auto;-ms-flex:1 auto;flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{-webkit-box-flex:2.5;-webkit-flex:2.5 auto;-ms-flex:2.5 auto;flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{-webkit-transition:opacity 350ms, -webkit-transform 350ms;transition:opacity 350ms, -webkit-transform 350ms;transition:transform 350ms, opacity 350ms;transition:transform 350ms, opacity 350ms, -webkit-transform 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{-webkit-transform:scale(1.1, 1.1);transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{-webkit-transform:scale(0.8, 0.8);transform:scale(0.8, 0.8)}.timepicker-canvas{-webkit-transition:opacity 175ms;transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width: 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}} diff --git a/app/static/img/FamilyMovingIn.png b/app/static/img/FamilyMovingIn.png new file mode 100644 index 0000000..8f763e9 Binary files /dev/null and b/app/static/img/FamilyMovingIn.png differ diff --git a/app/static/img/Quarterhome Logo 300x86 WHITE.png b/app/static/img/Quarterhome Logo 300x86 WHITE.png new file mode 100644 index 0000000..5b83c71 Binary files /dev/null and b/app/static/img/Quarterhome Logo 300x86 WHITE.png differ diff --git a/app/static/img/Quarterhome Logo 300x86.png b/app/static/img/Quarterhome Logo 300x86.png new file mode 100644 index 0000000..07045c2 Binary files /dev/null and b/app/static/img/Quarterhome Logo 300x86.png differ diff --git a/app/static/img/Quarterhome Logo 500x144.png b/app/static/img/Quarterhome Logo 500x144.png new file mode 100644 index 0000000..03a9e30 Binary files /dev/null and b/app/static/img/Quarterhome Logo 500x144.png differ diff --git a/app/static/img/aaron_420w.png b/app/static/img/aaron_420w.png new file mode 100644 index 0000000..2c13b1c Binary files /dev/null and b/app/static/img/aaron_420w.png differ diff --git a/app/static/img/alphatest.webm b/app/static/img/alphatest.webm new file mode 100644 index 0000000..1789dfc Binary files /dev/null and b/app/static/img/alphatest.webm differ diff --git a/app/static/img/animlogo.mp4 b/app/static/img/animlogo.mp4 new file mode 100644 index 0000000..4760b93 Binary files /dev/null and b/app/static/img/animlogo.mp4 differ diff --git a/app/static/img/favicon.ico b/app/static/img/favicon.ico new file mode 100644 index 0000000..ac440ab Binary files /dev/null and b/app/static/img/favicon.ico differ diff --git a/app/static/img/plus.png b/app/static/img/plus.png new file mode 100644 index 0000000..3742439 Binary files /dev/null and b/app/static/img/plus.png differ diff --git a/app/static/img/plus_30.png b/app/static/img/plus_30.png new file mode 100644 index 0000000..4963b6d Binary files /dev/null and b/app/static/img/plus_30.png differ diff --git a/app/static/img/slide1.jpg b/app/static/img/slide1.jpg new file mode 100644 index 0000000..9fea729 Binary files /dev/null and b/app/static/img/slide1.jpg differ diff --git a/app/static/js/helpers.js b/app/static/js/helpers.js new file mode 100644 index 0000000..9a20d10 --- /dev/null +++ b/app/static/js/helpers.js @@ -0,0 +1,13 @@ + + +function request_with_jwt(url, method, callback, current_jwt) { + $.ajax({ + url: url, + type: method, + headers: { + 'Auth-Token': current_jwt + }, + success: callback + }); +} + diff --git a/app/static/js/jquery-3.5.1.min.js b/app/static/js/jquery-3.5.1.min.js new file mode 100644 index 0000000..b061403 --- /dev/null +++ b/app/static/js/jquery-3.5.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0/,p=/^\w+$/;function v(t,e){e=e||o;var i=u.test(t)?e.getElementsByClassName(t.slice(1)):p.test(t)?e.getElementsByTagName(t):e.querySelectorAll(t);return i}function f(t){if(!i){var e=(i=o.implementation.createHTMLDocument(null)).createElement("base");e.href=o.location.href,i.head.appendChild(e)}return i.body.innerHTML=t,i.body.childNodes}function m(t){"loading"!==o.readyState?t():o.addEventListener("DOMContentLoaded",t)}function g(t,e){if(!t)return this;if(t.cash&&t!==a)return t;var i,n=t,s=0;if(d(t))n=l.test(t)?o.getElementById(t.slice(1)):c.test(t)?f(t):v(t,e);else if(h(t))return m(t),this;if(!n)return this;if(n.nodeType||n===a)this[0]=n,this.length=1;else for(i=this.length=n.length;ss.right-i||l+e.width>window.innerWidth-i)&&(n.right=!0),(ho-i||h+e.height>window.innerHeight-i)&&(n.bottom=!0),n},M.checkPossibleAlignments=function(t,e,i,n){var s={top:!0,right:!0,bottom:!0,left:!0,spaceOnTop:null,spaceOnRight:null,spaceOnBottom:null,spaceOnLeft:null},o="visible"===getComputedStyle(e).overflow,a=e.getBoundingClientRect(),r=Math.min(a.height,window.innerHeight),l=Math.min(a.width,window.innerWidth),h=t.getBoundingClientRect(),d=e.scrollLeft,u=e.scrollTop,c=i.left-d,p=i.top-u,v=i.top+h.height-u;return s.spaceOnRight=o?window.innerWidth-(h.left+i.width):l-(c+i.width),s.spaceOnRight<0&&(s.left=!1),s.spaceOnLeft=o?h.right-i.width:c-i.width+h.width,s.spaceOnLeft<0&&(s.right=!1),s.spaceOnBottom=o?window.innerHeight-(h.top+i.height+n):r-(p+i.height+n),s.spaceOnBottom<0&&(s.top=!1),s.spaceOnTop=o?h.bottom-(i.height+n):v-(i.height-n),s.spaceOnTop<0&&(s.bottom=!1),s},M.getOverflowParent=function(t){return null==t?null:t===document.body||"visible"!==getComputedStyle(t).overflow?t:M.getOverflowParent(t.parentElement)},M.getIdFromTrigger=function(t){var e=t.getAttribute("data-target");return e||(e=(e=t.getAttribute("href"))?e.slice(1):""),e},M.getDocumentScrollTop=function(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},M.getDocumentScrollLeft=function(){return window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0};var getTime=Date.now||function(){return(new Date).getTime()};M.throttle=function(i,n,s){var o=void 0,a=void 0,r=void 0,l=null,h=0;s||(s={});var d=function(){h=!1===s.leading?0:getTime(),l=null,r=i.apply(o,a),o=a=null};return function(){var t=getTime();h||!1!==s.leading||(h=t);var e=n-(t-h);return o=this,a=arguments,e<=0?(clearTimeout(l),l=null,h=t,r=i.apply(o,a),o=a=null):l||!1===s.trailing||(l=setTimeout(d,e)),r}};var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(t,e,i){if(i.get||i.set)throw new TypeError("ES3 does not support getters and setters.");t!=Array.prototype&&t!=Object.prototype&&(t[e]=i.value)},$jscomp.getGlobal=function(t){return"undefined"!=typeof window&&window===t?t:"undefined"!=typeof global&&null!=global?global:t},$jscomp.global=$jscomp.getGlobal(this),$jscomp.SYMBOL_PREFIX="jscomp_symbol_",$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){},$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)},$jscomp.symbolCounter_=0,$jscomp.Symbol=function(t){return $jscomp.SYMBOL_PREFIX+(t||"")+$jscomp.symbolCounter_++},$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var t=$jscomp.global.Symbol.iterator;t||(t=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator")),"function"!=typeof Array.prototype[t]&&$jscomp.defineProperty(Array.prototype,t,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}}),$jscomp.initSymbolIterator=function(){}},$jscomp.arrayIterator=function(t){var e=0;return $jscomp.iteratorPrototype(function(){return e=k.currentTime)for(var h=0;ht&&(s.duration=e.duration),s.children.push(e)}),s.seek(0),s.reset(),s.autoplay&&s.restart(),s},s},O.random=function(t,e){return Math.floor(Math.random()*(e-t+1))+t},O}(),function(r,l){"use strict";var e={accordion:!0,onOpenStart:void 0,onOpenEnd:void 0,onCloseStart:void 0,onCloseEnd:void 0,inDuration:300,outDuration:300},t=function(t){function s(t,e){_classCallCheck(this,s);var i=_possibleConstructorReturn(this,(s.__proto__||Object.getPrototypeOf(s)).call(this,s,t,e));(i.el.M_Collapsible=i).options=r.extend({},s.defaults,e),i.$headers=i.$el.children("li").children(".collapsible-header"),i.$headers.attr("tabindex",0),i._setupEventHandlers();var n=i.$el.children("li.active").children(".collapsible-body");return i.options.accordion?n.first().css("display","block"):n.css("display","block"),i}return _inherits(s,Component),_createClass(s,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Collapsible=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleCollapsibleClickBound=this._handleCollapsibleClick.bind(this),this._handleCollapsibleKeydownBound=this._handleCollapsibleKeydown.bind(this),this.el.addEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.addEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_removeEventHandlers",value:function(){var e=this;this.el.removeEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.removeEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_handleCollapsibleClick",value:function(t){var e=r(t.target).closest(".collapsible-header");if(t.target&&e.length){var i=e.closest(".collapsible");if(i[0]===this.el){var n=e.closest("li"),s=i.children("li"),o=n[0].classList.contains("active"),a=s.index(n);o?this.close(a):this.open(a)}}}},{key:"_handleCollapsibleKeydown",value:function(t){13===t.keyCode&&this._handleCollapsibleClickBound(t)}},{key:"_animateIn",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css({display:"block",overflow:"hidden",height:0,paddingTop:"",paddingBottom:""});var s=n.css("padding-top"),o=n.css("padding-bottom"),a=n[0].scrollHeight;n.css({paddingTop:0,paddingBottom:0}),l({targets:n[0],height:a,paddingTop:s,paddingBottom:o,duration:this.options.inDuration,easing:"easeInOutCubic",complete:function(t){n.css({overflow:"",paddingTop:"",paddingBottom:"",height:""}),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,i[0])}})}}},{key:"_animateOut",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css("overflow","hidden"),l({targets:n[0],height:0,paddingTop:0,paddingBottom:0,duration:this.options.outDuration,easing:"easeInOutCubic",complete:function(){n.css({height:"",overflow:"",padding:"",display:""}),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,i[0])}})}}},{key:"open",value:function(t){var i=this,e=this.$el.children("li").eq(t);if(e.length&&!e[0].classList.contains("active")){if("function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,e[0]),this.options.accordion){var n=this.$el.children("li");this.$el.children("li.active").each(function(t){var e=n.index(r(t));i.close(e)})}e[0].classList.add("active"),this._animateIn(t)}}},{key:"close",value:function(t){var e=this.$el.children("li").eq(t);e.length&&e[0].classList.contains("active")&&("function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,e[0]),e[0].classList.remove("active"),this._animateOut(t))}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Collapsible}},{key:"defaults",get:function(){return e}}]),s}();M.Collapsible=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"collapsible","M_Collapsible")}(cash,M.anime),function(h,i){"use strict";var e={alignment:"left",autoFocus:!0,constrainWidth:!0,container:null,coverTrigger:!0,closeOnClick:!0,hover:!1,inDuration:150,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onItemClick:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return i.el.M_Dropdown=i,n._dropdowns.push(i),i.id=M.getIdFromTrigger(t),i.dropdownEl=document.getElementById(i.id),i.$dropdownEl=h(i.dropdownEl),i.options=h.extend({},n.defaults,e),i.isOpen=!1,i.isScrollable=!1,i.isTouchMoving=!1,i.focusedIndex=-1,i.filterQuery=[],i.options.container?h(i.options.container).append(i.dropdownEl):i.$el.after(i.dropdownEl),i._makeDropdownFocusable(),i._resetFilterQueryBound=i._resetFilterQuery.bind(i),i._handleDocumentClickBound=i._handleDocumentClick.bind(i),i._handleDocumentTouchmoveBound=i._handleDocumentTouchmove.bind(i),i._handleDropdownClickBound=i._handleDropdownClick.bind(i),i._handleDropdownKeydownBound=i._handleDropdownKeydown.bind(i),i._handleTriggerKeydownBound=i._handleTriggerKeydown.bind(i),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._resetDropdownStyles(),this._removeEventHandlers(),n._dropdowns.splice(n._dropdowns.indexOf(this),1),this.el.M_Dropdown=void 0}},{key:"_setupEventHandlers",value:function(){this.el.addEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.addEventListener("click",this._handleDropdownClickBound),this.options.hover?(this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.addEventListener("mouseleave",this._handleMouseLeaveBound)):(this._handleClickBound=this._handleClick.bind(this),this.el.addEventListener("click",this._handleClickBound))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.removeEventListener("click",this._handleDropdownClickBound),this.options.hover?(this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.removeEventListener("mouseleave",this._handleMouseLeaveBound)):this.el.removeEventListener("click",this._handleClickBound)}},{key:"_setupTemporaryEventHandlers",value:function(){document.body.addEventListener("click",this._handleDocumentClickBound,!0),document.body.addEventListener("touchend",this._handleDocumentClickBound),document.body.addEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.addEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_removeTemporaryEventHandlers",value:function(){document.body.removeEventListener("click",this._handleDocumentClickBound,!0),document.body.removeEventListener("touchend",this._handleDocumentClickBound),document.body.removeEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.removeEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_handleClick",value:function(t){t.preventDefault(),this.open()}},{key:"_handleMouseEnter",value:function(){this.open()}},{key:"_handleMouseLeave",value:function(t){var e=t.toElement||t.relatedTarget,i=!!h(e).closest(".dropdown-content").length,n=!1,s=h(e).closest(".dropdown-trigger");s.length&&s[0].M_Dropdown&&s[0].M_Dropdown.isOpen&&(n=!0),n||i||this.close()}},{key:"_handleDocumentClick",value:function(t){var e=this,i=h(t.target);this.options.closeOnClick&&i.closest(".dropdown-content").length&&!this.isTouchMoving?setTimeout(function(){e.close()},0):!i.closest(".dropdown-trigger").length&&i.closest(".dropdown-content").length||setTimeout(function(){e.close()},0),this.isTouchMoving=!1}},{key:"_handleTriggerKeydown",value:function(t){t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ENTER||this.isOpen||(t.preventDefault(),this.open())}},{key:"_handleDocumentTouchmove",value:function(t){h(t.target).closest(".dropdown-content").length&&(this.isTouchMoving=!0)}},{key:"_handleDropdownClick",value:function(t){if("function"==typeof this.options.onItemClick){var e=h(t.target).closest("li")[0];this.options.onItemClick.call(this,e)}}},{key:"_handleDropdownKeydown",value:function(t){if(t.which===M.keys.TAB)t.preventDefault(),this.close();else if(t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||!this.isOpen)if(t.which===M.keys.ENTER&&this.isOpen){var e=this.dropdownEl.children[this.focusedIndex],i=h(e).find("a, button").first();i.length?i[0].click():e&&e.click()}else t.which===M.keys.ESC&&this.isOpen&&(t.preventDefault(),this.close());else{t.preventDefault();var n=t.which===M.keys.ARROW_DOWN?1:-1,s=this.focusedIndex,o=!1;do{if(s+=n,this.dropdownEl.children[s]&&-1!==this.dropdownEl.children[s].tabIndex){o=!0;break}}while(sl.spaceOnBottom?(h="bottom",i+=l.spaceOnTop,o-=l.spaceOnTop):i+=l.spaceOnBottom)),!l[d]){var u="left"===d?"right":"left";l[u]?d=u:l.spaceOnLeft>l.spaceOnRight?(d="right",n+=l.spaceOnLeft,s-=l.spaceOnLeft):(d="left",n+=l.spaceOnRight)}return"bottom"===h&&(o=o-e.height+(this.options.coverTrigger?t.height:0)),"right"===d&&(s=s-e.width+t.width),{x:s,y:o,verticalAlignment:h,horizontalAlignment:d,height:i,width:n}}},{key:"_animateIn",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:[0,1],easing:"easeOutQuad"},scaleX:[.3,1],scaleY:[.3,1],duration:this.options.inDuration,easing:"easeOutQuint",complete:function(t){e.options.autoFocus&&e.dropdownEl.focus(),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,e.el)}})}},{key:"_animateOut",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:0,easing:"easeOutQuint"},scaleX:.3,scaleY:.3,duration:this.options.outDuration,easing:"easeOutQuint",complete:function(t){e._resetDropdownStyles(),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,e.el)}})}},{key:"_placeDropdown",value:function(){var t=this.options.constrainWidth?this.el.getBoundingClientRect().width:this.dropdownEl.getBoundingClientRect().width;this.dropdownEl.style.width=t+"px";var e=this._getDropdownPosition();this.dropdownEl.style.left=e.x+"px",this.dropdownEl.style.top=e.y+"px",this.dropdownEl.style.height=e.height+"px",this.dropdownEl.style.width=e.width+"px",this.dropdownEl.style.transformOrigin=("left"===e.horizontalAlignment?"0":"100%")+" "+("top"===e.verticalAlignment?"0":"100%")}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._resetDropdownStyles(),this.dropdownEl.style.display="block",this._placeDropdown(),this._animateIn(),this._setupTemporaryEventHandlers())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.focusedIndex=-1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._animateOut(),this._removeTemporaryEventHandlers(),this.options.autoFocus&&this.el.focus())}},{key:"recalculateDimensions",value:function(){this.isOpen&&(this.$dropdownEl.css({width:"",height:"",left:"",top:"","transform-origin":""}),this._placeDropdown())}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Dropdown}},{key:"defaults",get:function(){return e}}]),n}();t._dropdowns=[],M.Dropdown=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"dropdown","M_Dropdown")}(cash,M.anime),function(s,i){"use strict";var e={opacity:.5,inDuration:250,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0,dismissible:!0,startingTop:"4%",endingTop:"10%"},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Modal=i).options=s.extend({},n.defaults,e),i.isOpen=!1,i.id=i.$el.attr("id"),i._openingTrigger=void 0,i.$overlay=s(''),i.el.tabIndex=0,i._nthModalOpened=0,n._count++,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._count--,this._removeEventHandlers(),this.el.removeAttribute("style"),this.$overlay.remove(),this.el.M_Modal=void 0}},{key:"_setupEventHandlers",value:function(){this._handleOverlayClickBound=this._handleOverlayClick.bind(this),this._handleModalCloseClickBound=this._handleModalCloseClick.bind(this),1===n._count&&document.body.addEventListener("click",this._handleTriggerClick),this.$overlay[0].addEventListener("click",this._handleOverlayClickBound),this.el.addEventListener("click",this._handleModalCloseClickBound)}},{key:"_removeEventHandlers",value:function(){0===n._count&&document.body.removeEventListener("click",this._handleTriggerClick),this.$overlay[0].removeEventListener("click",this._handleOverlayClickBound),this.el.removeEventListener("click",this._handleModalCloseClickBound)}},{key:"_handleTriggerClick",value:function(t){var e=s(t.target).closest(".modal-trigger");if(e.length){var i=M.getIdFromTrigger(e[0]),n=document.getElementById(i).M_Modal;n&&n.open(e),t.preventDefault()}}},{key:"_handleOverlayClick",value:function(){this.options.dismissible&&this.close()}},{key:"_handleModalCloseClick",value:function(t){s(t.target).closest(".modal-close").length&&this.close()}},{key:"_handleKeydown",value:function(t){27===t.keyCode&&this.options.dismissible&&this.close()}},{key:"_handleFocus",value:function(t){this.el.contains(t.target)||this._nthModalOpened!==n._modalsOpen||this.el.focus()}},{key:"_animateIn",value:function(){var t=this;s.extend(this.el.style,{display:"block",opacity:0}),s.extend(this.$overlay[0].style,{display:"block",opacity:0}),i({targets:this.$overlay[0],opacity:this.options.opacity,duration:this.options.inDuration,easing:"easeOutQuad"});var e={targets:this.el,duration:this.options.inDuration,easing:"easeOutCubic",complete:function(){"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el,t._openingTrigger)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:0,opacity:1}):s.extend(e,{top:[this.options.startingTop,this.options.endingTop],opacity:1,scaleX:[.8,1],scaleY:[.8,1]}),i(e)}},{key:"_animateOut",value:function(){var t=this;i({targets:this.$overlay[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuart"});var e={targets:this.el,duration:this.options.outDuration,easing:"easeOutCubic",complete:function(){t.el.style.display="none",t.$overlay.remove(),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:"-100%",opacity:0}):s.extend(e,{top:[this.options.endingTop,this.options.startingTop],opacity:0,scaleX:.8,scaleY:.8}),i(e)}},{key:"open",value:function(t){if(!this.isOpen)return this.isOpen=!0,n._modalsOpen++,this._nthModalOpened=n._modalsOpen,this.$overlay[0].style.zIndex=1e3+2*n._modalsOpen,this.el.style.zIndex=1e3+2*n._modalsOpen+1,this._openingTrigger=t?t[0]:void 0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el,this._openingTrigger),this.options.preventScrolling&&(document.body.style.overflow="hidden"),this.el.classList.add("open"),this.el.insertAdjacentElement("afterend",this.$overlay[0]),this.options.dismissible&&(this._handleKeydownBound=this._handleKeydown.bind(this),this._handleFocusBound=this._handleFocus.bind(this),document.addEventListener("keydown",this._handleKeydownBound),document.addEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateIn(),this.el.focus(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,n._modalsOpen--,this._nthModalOpened=0,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this.el.classList.remove("open"),0===n._modalsOpen&&(document.body.style.overflow=""),this.options.dismissible&&(document.removeEventListener("keydown",this._handleKeydownBound),document.removeEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateOut(),this}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Modal}},{key:"defaults",get:function(){return e}}]),n}();t._modalsOpen=0,t._count=0,M.Modal=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"modal","M_Modal")}(cash,M.anime),function(o,a){"use strict";var e={inDuration:275,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Materialbox=i).options=o.extend({},n.defaults,e),i.overlayActive=!1,i.doneAnimating=!0,i.placeholder=o("
").addClass("material-placeholder"),i.originalWidth=0,i.originalHeight=0,i.originInlineStyles=i.$el.attr("style"),i.caption=i.el.getAttribute("data-caption")||"",i.$el.before(i.placeholder),i.placeholder.append(i.$el),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Materialbox=void 0,o(this.placeholder).after(this.el).remove(),this.$el.removeAttr("style")}},{key:"_setupEventHandlers",value:function(){this._handleMaterialboxClickBound=this._handleMaterialboxClick.bind(this),this.el.addEventListener("click",this._handleMaterialboxClickBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleMaterialboxClickBound)}},{key:"_handleMaterialboxClick",value:function(t){!1===this.doneAnimating||this.overlayActive&&this.doneAnimating?this.close():this.open()}},{key:"_handleWindowScroll",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowResize",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowEscape",value:function(t){27===t.keyCode&&this.doneAnimating&&this.overlayActive&&this.close()}},{key:"_makeAncestorsOverflowVisible",value:function(){this.ancestorsChanged=o();for(var t=this.placeholder[0].parentNode;null!==t&&!o(t).is(document);){var e=o(t);"visible"!==e.css("overflow")&&(e.css("overflow","visible"),void 0===this.ancestorsChanged?this.ancestorsChanged=e:this.ancestorsChanged=this.ancestorsChanged.add(e)),t=t.parentNode}}},{key:"_animateImageIn",value:function(){var t=this,e={targets:this.el,height:[this.originalHeight,this.newHeight],width:[this.originalWidth,this.newWidth],left:M.getDocumentScrollLeft()+this.windowWidth/2-this.placeholder.offset().left-this.newWidth/2,top:M.getDocumentScrollTop()+this.windowHeight/2-this.placeholder.offset().top-this.newHeight/2,duration:this.options.inDuration,easing:"easeOutQuad",complete:function(){t.doneAnimating=!0,"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el)}};this.maxWidth=this.$el.css("max-width"),this.maxHeight=this.$el.css("max-height"),"none"!==this.maxWidth&&(e.maxWidth=this.newWidth),"none"!==this.maxHeight&&(e.maxHeight=this.newHeight),a(e)}},{key:"_animateImageOut",value:function(){var t=this,e={targets:this.el,width:this.originalWidth,height:this.originalHeight,left:0,top:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.placeholder.css({height:"",width:"",position:"",top:"",left:""}),t.attrWidth&&t.$el.attr("width",t.attrWidth),t.attrHeight&&t.$el.attr("height",t.attrHeight),t.$el.removeAttr("style"),t.originInlineStyles&&t.$el.attr("style",t.originInlineStyles),t.$el.removeClass("active"),t.doneAnimating=!0,t.ancestorsChanged.length&&t.ancestorsChanged.css("overflow",""),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};a(e)}},{key:"_updateVars",value:function(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight,this.caption=this.el.getAttribute("data-caption")||""}},{key:"open",value:function(){var t=this;this._updateVars(),this.originalWidth=this.el.getBoundingClientRect().width,this.originalHeight=this.el.getBoundingClientRect().height,this.doneAnimating=!1,this.$el.addClass("active"),this.overlayActive=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this.placeholder.css({width:this.placeholder[0].getBoundingClientRect().width+"px",height:this.placeholder[0].getBoundingClientRect().height+"px",position:"relative",top:0,left:0}),this._makeAncestorsOverflowVisible(),this.$el.css({position:"absolute","z-index":1e3,"will-change":"left, top, width, height"}),this.attrWidth=this.$el.attr("width"),this.attrHeight=this.$el.attr("height"),this.attrWidth&&(this.$el.css("width",this.attrWidth+"px"),this.$el.removeAttr("width")),this.attrHeight&&(this.$el.css("width",this.attrHeight+"px"),this.$el.removeAttr("height")),this.$overlay=o('
').css({opacity:0}).one("click",function(){t.doneAnimating&&t.close()}),this.$el.before(this.$overlay);var e=this.$overlay[0].getBoundingClientRect();this.$overlay.css({width:this.windowWidth+"px",height:this.windowHeight+"px",left:-1*e.left+"px",top:-1*e.top+"px"}),a.remove(this.el),a.remove(this.$overlay[0]),a({targets:this.$overlay[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}),""!==this.caption&&(this.$photocaption&&a.remove(this.$photoCaption[0]),this.$photoCaption=o('
'),this.$photoCaption.text(this.caption),o("body").append(this.$photoCaption),this.$photoCaption.css({display:"inline"}),a({targets:this.$photoCaption[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}));var i=0,n=this.originalWidth/this.windowWidth,s=this.originalHeight/this.windowHeight;this.newWidth=0,this.newHeight=0,si.options.responsiveThreshold,i.$img=i.$el.find("img").first(),i.$img.each(function(){this.complete&&s(this).trigger("load")}),i._updateParallax(),i._setupEventHandlers(),i._setupStyles(),n._parallaxes.push(i),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._parallaxes.splice(n._parallaxes.indexOf(this),1),this.$img[0].style.transform="",this._removeEventHandlers(),this.$el[0].M_Parallax=void 0}},{key:"_setupEventHandlers",value:function(){this._handleImageLoadBound=this._handleImageLoad.bind(this),this.$img[0].addEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(n._handleScrollThrottled=M.throttle(n._handleScroll,5),window.addEventListener("scroll",n._handleScrollThrottled),n._handleWindowResizeThrottled=M.throttle(n._handleWindowResize,5),window.addEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_removeEventHandlers",value:function(){this.$img[0].removeEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(window.removeEventListener("scroll",n._handleScrollThrottled),window.removeEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_setupStyles",value:function(){this.$img[0].style.opacity=1}},{key:"_handleImageLoad",value:function(){this._updateParallax()}},{key:"_updateParallax",value:function(){var t=0e.options.responsiveThreshold}}},{key:"defaults",get:function(){return e}}]),n}();t._parallaxes=[],M.Parallax=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"parallax","M_Parallax")}(cash),function(a,s){"use strict";var e={duration:300,onShow:null,swipeable:!1,responsiveThreshold:1/0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tabs=i).options=a.extend({},n.defaults,e),i.$tabLinks=i.$el.children("li.tab").children("a"),i.index=0,i._setupActiveTabLink(),i.options.swipeable?i._setupSwipeableTabs():i._setupNormalTabs(),i._setTabsAndTabWidth(),i._createIndicator(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._indicator.parentNode.removeChild(this._indicator),this.options.swipeable?this._teardownSwipeableTabs():this._teardownNormalTabs(),this.$el[0].M_Tabs=void 0}},{key:"_setupEventHandlers",value:function(){this._handleWindowResizeBound=this._handleWindowResize.bind(this),window.addEventListener("resize",this._handleWindowResizeBound),this._handleTabClickBound=this._handleTabClick.bind(this),this.el.addEventListener("click",this._handleTabClickBound)}},{key:"_removeEventHandlers",value:function(){window.removeEventListener("resize",this._handleWindowResizeBound),this.el.removeEventListener("click",this._handleTabClickBound)}},{key:"_handleWindowResize",value:function(){this._setTabsAndTabWidth(),0!==this.tabWidth&&0!==this.tabsWidth&&(this._indicator.style.left=this._calcLeftPos(this.$activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this.$activeTabLink)+"px")}},{key:"_handleTabClick",value:function(t){var e=this,i=a(t.target).closest("li.tab"),n=a(t.target).closest("a");if(n.length&&n.parent().hasClass("tab"))if(i.hasClass("disabled"))t.preventDefault();else if(!n.attr("target")){this.$activeTabLink.removeClass("active");var s=this.$content;this.$activeTabLink=n,this.$content=a(M.escapeHash(n[0].hash)),this.$tabLinks=this.$el.children("li.tab").children("a"),this.$activeTabLink.addClass("active");var o=this.index;this.index=Math.max(this.$tabLinks.index(n),0),this.options.swipeable?this._tabsCarousel&&this._tabsCarousel.set(this.index,function(){"function"==typeof e.options.onShow&&e.options.onShow.call(e,e.$content[0])}):this.$content.length&&(this.$content[0].style.display="block",this.$content.addClass("active"),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this.$content[0]),s.length&&!s.is(this.$content)&&(s[0].style.display="none",s.removeClass("active"))),this._setTabsAndTabWidth(),this._animateIndicator(o),t.preventDefault()}}},{key:"_createIndicator",value:function(){var t=this,e=document.createElement("li");e.classList.add("indicator"),this.el.appendChild(e),this._indicator=e,setTimeout(function(){t._indicator.style.left=t._calcLeftPos(t.$activeTabLink)+"px",t._indicator.style.right=t._calcRightPos(t.$activeTabLink)+"px"},0)}},{key:"_setupActiveTabLink",value:function(){this.$activeTabLink=a(this.$tabLinks.filter('[href="'+location.hash+'"]')),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a.active").first()),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a").first()),this.$tabLinks.removeClass("active"),this.$activeTabLink[0].classList.add("active"),this.index=Math.max(this.$tabLinks.index(this.$activeTabLink),0),this.$activeTabLink.length&&(this.$content=a(M.escapeHash(this.$activeTabLink[0].hash)),this.$content.addClass("active"))}},{key:"_setupSwipeableTabs",value:function(){var i=this;window.innerWidth>this.options.responsiveThreshold&&(this.options.swipeable=!1);var n=a();this.$tabLinks.each(function(t){var e=a(M.escapeHash(t.hash));e.addClass("carousel-item"),n=n.add(e)});var t=a('');n.first().before(t),t.append(n),n[0].style.display="";var e=this.$activeTabLink.closest(".tab").index();this._tabsCarousel=M.Carousel.init(t[0],{fullWidth:!0,noWrap:!0,onCycleTo:function(t){var e=i.index;i.index=a(t).index(),i.$activeTabLink.removeClass("active"),i.$activeTabLink=i.$tabLinks.eq(i.index),i.$activeTabLink.addClass("active"),i._animateIndicator(e),"function"==typeof i.options.onShow&&i.options.onShow.call(i,i.$content[0])}}),this._tabsCarousel.set(e)}},{key:"_teardownSwipeableTabs",value:function(){var t=this._tabsCarousel.$el;this._tabsCarousel.destroy(),t.after(t.children()),t.remove()}},{key:"_setupNormalTabs",value:function(){this.$tabLinks.not(this.$activeTabLink).each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="none")}})}},{key:"_teardownNormalTabs",value:function(){this.$tabLinks.each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="")}})}},{key:"_setTabsAndTabWidth",value:function(){this.tabsWidth=this.$el.width(),this.tabWidth=Math.max(this.tabsWidth,this.el.scrollWidth)/this.$tabLinks.length}},{key:"_calcRightPos",value:function(t){return Math.ceil(this.tabsWidth-t.position().left-t[0].getBoundingClientRect().width)}},{key:"_calcLeftPos",value:function(t){return Math.floor(t.position().left)}},{key:"updateTabIndicator",value:function(){this._setTabsAndTabWidth(),this._animateIndicator(this.index)}},{key:"_animateIndicator",value:function(t){var e=0,i=0;0<=this.index-t?e=90:i=90;var n={targets:this._indicator,left:{value:this._calcLeftPos(this.$activeTabLink),delay:e},right:{value:this._calcRightPos(this.$activeTabLink),delay:i},duration:this.options.duration,easing:"easeOutQuad"};s.remove(this._indicator),s(n)}},{key:"select",value:function(t){var e=this.$tabLinks.filter('[href="#'+t+'"]');e.length&&e.trigger("click")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tabs}},{key:"defaults",get:function(){return e}}]),n}();M.Tabs=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tabs","M_Tabs")}(cash,M.anime),function(d,e){"use strict";var i={exitDelay:200,enterDelay:0,html:null,margin:5,inDuration:250,outDuration:200,position:"bottom",transitionMovement:10},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tooltip=i).options=d.extend({},n.defaults,e),i.isOpen=!1,i.isHovered=!1,i.isFocused=!1,i._appendTooltipEl(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){d(this.tooltipEl).remove(),this._removeEventHandlers(),this.el.M_Tooltip=void 0}},{key:"_appendTooltipEl",value:function(){var t=document.createElement("div");t.classList.add("material-tooltip"),this.tooltipEl=t;var e=document.createElement("div");e.classList.add("tooltip-content"),e.innerHTML=this.options.html,t.appendChild(e),document.body.appendChild(t)}},{key:"_updateTooltipContent",value:function(){this.tooltipEl.querySelector(".tooltip-content").innerHTML=this.options.html}},{key:"_setupEventHandlers",value:function(){this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this._handleFocusBound=this._handleFocus.bind(this),this._handleBlurBound=this._handleBlur.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.el.addEventListener("focus",this._handleFocusBound,!0),this.el.addEventListener("blur",this._handleBlurBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.el.removeEventListener("focus",this._handleFocusBound,!0),this.el.removeEventListener("blur",this._handleBlurBound,!0)}},{key:"open",value:function(t){this.isOpen||(t=void 0===t||void 0,this.isOpen=!0,this.options=d.extend({},this.options,this._getAttributeOptions()),this._updateTooltipContent(),this._setEnterDelayTimeout(t))}},{key:"close",value:function(){this.isOpen&&(this.isHovered=!1,this.isFocused=!1,this.isOpen=!1,this._setExitDelayTimeout())}},{key:"_setExitDelayTimeout",value:function(){var t=this;clearTimeout(this._exitDelayTimeout),this._exitDelayTimeout=setTimeout(function(){t.isHovered||t.isFocused||t._animateOut()},this.options.exitDelay)}},{key:"_setEnterDelayTimeout",value:function(t){var e=this;clearTimeout(this._enterDelayTimeout),this._enterDelayTimeout=setTimeout(function(){(e.isHovered||e.isFocused||t)&&e._animateIn()},this.options.enterDelay)}},{key:"_positionTooltip",value:function(){var t,e=this.el,i=this.tooltipEl,n=e.offsetHeight,s=e.offsetWidth,o=i.offsetHeight,a=i.offsetWidth,r=this.options.margin,l=void 0,h=void 0;this.xMovement=0,this.yMovement=0,l=e.getBoundingClientRect().top+M.getDocumentScrollTop(),h=e.getBoundingClientRect().left+M.getDocumentScrollLeft(),"top"===this.options.position?(l+=-o-r,h+=s/2-a/2,this.yMovement=-this.options.transitionMovement):"right"===this.options.position?(l+=n/2-o/2,h+=s+r,this.xMovement=this.options.transitionMovement):"left"===this.options.position?(l+=n/2-o/2,h+=-a-r,this.xMovement=-this.options.transitionMovement):(l+=n+r,h+=s/2-a/2,this.yMovement=this.options.transitionMovement),t=this._repositionWithinScreen(h,l,a,o),d(i).css({top:t.y+"px",left:t.x+"px"})}},{key:"_repositionWithinScreen",value:function(t,e,i,n){var s=M.getDocumentScrollLeft(),o=M.getDocumentScrollTop(),a=t-s,r=e-o,l={left:a,top:r,width:i,height:n},h=this.options.margin+this.options.transitionMovement,d=M.checkWithinContainer(document.body,l,h);return d.left?a=h:d.right&&(a-=a+i-window.innerWidth),d.top?r=h:d.bottom&&(r-=r+n-window.innerHeight),{x:a+s,y:r+o}}},{key:"_animateIn",value:function(){this._positionTooltip(),this.tooltipEl.style.visibility="visible",e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:1,translateX:this.xMovement,translateY:this.yMovement,duration:this.options.inDuration,easing:"easeOutCubic"})}},{key:"_animateOut",value:function(){e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:0,translateX:0,translateY:0,duration:this.options.outDuration,easing:"easeOutCubic"})}},{key:"_handleMouseEnter",value:function(){this.isHovered=!0,this.isFocused=!1,this.open(!1)}},{key:"_handleMouseLeave",value:function(){this.isHovered=!1,this.isFocused=!1,this.close()}},{key:"_handleFocus",value:function(){M.tabPressed&&(this.isFocused=!0,this.open(!1))}},{key:"_handleBlur",value:function(){this.isFocused=!1,this.close()}},{key:"_getAttributeOptions",value:function(){var t={},e=this.el.getAttribute("data-tooltip"),i=this.el.getAttribute("data-position");return e&&(t.html=e),i&&(t.position=i),t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tooltip}},{key:"defaults",get:function(){return i}}]),n}();M.Tooltip=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tooltip","M_Tooltip")}(cash,M.anime),function(i){"use strict";var t=t||{},e=document.querySelectorAll.bind(document);function m(t){var e="";for(var i in t)t.hasOwnProperty(i)&&(e+=i+":"+t[i]+";");return e}var g={duration:750,show:function(t,e){if(2===t.button)return!1;var i=e||this,n=document.createElement("div");n.className="waves-ripple",i.appendChild(n);var s,o,a,r,l,h,d,u=(h={top:0,left:0},d=(s=i)&&s.ownerDocument,o=d.documentElement,void 0!==s.getBoundingClientRect&&(h=s.getBoundingClientRect()),a=null!==(l=r=d)&&l===l.window?r:9===r.nodeType&&r.defaultView,{top:h.top+a.pageYOffset-o.clientTop,left:h.left+a.pageXOffset-o.clientLeft}),c=t.pageY-u.top,p=t.pageX-u.left,v="scale("+i.clientWidth/100*10+")";"touches"in t&&(c=t.touches[0].pageY-u.top,p=t.touches[0].pageX-u.left),n.setAttribute("data-hold",Date.now()),n.setAttribute("data-scale",v),n.setAttribute("data-x",p),n.setAttribute("data-y",c);var f={top:c+"px",left:p+"px"};n.className=n.className+" waves-notransition",n.setAttribute("style",m(f)),n.className=n.className.replace("waves-notransition",""),f["-webkit-transform"]=v,f["-moz-transform"]=v,f["-ms-transform"]=v,f["-o-transform"]=v,f.transform=v,f.opacity="1",f["-webkit-transition-duration"]=g.duration+"ms",f["-moz-transition-duration"]=g.duration+"ms",f["-o-transition-duration"]=g.duration+"ms",f["transition-duration"]=g.duration+"ms",f["-webkit-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-moz-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-o-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",n.setAttribute("style",m(f))},hide:function(t){l.touchup(t);var e=this,i=(e.clientWidth,null),n=e.getElementsByClassName("waves-ripple");if(!(0i||1"+o+""+a+""+r+""),i.length&&e.prepend(i)}},{key:"_resetCurrentElement",value:function(){this.activeIndex=-1,this.$active.removeClass("active")}},{key:"_resetAutocomplete",value:function(){h(this.container).empty(),this._resetCurrentElement(),this.oldVal=null,this.isOpen=!1,this._mousedown=!1}},{key:"selectOption",value:function(t){var e=t.text().trim();this.el.value=e,this.$el.trigger("change"),this._resetAutocomplete(),this.close(),"function"==typeof this.options.onAutocomplete&&this.options.onAutocomplete.call(this,e)}},{key:"_renderDropdown",value:function(t,i){var n=this;this._resetAutocomplete();var e=[];for(var s in t)if(t.hasOwnProperty(s)&&-1!==s.toLowerCase().indexOf(i)){if(this.count>=this.options.limit)break;var o={data:t[s],key:s};e.push(o),this.count++}if(this.options.sortFunction){e.sort(function(t,e){return n.options.sortFunction(t.key.toLowerCase(),e.key.toLowerCase(),i.toLowerCase())})}for(var a=0;a");r.data?l.append(''+r.key+""):l.append(""+r.key+""),h(this.container).append(l),this._highlight(i,l)}}},{key:"open",value:function(){var t=this.el.value.toLowerCase();this._resetAutocomplete(),t.length>=this.options.minLength&&(this.isOpen=!0,this._renderDropdown(this.options.data,t)),this.dropdown.isOpen?this.dropdown.recalculateDimensions():this.dropdown.open()}},{key:"close",value:function(){this.dropdown.close()}},{key:"updateData",value:function(t){var e=this.el.value.toLowerCase();this.options.data=t,this.isOpen&&this._renderDropdown(t,e)}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Autocomplete}},{key:"defaults",get:function(){return e}}]),s}();t._keydown=!1,M.Autocomplete=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"autocomplete","M_Autocomplete")}(cash),function(d){M.updateTextFields=function(){d("input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea").each(function(t,e){var i=d(this);0'),d("body").append(e));var i=t.css("font-family"),n=t.css("font-size"),s=t.css("line-height"),o=t.css("padding-top"),a=t.css("padding-right"),r=t.css("padding-bottom"),l=t.css("padding-left");n&&e.css("font-size",n),i&&e.css("font-family",i),s&&e.css("line-height",s),o&&e.css("padding-top",o),a&&e.css("padding-right",a),r&&e.css("padding-bottom",r),l&&e.css("padding-left",l),t.data("original-height")||t.data("original-height",t.height()),"off"===t.attr("wrap")&&e.css("overflow-wrap","normal").css("white-space","pre"),e.text(t[0].value+"\n");var h=e.html().replace(/\n/g,"
");e.html(h),0'),this.$slides.each(function(t,e){var i=s('
  • ');n.$indicators.append(i[0])}),this.$el.append(this.$indicators[0]),this.$indicators=this.$indicators.children("li.indicator-item"))}},{key:"_removeIndicators",value:function(){this.$el.find("ul.indicators").remove()}},{key:"set",value:function(t){var e=this;if(t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.activeIndex!=t){this.$active=this.$slides.eq(this.activeIndex);var i=this.$active.find(".caption");this.$active.removeClass("active"),o({targets:this.$active[0],opacity:0,duration:this.options.duration,easing:"easeOutQuad",complete:function(){e.$slides.not(".active").each(function(t){o({targets:t,opacity:0,translateX:0,translateY:0,duration:0,easing:"easeOutQuad"})})}}),this._animateCaptionIn(i[0],this.options.duration),this.options.indicators&&(this.$indicators.eq(this.activeIndex).removeClass("active"),this.$indicators.eq(t).addClass("active")),o({targets:this.$slides.eq(t)[0],opacity:1,duration:this.options.duration,easing:"easeOutQuad"}),o({targets:this.$slides.eq(t).find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:this.options.duration,delay:this.options.duration,easing:"easeOutQuad"}),this.$slides.eq(t).addClass("active"),this.activeIndex=t,this.start()}}},{key:"pause",value:function(){clearInterval(this.interval)}},{key:"start",value:function(){clearInterval(this.interval),this.interval=setInterval(this._handleIntervalBound,this.options.duration+this.options.interval)}},{key:"next",value:function(){var t=this.activeIndex+1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}},{key:"prev",value:function(){var t=this.activeIndex-1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Slider}},{key:"defaults",get:function(){return e}}]),n}();M.Slider=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"slider","M_Slider")}(cash,M.anime),function(n,s){n(document).on("click",".card",function(t){if(n(this).children(".card-reveal").length){var i=n(t.target).closest(".card");void 0===i.data("initialOverflow")&&i.data("initialOverflow",void 0===i.css("overflow")?"":i.css("overflow"));var e=n(this).find(".card-reveal");n(t.target).is(n(".card-reveal .card-title"))||n(t.target).is(n(".card-reveal .card-title i"))?s({targets:e[0],translateY:0,duration:225,easing:"easeInOutQuad",complete:function(t){var e=t.animatables[0].target;n(e).css({display:"none"}),i.css("overflow",i.data("initialOverflow"))}}):(n(t.target).is(n(".card .activator"))||n(t.target).is(n(".card .activator i")))&&(i.css("overflow","hidden"),e.css({display:"block"}),s({targets:e[0],translateY:"-100%",duration:300,easing:"easeInOutQuad"}))}})}(cash,M.anime),function(h){"use strict";var e={data:[],placeholder:"",secondaryPlaceholder:"",autocompleteOptions:{},limit:1/0,onChipAdd:null,onChipSelect:null,onChipDelete:null},t=function(t){function l(t,e){_classCallCheck(this,l);var i=_possibleConstructorReturn(this,(l.__proto__||Object.getPrototypeOf(l)).call(this,l,t,e));return(i.el.M_Chips=i).options=h.extend({},l.defaults,e),i.$el.addClass("chips input-field"),i.chipsData=[],i.$chips=h(),i._setupInput(),i.hasAutocomplete=0"),this.$el.append(this.$input)),this.$input.addClass("input")}},{key:"_setupLabel",value:function(){this.$label=this.$el.find("label"),this.$label.length&&this.$label.setAttribute("for",this.$input.attr("id"))}},{key:"_setPlaceholder",value:function(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?h(this.$input).prop("placeholder",this.options.placeholder):(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&h(this.$input).prop("placeholder",this.options.secondaryPlaceholder)}},{key:"_isValid",value:function(t){if(t.hasOwnProperty("tag")&&""!==t.tag){for(var e=!1,i=0;i=this.options.limit)){var e=this._renderChip(t);this.$chips.add(e),this.chipsData.push(t),h(this.$input).before(e),this._setPlaceholder(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd.call(this,this.$el,e)}}},{key:"deleteChip",value:function(t){var e=this.$chips.eq(t);this.$chips.eq(t).remove(),this.$chips=this.$chips.filter(function(t){return 0<=h(t).index()}),this.chipsData.splice(t,1),this._setPlaceholder(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete.call(this,this.$el,e[0])}},{key:"selectChip",value:function(t){var e=this.$chips.eq(t);(this._selectedChip=e)[0].focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect.call(this,this.$el,e[0])}}],[{key:"init",value:function(t,e){return _get(l.__proto__||Object.getPrototypeOf(l),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Chips}},{key:"_handleChipsKeydown",value:function(t){l._keydown=!0;var e=h(t.target).closest(".chips"),i=t.target&&e.length;if(!h(t.target).is("input, textarea")&&i){var n=e[0].M_Chips;if(8===t.keyCode||46===t.keyCode){t.preventDefault();var s=n.chipsData.length;if(n._selectedChip){var o=n._selectedChip.index();n.deleteChip(o),n._selectedChip=null,s=Math.max(o-1,0)}n.chipsData.length&&n.selectChip(s)}else if(37===t.keyCode){if(n._selectedChip){var a=n._selectedChip.index()-1;if(a<0)return;n.selectChip(a)}}else if(39===t.keyCode&&n._selectedChip){var r=n._selectedChip.index()+1;r>=n.chipsData.length?n.$input[0].focus():n.selectChip(r)}}}},{key:"_handleChipsKeyup",value:function(t){l._keydown=!1}},{key:"_handleChipsBlur",value:function(t){l._keydown||(h(t.target).closest(".chips")[0].M_Chips._selectedChip=null)}},{key:"defaults",get:function(){return e}}]),l}();t._keydown=!1,M.Chips=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"chips","M_Chips"),h(document).ready(function(){h(document.body).on("click",".chip .close",function(){var t=h(this).closest(".chips");t.length&&t[0].M_Chips||h(this).closest(".chip").remove()})})}(cash),function(s){"use strict";var e={top:0,bottom:1/0,offset:0,onPositionChange:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Pushpin=i).options=s.extend({},n.defaults,e),i.originalOffset=i.el.offsetTop,n._pushpins.push(i),i._setupEventHandlers(),i._updatePosition(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.el.style.top=null,this._removePinClasses(),this._removeEventHandlers();var t=n._pushpins.indexOf(this);n._pushpins.splice(t,1)}},{key:"_setupEventHandlers",value:function(){document.addEventListener("scroll",n._updateElements)}},{key:"_removeEventHandlers",value:function(){document.removeEventListener("scroll",n._updateElements)}},{key:"_updatePosition",value:function(){var t=M.getDocumentScrollTop()+this.options.offset;this.options.top<=t&&this.options.bottom>=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=this.options.offset+"px",this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),tthis.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}},{key:"_removePinClasses",value:function(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Pushpin}},{key:"_updateElements",value:function(){for(var t in n._pushpins){n._pushpins[t]._updatePosition()}}},{key:"defaults",get:function(){return e}}]),n}();t._pushpins=[],M.Pushpin=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"pushpin","M_Pushpin")}(cash),function(r,s){"use strict";var e={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};r.fn.reverse=[].reverse;var t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_FloatingActionButton=i).options=r.extend({},n.defaults,e),i.isOpen=!1,i.$anchor=i.$el.children("a").first(),i.$menu=i.$el.children("ul").first(),i.$floatingBtns=i.$el.find("ul .btn-floating"),i.$floatingBtnsReverse=i.$el.find("ul .btn-floating").reverse(),i.offsetY=0,i.offsetX=0,i.$el.addClass("direction-"+i.options.direction),"top"===i.options.direction?i.offsetY=40:"right"===i.options.direction?i.offsetX=-40:"bottom"===i.options.direction?i.offsetY=-40:i.offsetX=40,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_FloatingActionButton=void 0}},{key:"_setupEventHandlers",value:function(){this._handleFABClickBound=this._handleFABClick.bind(this),this._handleOpenBound=this.open.bind(this),this._handleCloseBound=this.close.bind(this),this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this._handleOpenBound),this.el.addEventListener("mouseleave",this._handleCloseBound)):this.el.addEventListener("click",this._handleFABClickBound)}},{key:"_removeEventHandlers",value:function(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this._handleOpenBound),this.el.removeEventListener("mouseleave",this._handleCloseBound)):this.el.removeEventListener("click",this._handleFABClickBound)}},{key:"_handleFABClick",value:function(){this.isOpen?this.close():this.open()}},{key:"_handleDocumentClick",value:function(t){r(t.target).closest(this.$menu).length||this.close()}},{key:"open",value:function(){this.isOpen||(this.options.toolbarEnabled?this._animateInToolbar():this._animateInFAB(),this.isOpen=!0)}},{key:"close",value:function(){this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this._handleCloseBound,!0),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),this._animateOutToolbar()):this._animateOutFAB(),this.isOpen=!1)}},{key:"_animateInFAB",value:function(){var e=this;this.$el.addClass("active");var i=0;this.$floatingBtnsReverse.each(function(t){s({targets:t,opacity:1,scale:[.4,1],translateY:[e.offsetY,0],translateX:[e.offsetX,0],duration:275,delay:i,easing:"easeInOutQuad"}),i+=40})}},{key:"_animateOutFAB",value:function(){var e=this;this.$floatingBtnsReverse.each(function(t){s.remove(t),s({targets:t,opacity:0,scale:.4,translateY:e.offsetY,translateX:e.offsetX,duration:175,easing:"easeOutQuad",complete:function(){e.$el.removeClass("active")}})})}},{key:"_animateInToolbar",value:function(){var t,e=this,i=window.innerWidth,n=window.innerHeight,s=this.el.getBoundingClientRect(),o=r('
    '),a=this.$anchor.css("background-color");this.$anchor.append(o),this.offsetX=s.left-i/2+s.width/2,this.offsetY=n-s.bottom,t=i/o[0].clientWidth,this.btnBottom=s.bottom,this.btnLeft=s.left,this.btnWidth=s.width,this.$el.addClass("active"),this.$el.css({"text-align":"center",width:"100%",bottom:0,left:0,transform:"translateX("+this.offsetX+"px)",transition:"none"}),this.$anchor.css({transform:"translateY("+-this.offsetY+"px)",transition:"none"}),o.css({"background-color":a}),setTimeout(function(){e.$el.css({transform:"",transition:"transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s"}),e.$anchor.css({overflow:"visible",transform:"",transition:"transform .2s"}),setTimeout(function(){e.$el.css({overflow:"hidden","background-color":a}),o.css({transform:"scale("+t+")",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"}),e.$menu.children("li").children("a").css({opacity:1}),e._handleDocumentClickBound=e._handleDocumentClick.bind(e),window.addEventListener("scroll",e._handleCloseBound,!0),document.body.addEventListener("click",e._handleDocumentClickBound,!0)},100)},0)}},{key:"_animateOutToolbar",value:function(){var t=this,e=window.innerWidth,i=window.innerHeight,n=this.$el.find(".fab-backdrop"),s=this.$anchor.css("background-color");this.offsetX=this.btnLeft-e/2+this.btnWidth/2,this.offsetY=i-this.btnBottom,this.$el.removeClass("active"),this.$el.css({"background-color":"transparent",transition:"none"}),this.$anchor.css({transition:"none"}),n.css({transform:"scale(0)","background-color":s}),this.$menu.children("li").children("a").css({opacity:""}),setTimeout(function(){n.remove(),t.$el.css({"text-align":"",width:"",bottom:"",left:"",overflow:"","background-color":"",transform:"translate3d("+-t.offsetX+"px,0,0)"}),t.$anchor.css({overflow:"",transform:"translate3d(0,"+t.offsetY+"px,0)"}),setTimeout(function(){t.$el.css({transform:"translate3d(0,0,0)",transition:"transform .2s"}),t.$anchor.css({transform:"translate3d(0,0,0)",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"})},20)},200)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FloatingActionButton}},{key:"defaults",get:function(){return e}}]),n}();M.FloatingActionButton=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"floatingActionButton","M_FloatingActionButton")}(cash,M.anime),function(g){"use strict";var e={autoClose:!1,format:"mmm dd, yyyy",parse:null,defaultDate:null,setDefaultDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,container:null,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onOpen:null,onClose:null,onDraw:null},t=function(t){function B(t,e){_classCallCheck(this,B);var i=_possibleConstructorReturn(this,(B.__proto__||Object.getPrototypeOf(B)).call(this,B,t,e));(i.el.M_Datepicker=i).options=g.extend({},B.defaults,e),e&&e.hasOwnProperty("i18n")&&"object"==typeof e.i18n&&(i.options.i18n=g.extend({},B.defaults.i18n,e.i18n)),i.options.minDate&&i.options.minDate.setHours(0,0,0,0),i.options.maxDate&&i.options.maxDate.setHours(0,0,0,0),i.id=M.guid(),i._setupVariables(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupEventHandlers(),i.options.defaultDate||(i.options.defaultDate=new Date(Date.parse(i.el.value)));var n=i.options.defaultDate;return B._isDate(n)?i.options.setDefaultDate?(i.setDate(n,!0),i.setInputValue()):i.gotoDate(n):i.gotoDate(new Date),i.isOpen=!1,i}return _inherits(B,Component),_createClass(B,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),g(this.modalEl).remove(),this.destroySelects(),this.el.M_Datepicker=void 0}},{key:"destroySelects",value:function(){var t=this.calendarEl.querySelector(".orig-select-year");t&&M.FormSelect.getInstance(t).destroy();var e=this.calendarEl.querySelector(".orig-select-month");e&&M.FormSelect.getInstance(e).destroy()}},{key:"_insertHTMLIntoDOM",value:function(){this.options.showClearBtn&&(g(this.clearBtn).css({visibility:""}),this.clearBtn.innerHTML=this.options.i18n.clear),this.doneBtn.innerHTML=this.options.i18n.done,this.cancelBtn.innerHTML=this.options.i18n.cancel,this.options.container?this.$modalEl.appendTo(this.options.container):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modalEl.id="modal-"+this.id,this.modal=M.Modal.init(this.modalEl,{onCloseEnd:function(){t.isOpen=!1}})}},{key:"toString",value:function(t){var e=this;return t=t||this.options.format,B._isDate(this.date)?t.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g).map(function(t){return e.formats[t]?e.formats[t]():t}).join(""):""}},{key:"setDate",value:function(t,e){if(!t)return this.date=null,this._renderDateDisplay(),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),B._isDate(t)){var i=this.options.minDate,n=this.options.maxDate;B._isDate(i)&&tn.maxDate||n.disableWeekends&&B._isWeekend(y)||n.disableDayFn&&n.disableDayFn(y),isEmpty:C,isStartRange:x,isEndRange:L,isInRange:T,showDaysInNextAndPreviousMonths:n.showDaysInNextAndPreviousMonths};l.push(this.renderDay($)),7==++_&&(r.push(this.renderRow(l,n.isRTL,m)),_=0,m=!(l=[]))}return this.renderTable(n,r,i)}},{key:"renderDay",value:function(t){var e=[],i="false";if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'';e.push("is-outside-current-month"),e.push("is-selection-disabled")}return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&(e.push("is-selected"),i="true"),t.hasEvent&&e.push("has-event"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"}},{key:"renderRow",value:function(t,e,i){return''+(e?t.reverse():t).join("")+""}},{key:"renderTable",value:function(t,e,i){return'
    '+this.renderHead(t)+this.renderBody(e)+"
    "}},{key:"renderHead",value:function(t){var e=void 0,i=[];for(e=0;e<7;e++)i.push(''+this.renderDayName(t,e,!0)+"");return""+(t.isRTL?i.reverse():i).join("")+""}},{key:"renderBody",value:function(t){return""+t.join("")+""}},{key:"renderTitle",value:function(t,e,i,n,s,o){var a,r,l=void 0,h=void 0,d=void 0,u=this.options,c=i===u.minYear,p=i===u.maxYear,v='
    ',f=!0,m=!0;for(d=[],l=0;l<12;l++)d.push('");for(a='",g.isArray(u.yearRange)?(l=u.yearRange[0],h=u.yearRange[1]+1):(l=i-u.yearRange,h=1+i+u.yearRange),d=[];l=u.minYear&&d.push('");r='";v+='',v+='
    ',u.showMonthAfterYear?v+=r+a:v+=a+r,v+="
    ",c&&(0===n||u.minMonth>=n)&&(f=!1),p&&(11===n||u.maxMonth<=n)&&(m=!1);return(v+='')+"
    "}},{key:"draw",value:function(t){if(this.isOpen||t){var e,i=this.options,n=i.minYear,s=i.maxYear,o=i.minMonth,a=i.maxMonth,r="";this._y<=n&&(this._y=n,!isNaN(o)&&this._m=s&&(this._y=s,!isNaN(a)&&this._m>a&&(this._m=a)),e="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(var l=0;l<1;l++)this._renderDateDisplay(),r+=this.renderTitle(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year,e)+this.render(this.calendars[l].year,this.calendars[l].month,e);this.destroySelects(),this.calendarEl.innerHTML=r;var h=this.calendarEl.querySelector(".orig-select-year"),d=this.calendarEl.querySelector(".orig-select-month");M.FormSelect.init(h,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),M.FormSelect.init(d,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),h.addEventListener("change",this._handleYearChange.bind(this)),d.addEventListener("change",this._handleMonthChange.bind(this)),"function"==typeof this.options.onDraw&&this.options.onDraw(this)}}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleInputChangeBound=this._handleInputChange.bind(this),this._handleCalendarClickBound=this._handleCalendarClick.bind(this),this._finishSelectionBound=this._finishSelection.bind(this),this._handleMonthChange=this._handleMonthChange.bind(this),this._closeBound=this.close.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("change",this._handleInputChangeBound),this.calendarEl.addEventListener("click",this._handleCalendarClickBound),this.doneBtn.addEventListener("click",this._finishSelectionBound),this.cancelBtn.addEventListener("click",this._closeBound),this.options.showClearBtn&&(this._handleClearClickBound=this._handleClearClick.bind(this),this.clearBtn.addEventListener("click",this._handleClearClickBound))}},{key:"_setupVariables",value:function(){var e=this;this.$modalEl=g(B._template),this.modalEl=this.$modalEl[0],this.calendarEl=this.modalEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.modalEl.querySelector(".year-text"),this.dateTextEl=this.modalEl.querySelector(".date-text"),this.options.showClearBtn&&(this.clearBtn=this.modalEl.querySelector(".datepicker-clear")),this.doneBtn=this.modalEl.querySelector(".datepicker-done"),this.cancelBtn=this.modalEl.querySelector(".datepicker-cancel"),this.formats={d:function(){return e.date.getDate()},dd:function(){var t=e.date.getDate();return(t<10?"0":"")+t},ddd:function(){return e.options.i18n.weekdaysShort[e.date.getDay()]},dddd:function(){return e.options.i18n.weekdays[e.date.getDay()]},m:function(){return e.date.getMonth()+1},mm:function(){var t=e.date.getMonth()+1;return(t<10?"0":"")+t},mmm:function(){return e.options.i18n.monthsShort[e.date.getMonth()]},mmmm:function(){return e.options.i18n.months[e.date.getMonth()]},yy:function(){return(""+e.date.getFullYear()).slice(2)},yyyy:function(){return e.date.getFullYear()}}}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("change",this._handleInputChangeBound),this.calendarEl.removeEventListener("click",this._handleCalendarClickBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleCalendarClick",value:function(t){if(this.isOpen){var e=g(t.target);e.hasClass("is-disabled")||(!e.hasClass("datepicker-day-button")||e.hasClass("is-empty")||e.parent().hasClass("is-disabled")?e.closest(".month-prev").length?this.prevMonth():e.closest(".month-next").length&&this.nextMonth():(this.setDate(new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"))),this.options.autoClose&&this._finishSelection()))}}},{key:"_handleClearClick",value:function(){this.date=null,this.setInputValue(),this.close()}},{key:"_handleMonthChange",value:function(t){this.gotoMonth(t.target.value)}},{key:"_handleYearChange",value:function(t){this.gotoYear(t.target.value)}},{key:"gotoMonth",value:function(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}},{key:"gotoYear",value:function(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}},{key:"_handleInputChange",value:function(t){var e=void 0;t.firedBy!==this&&(e=this.options.parse?this.options.parse(this.el.value,this.options.format):new Date(Date.parse(this.el.value)),B._isDate(e)&&this.setDate(e))}},{key:"renderDayName",value:function(t,e,i){for(e+=t.firstDay;7<=e;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}},{key:"_finishSelection",value:function(){this.setInputValue(),this.close()}},{key:"open",value:function(){if(!this.isOpen)return this.isOpen=!0,"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.draw(),this.modal.open(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.modal.close(),this}}],[{key:"init",value:function(t,e){return _get(B.__proto__||Object.getPrototypeOf(B),"init",this).call(this,this,t,e)}},{key:"_isDate",value:function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}},{key:"_isWeekend",value:function(t){var e=t.getDay();return 0===e||6===e}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"_getDaysInMonth",value:function(t,e){return[31,B._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}},{key:"_isLeapYear",value:function(t){return t%4==0&&t%100!=0||t%400==0}},{key:"_compareDates",value:function(t,e){return t.getTime()===e.getTime()}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Datepicker}},{key:"defaults",get:function(){return e}}]),B}();t._template=['"].join(""),M.Datepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"datepicker","M_Datepicker")}(cash),function(h){"use strict";var e={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},autoClose:!1,twelveHour:!0,vibrate:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onSelect:null},t=function(t){function f(t,e){_classCallCheck(this,f);var i=_possibleConstructorReturn(this,(f.__proto__||Object.getPrototypeOf(f)).call(this,f,t,e));return(i.el.M_Timepicker=i).options=h.extend({},f.defaults,e),i.id=M.guid(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupVariables(),i._setupEventHandlers(),i._clockSetup(),i._pickerSetup(),i}return _inherits(f,Component),_createClass(f,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),h(this.modalEl).remove(),this.el.M_Timepicker=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleClockClickStartBound=this._handleClockClickStart.bind(this),this._handleDocumentClickMoveBound=this._handleDocumentClickMove.bind(this),this._handleDocumentClickEndBound=this._handleDocumentClickEnd.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.plate.addEventListener("mousedown",this._handleClockClickStartBound),this.plate.addEventListener("touchstart",this._handleClockClickStartBound),h(this.spanHours).on("click",this.showView.bind(this,"hours")),h(this.spanMinutes).on("click",this.showView.bind(this,"minutes"))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleClockClickStart",value:function(t){t.preventDefault();var e=this.plate.getBoundingClientRect(),i=e.left,n=e.top;this.x0=i+this.options.dialRadius,this.y0=n+this.options.dialRadius,this.moved=!1;var s=f._Pos(t);this.dx=s.x-this.x0,this.dy=s.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMoveBound),document.addEventListener("touchmove",this._handleDocumentClickMoveBound),document.addEventListener("mouseup",this._handleDocumentClickEndBound),document.addEventListener("touchend",this._handleDocumentClickEndBound)}},{key:"_handleDocumentClickMove",value:function(t){t.preventDefault();var e=f._Pos(t),i=e.x-this.x0,n=e.y-this.y0;this.moved=!0,this.setHand(i,n,!1,!0)}},{key:"_handleDocumentClickEnd",value:function(t){var e=this;t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEndBound),document.removeEventListener("touchend",this._handleDocumentClickEndBound);var i=f._Pos(t),n=i.x-this.x0,s=i.y-this.y0;this.moved&&n===this.dx&&s===this.dy&&this.setHand(n,s),"hours"===this.currentView?this.showView("minutes",this.options.duration/2):this.options.autoClose&&(h(this.minutesView).addClass("timepicker-dial-out"),setTimeout(function(){e.done()},this.options.duration/2)),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMoveBound),document.removeEventListener("touchmove",this._handleDocumentClickMoveBound)}},{key:"_insertHTMLIntoDOM",value:function(){this.$modalEl=h(f._template),this.modalEl=this.$modalEl[0],this.modalEl.id="modal-"+this.id;var t=document.querySelector(this.options.container);this.options.container&&t?this.$modalEl.appendTo(t):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modal=M.Modal.init(this.modalEl,{onOpenStart:this.options.onOpenStart,onOpenEnd:this.options.onOpenEnd,onCloseStart:this.options.onCloseStart,onCloseEnd:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t),t.isOpen=!1}})}},{key:"_setupVariables",value:function(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.modalEl.querySelector(".timepicker-canvas"),this.plate=this.modalEl.querySelector(".timepicker-plate"),this.hoursView=this.modalEl.querySelector(".timepicker-hours"),this.minutesView=this.modalEl.querySelector(".timepicker-minutes"),this.spanHours=this.modalEl.querySelector(".timepicker-span-hours"),this.spanMinutes=this.modalEl.querySelector(".timepicker-span-minutes"),this.spanAmPm=this.modalEl.querySelector(".timepicker-span-am-pm"),this.footer=this.modalEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}},{key:"_pickerSetup",value:function(){var t=h('").appendTo(this.footer).on("click",this.clear.bind(this));this.options.showClearBtn&&t.css({visibility:""});var e=h('
    ');h('").appendTo(e).on("click",this.close.bind(this)),h('").appendTo(e).on("click",this.done.bind(this)),e.appendTo(this.footer)}},{key:"_clockSetup",value:function(){this.options.twelveHour&&(this.$amBtn=h('
    AM
    '),this.$pmBtn=h('
    PM
    '),this.$amBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm),this.$pmBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}},{key:"_buildSVGClock",value:function(){var t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,n=f._createSVGEl("svg");n.setAttribute("class","timepicker-svg"),n.setAttribute("width",i),n.setAttribute("height",i);var s=f._createSVGEl("g");s.setAttribute("transform","translate("+t+","+t+")");var o=f._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx",0),o.setAttribute("cy",0),o.setAttribute("r",4);var a=f._createSVGEl("line");a.setAttribute("x1",0),a.setAttribute("y1",0);var r=f._createSVGEl("circle");r.setAttribute("class","timepicker-canvas-bg"),r.setAttribute("r",e),s.appendChild(a),s.appendChild(r),s.appendChild(o),n.appendChild(s),this._canvas.appendChild(n),this.hand=a,this.bg=r,this.bearing=o,this.g=s}},{key:"_buildHoursView",value:function(){var t=h('
    ');if(this.options.twelveHour)for(var e=1;e<13;e+=1){var i=t.clone(),n=e/6*Math.PI,s=this.options.outerRadius;i.css({left:this.options.dialRadius+Math.sin(n)*s-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*s-this.options.tickRadius+"px"}),i.html(0===e?"00":e),this.hoursView.appendChild(i[0])}else for(var o=0;o<24;o+=1){var a=t.clone(),r=o/6*Math.PI,l=0'),e=0;e<60;e+=5){var i=t.clone(),n=e/30*Math.PI;i.css({left:this.options.dialRadius+Math.sin(n)*this.options.outerRadius-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*this.options.outerRadius-this.options.tickRadius+"px"}),i.html(f._addLeadingZero(e)),this.minutesView.appendChild(i[0])}}},{key:"_handleAmPmClick",value:function(t){var e=h(t.target);this.amOrPm=e.hasClass("am-btn")?"AM":"PM",this._updateAmPmView()}},{key:"_updateAmPmView",value:function(){this.options.twelveHour&&(this.$amBtn.toggleClass("text-primary","AM"===this.amOrPm),this.$pmBtn.toggleClass("text-primary","PM"===this.amOrPm))}},{key:"_updateTimeFromInput",value:function(){var t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(0','",""].join(""),M.Timepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"timepicker","M_Timepicker")}(cash),function(s){"use strict";var e={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_CharacterCounter=i).options=s.extend({},n.defaults,e),i.isInvalid=!1,i.isValidLength=!1,i._setupCounter(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}},{key:"_setupEventHandlers",value:function(){this._handleUpdateCounterBound=this.updateCounter.bind(this),this.el.addEventListener("focus",this._handleUpdateCounterBound,!0),this.el.addEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("focus",this._handleUpdateCounterBound,!0),this.el.removeEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_setupCounter",value:function(){this.counterEl=document.createElement("span"),s(this.counterEl).addClass("character-counter").css({float:"right","font-size":"12px",height:1}),this.$el.parent().append(this.counterEl)}},{key:"_removeCounter",value:function(){s(this.counterEl).remove()}},{key:"updateCounter",value:function(){var t=+this.$el.attr("data-length"),e=this.el.value.length;this.isValidLength=e<=t;var i=e;t&&(i+="/"+t,this._validateInput()),s(this.counterEl).html(i)}},{key:"_validateInput",value:function(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.$el.removeClass("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.$el.removeClass("valid"),this.$el.addClass("invalid"))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_CharacterCounter}},{key:"defaults",get:function(){return e}}]),n}();M.CharacterCounter=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"characterCounter","M_CharacterCounter")}(cash),function(b){"use strict";var e={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null},t=function(t){function i(t,e){_classCallCheck(this,i);var n=_possibleConstructorReturn(this,(i.__proto__||Object.getPrototypeOf(i)).call(this,i,t,e));return(n.el.M_Carousel=n).options=b.extend({},i.defaults,e),n.hasMultipleSlides=1'),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('
  • ');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i'+t.getAttribute("label")+"")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'":e.innerHTML,a=d("
  • "),r=d("");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime); \ No newline at end of file diff --git a/app/static/video/alphatest.webm b/app/static/video/alphatest.webm new file mode 100644 index 0000000..1789dfc Binary files /dev/null and b/app/static/video/alphatest.webm differ diff --git a/app/templates/apply/index.html b/app/templates/apply/index.html new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/auth/account.html b/app/templates/auth/account.html new file mode 100644 index 0000000..c3892ef --- /dev/null +++ b/app/templates/auth/account.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% set title = current_user.id %} + +{% block content %} +
    +
    +
    +
    +
    +
    +

    {{ current_user.first_name }} {{ current_user.last_name }}

    +

    {{ current_user.type }}

    +
    +
    +

    Member since

    +

    {{ current_user.join_date }}

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    Address
    +

    123 Example St, Boulder CO, 80303

    +
    +
    +
    +
    Monthly payment
    +

    $1,000.00

    +
    +
    +
    Current Ownership
    +

    10%

    +
    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/email_confirmation.html b/app/templates/auth/email_confirmation.html new file mode 100644 index 0000000..8ed19ed --- /dev/null +++ b/app/templates/auth/email_confirmation.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% import "includes/macros.html" as macros %} + +{% block page_js %} + +{% endblock %} + +{% block content %} + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/login_modal.html b/app/templates/auth/login_modal.html new file mode 100644 index 0000000..bf34026 --- /dev/null +++ b/app/templates/auth/login_modal.html @@ -0,0 +1,37 @@ +{% import "includes/macros.html" as macros %} + +
    +
    +
    + {{ macros.render_field(form.user_id, 'account_circle') }} +
    +
    +
    + {{ macros.render_field(form.password, 'lock') }} +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    diff --git a/app/templates/auth/register.html b/app/templates/auth/register.html new file mode 100644 index 0000000..ddf3d48 --- /dev/null +++ b/app/templates/auth/register.html @@ -0,0 +1,59 @@ +{% import "includes/macros.html" as macros %} + +
    +
    +
    + {{ macros.render_field(form.user_id, 'account_circle') }} +
    +
    +
    +
    + {{ macros.render_field(form.email, 'email')}} +
    +
    + {{ macros.render_field(form.confirm_email) }} +
    +
    +
    +
    + {{ macros.render_field(form.phone_number, 'phone_android') }} +
    +
    +
    +
    + {{ macros.render_field(form.first_name, 'person') }} +
    +
    + {{ macros.render_field(form.last_name) }} +
    +
    + {% if not form.date_of_birth.errors %} + {{ form.date_of_birth(class='datepicker') }} + {{ form.date_of_birth.label }} + {% else %} + {{ form.date_of_birth(class='datepicker invalid data') }} + {% for err in form.date_of_birth.errors %} + + {% endfor %} + {{ form.date_of_birth.label(class='active') }} + {% endif %} +
    +
    +
    +
    + {{ macros.render_field(form.password, 'lock') }} +
    +
    + {{ macros.render_field(form.confirm_pass) }} +
    +
    + +
    + + diff --git a/app/templates/auth/request_password_reset.html b/app/templates/auth/request_password_reset.html new file mode 100644 index 0000000..6a077f1 --- /dev/null +++ b/app/templates/auth/request_password_reset.html @@ -0,0 +1,20 @@ +{% import "includes/macros.html" as macros %} + +
    +
    +
    + {{ macros.render_field(form.email, 'email')}} +
    +
    +
    +

    OR

    +
    +
    +
    + {{ macros.render_field(form.user_id, 'account_circle') }} +
    +
    + +
    \ No newline at end of file diff --git a/app/templates/auth/reset_password.html b/app/templates/auth/reset_password.html new file mode 100644 index 0000000..51d1c97 --- /dev/null +++ b/app/templates/auth/reset_password.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% import "includes/macros.html" as macros %} + +{% set title="Reset password" %} + +{% block page_js %} + +{% endblock %} + +{% block content %} + + +{% endblock %} diff --git a/app/templates/auth/unauthorized.html b/app/templates/auth/unauthorized.html new file mode 100644 index 0000000..65b2f79 --- /dev/null +++ b/app/templates/auth/unauthorized.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block page_js %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..c540e24 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,55 @@ +{% import "includes/macros.html" as macros %} + +{% block head %} + Quarter Homes +{% endblock %} + + + + + + + + +{% block page_js %} +{% endblock %} + + +{{ macros.make_modal_form('register', 'Register', url_for('auth.register'), 'register-form', 'btn-register-submit') }} +{{ macros.make_modal_form('login', 'Login', url_for('auth.login'), 'login-form', 'btn-login-submit') }} +{{ macros.make_modal_form('requset-pwd-reset', 'Reset Password', url_for('auth.request_password_reset'), 'request-pwd-reset-form', 'btn-request-reset-pwd-submit') }} + +{% block flash_messages %} + {% include "includes/flash_messages.html" %} +{% endblock %} + +{% block body_content %} + + {% include "includes/navbar.html" %} + + {% block content %} + {% endblock %} + +{% endblock %} + + +{% block footer %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/email/confirm_email.html b/app/templates/email/confirm_email.html new file mode 100644 index 0000000..0a5421a --- /dev/null +++ b/app/templates/email/confirm_email.html @@ -0,0 +1,8 @@ +

    {{ user.user_id }},

    +

    + To confirm your email address click here. +

    +

    + Alternatively you can paste the following link into your browser's address bar: +

    +

    {{ url_for('auth.confirm_email', token=token, _external=True) }}

    diff --git a/app/templates/email/confirm_email.txt b/app/templates/email/confirm_email.txt new file mode 100644 index 0000000..94410e8 --- /dev/null +++ b/app/templates/email/confirm_email.txt @@ -0,0 +1,6 @@ +{{user.user_id}}, + +Please confirm your email address with the following link (expires in {{ exp_mins }} minutes): + +{{ url_for('auth.confirm_email', token=token, _external=True) }} + diff --git a/app/templates/email/forgot_user_id.html b/app/templates/email/forgot_user_id.html new file mode 100644 index 0000000..015bf8a --- /dev/null +++ b/app/templates/email/forgot_user_id.html @@ -0,0 +1,4 @@ +

    {{ user.user_id }},

    +

    + Your quarter user ID is "{{ user.user_id }}". +

    \ No newline at end of file diff --git a/app/templates/email/forgot_user_id.txt b/app/templates/email/forgot_user_id.txt new file mode 100644 index 0000000..9aa86d7 --- /dev/null +++ b/app/templates/email/forgot_user_id.txt @@ -0,0 +1,3 @@ +{{ user.user_id }}, + +Your quarter user ID is "{{ user.user_id }}". \ No newline at end of file diff --git a/app/templates/email/reset_password.html b/app/templates/email/reset_password.html new file mode 100644 index 0000000..6854d0f --- /dev/null +++ b/app/templates/email/reset_password.html @@ -0,0 +1,11 @@ +

    {{ user.user_id }},

    +

    + To reset your password click here. +

    +

    + Alternatively you can paste the following link into your browser's address bar: +

    +

    {{ url_for('auth.reset_password', token=token, _external=True) }}

    +

    + If you have not requested a password reset, please ignore this email. +

    diff --git a/app/templates/email/reset_password.txt b/app/templates/email/reset_password.txt new file mode 100644 index 0000000..0505bf5 --- /dev/null +++ b/app/templates/email/reset_password.txt @@ -0,0 +1,8 @@ +{{user.user_id}}, + +Reset your password with the following link (expires in {{ exp_mins }} minutes): + +{{ url_for('auth.reset_password', token=token, _external=True) }} + +If you have not requested a password reset, ignore this message. + diff --git a/app/templates/includes/flash_messages.html b/app/templates/includes/flash_messages.html new file mode 100644 index 0000000..2029b59 --- /dev/null +++ b/app/templates/includes/flash_messages.html @@ -0,0 +1,18 @@ +{% import "includes/macros.html" as macros %} +{% with messages = get_flashed_messages(with_categories=True) %} + {% if messages %} +
    + {% for category, message in messages %} + {% if category == 'error' %} + {{ macros.make_toast(message, "color: rgba(255, 0, 0, 0.5)", None) }} + {% elif category == 'success' %} + {{ macros.make_toast(message, "color: rgba(0, 255, 0, 0.5)", None) }} + {% elif category == 'warning' %} + {{ macros.make_toast(message, "color: rgba(220, 200, 0, 0.5)", None) }} + {% else %} + {{ macros.make_toast(message, None, None) }} + {% endif %} + {% endfor %} +
    + {% endif %} +{% endwith %} diff --git a/app/templates/includes/macros.html b/app/templates/includes/macros.html new file mode 100644 index 0000000..c4fdf03 --- /dev/null +++ b/app/templates/includes/macros.html @@ -0,0 +1,67 @@ +{% macro render_field(field, icon_prefix=None) %} + {% if icon_prefix %} + {{ icon_prefix }} + {% endif %} + {% if field.errors %} + {{ field(class='invalid data') }} + {{ field.label(class='active') }} + {% for err in field.errors %} + + {% endfor %} + {% else %} + {{ field }} + {{ field.label }} + {% endif %} +{% endmacro %} + +{% macro make_toast(message, style, close_btn_msg) %} +
    + {{ message|safe -}} + +
    +{% endmacro %} + + +{% macro make_modal_form(id, title, form_url, form_id, form_submit_btn_id) %} + + + +{% endmacro %} + +{% macro link_modal(id, class, content, modal_id) %} + {{ content | safe }} +{% endmacro %} diff --git a/app/templates/includes/navbar.html b/app/templates/includes/navbar.html new file mode 100644 index 0000000..5eccc74 --- /dev/null +++ b/app/templates/includes/navbar.html @@ -0,0 +1,39 @@ +{% import "includes/macros.html" as macros %} + + + + + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..ed592b8 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} + +{% block page_js %} + +{% endblock %} + +{% block content %} +
    +
    +
    + +
    Catchy Slogan
    +
    +
    +
    + +
    +
    +{% endblock %} diff --git a/app/util/__init__.py b/app/util/__init__.py new file mode 100644 index 0000000..bf3ec9b --- /dev/null +++ b/app/util/__init__.py @@ -0,0 +1,62 @@ + + +def camel_to_snake(camel_case): + camel_case = camel_case.replace('ID', 'Id') + res = camel_case[0].lower() + for c in camel_case[1:]: + if c.isupper(): + res += f'_{c.lower()}' + continue + res += c + return res + + +def snake_to_camel(snake_case): + res = snake_case.split('_')[0] + res += ''.join(w.title() if w != 'id' else w.upper() for w in snake_case.split('_')[1:]) + return res + + +def convert_dict_names(rename_func, to_convert): + + if isinstance(to_convert, dict): + new = to_convert.__class__() + for key, v in to_convert.items(): + new[rename_func(key)] = convert_dict_names(rename_func, v) + elif isinstance(to_convert, (list, tuple, set)): + new = to_convert.__class__([convert_dict_names(rename_func, item) for item in to_convert]) + else: + return to_convert + return new + + +def remove_dict_values(a_dict, map_to_none_fn): + if isinstance(a_dict, dict): + result = a_dict.__class__() + for k, v in a_dict.items(): + should_ignore = map_to_none_fn(v) + if not should_ignore: + result[k] = remove_dict_values(v, map_to_none_fn) + return result + elif isinstance(a_dict, list): + return [remove_dict_values(v, map_to_none_fn) for v in a_dict] + elif not map_to_none_fn(a_dict): + return a_dict + + +def emtpy_string_to_none(some_dict): + def default_to_none(value): + if not isinstance(value, bool): + if not value: + return True + return False + + return remove_dict_values(some_dict, default_to_none) + + +def dict_to_camel(value): + return convert_dict_names(snake_to_camel, value) + + +def dict_to_snake(value): + return convert_dict_names(camel_to_snake, value) diff --git a/app/util/datatypes/__init__.py b/app/util/datatypes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/util/datatypes/enum.py b/app/util/datatypes/enum.py new file mode 100644 index 0000000..dcc00c6 --- /dev/null +++ b/app/util/datatypes/enum.py @@ -0,0 +1,27 @@ +from enum import Enum, EnumMeta + + +class MetaEnum(EnumMeta): + def __contains__(cls, item): + try: + cls(item) + except ValueError: + return False + return True + + +class BaseEnum(Enum, metaclass=MetaEnum): + def __eq__(self, other): + if isinstance(other, MetaEnum): + return self == other + return self.value == other + + def __hash__(self): + return hash(self.value) + + def __str__(self): + return str(self.value) + + @classmethod + def values(cls): + return list(map(lambda c: c.value, cls)) diff --git a/app/util/log.py b/app/util/log.py new file mode 100644 index 0000000..7ea6cab --- /dev/null +++ b/app/util/log.py @@ -0,0 +1,37 @@ +import logging + +LOG_FORMAT = '%(asctime)s %(app_name)s v%(app_version)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]' + + +class ContextFilter(logging.Filter): + def __init__(self, app_name, app_version): + self.app_name = app_name.upper() + self.app_version = app_version + super().__init__() + + def filter(self, record): + record.app_name = self.app_name + record.app_version = self.app_version + return True + + +def set_up_logging(app, app_name): + log = logging.getLogger('werkzeug') + log.setLevel(logging.WARN) + + logger = app.logger + + for handler in logger.handlers: + logger.removeHandler(handler) + + log_formatter = logging.Formatter(LOG_FORMAT) + + # set up the new handlers + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(log_formatter) + stream_handler.setLevel(logging.INFO) + + logger.addFilter(ContextFilter(app_name, app.config['VERSION'])) + logger.addHandler(stream_handler) + + app.logger.setLevel(logging.INFO) diff --git a/app/util/schema.py b/app/util/schema.py new file mode 100644 index 0000000..31785d7 --- /dev/null +++ b/app/util/schema.py @@ -0,0 +1,10 @@ +import os +import jsonref + + +def load_json_schema(filename): + base_path = os.path.dirname(filename) + base_url = f'file://{base_path}/' + + with open(filename) as schema_file: + return jsonref.loads(schema_file.read(), base_uri=base_url, jsonschema=True) diff --git a/app/util/validators.py b/app/util/validators.py new file mode 100644 index 0000000..1f935fc --- /dev/null +++ b/app/util/validators.py @@ -0,0 +1,62 @@ +import re +import threading +from wtforms import ValidationError +from datetime import datetime +from jsonschema import validate +from jsonschema.exceptions import ValidationError as JSONValidationError + +from .schema import load_json_schema + +_thread_local = threading.local() + + +def validate_schema(data, schema_file): + schema_dict = getattr(_thread_local, 'schema_dict', None) + if schema_dict is None: + _thread_local.schema_dict = {} + + if schema_file not in _thread_local.schema_dict: + _thread_local.schema_dict[schema_file] = load_json_schema(schema_file) + + schema = _thread_local.schema_dict[schema_file] + # try: + validate(instance=data, schema=schema) + # except JSONValidationError as err: + # print(err) + # raise err + + +def _regex_validator(regex_str, err_msg='Value does not match expected format'): + + def validate(form, field): + if re.fullmatch(regex_str, field.data): + return field.data + + raise ValidationError(err_msg) + + return validate + + +def social_security_number(): + ssn_re = '[0-9]{3}[\s.-]?[0-9]{2}[\s.-]?[0-9]{4}' + + return _regex_validator(ssn_re, 'The given SSN is not in the correct format (expected "XXX-XX-XXXX").') + + +def phone_number(): + phone_number_re = '(\+?\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}' + + return _regex_validator(phone_number_re, 'The given phone number is invalid.') + + +def date_of_birth(): + def validate(form, field): + if not field.raw_data or not field.raw_data[0]: + raise ValidationError('This field is required.') + + dob = datetime.strptime(field.raw_data[0], '%b %d, %Y') + if dob.date() > datetime.now().date(): + raise ValidationError('You cannot have been born in the future!') + + return validate + diff --git a/app_common/__init__.py b/app_common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app_common/const.py b/app_common/const.py new file mode 100644 index 0000000..8cd1911 --- /dev/null +++ b/app_common/const.py @@ -0,0 +1,10 @@ +import re + +USER_NOT_ACTIVE_STATUS_CODE = 406 +USER_NOT_ACTIVE_MESSAGE = 'The given user is not active.' + +NOT_UNIQUE = 'NOT_UNIQUE' +MALFORMED_DATA = 'MALFORMED_DATA' +BAD_USER_ROLES = 'BAD_ROLES' + +USER_ID_REGEX = re.compile('[a-z0-9_-]*') diff --git a/app_common/inputs.py b/app_common/inputs.py new file mode 100644 index 0000000..6c14630 --- /dev/null +++ b/app_common/inputs.py @@ -0,0 +1,41 @@ +import re +from datetime import datetime +from app_common.const import USER_ID_REGEX + + +def quarter_date_time(dt_format='%b %d, %Y'): + def validate(value): + try: + value = datetime.strptime(value, dt_format) + except ValueError: + raise ValueError(f'The given string "{value}" does not conform to the date time format string "{dt_format}" ' + f'(see: https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior).') + return value + + return validate + + +def regex_str(regex, error_msg=None): + def validate(value): + if not isinstance(value, str): + raise ValueError('The given value is not a string.') + + if re.search(regex, value): + return value + exp_msg = error_msg or f'The given string does not match the expected regex (value="{value}", regex="{regex}").' + raise ValueError(exp_msg) + return validate + + +def email(): + email_regex = '^(\w|\.|\_|\-)+[@](\w|\_|\-|\.)+[.]\w{2,3}$' + return regex_str(email_regex, error_msg='The given value is not an email address.') + + +def user_id(): + def validate(value): + if USER_ID_REGEX.fullmatch(value): + return value + raise ValueError(f'The given value is not a valid username (value="{value}").') + + return validate diff --git a/app_common/parser.py b/app_common/parser.py new file mode 100644 index 0000000..cf7e865 --- /dev/null +++ b/app_common/parser.py @@ -0,0 +1,15 @@ +from flask_restx.reqparse import RequestParser +from werkzeug.exceptions import BadRequest +from app_common.const import MALFORMED_DATA, BAD_USER_ROLES + + +class QuarterRequestParser(RequestParser): + def parse_args(self, req=None, strict=False): + try: + return super().parse_args(req=req, strict=strict) + except BadRequest as err: + error_type = MALFORMED_DATA + if 'roles' in err.data.get('errors', {}): + error_type = BAD_USER_ROLES + err.data.update({'error_type': error_type}) + raise err diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..921fd5a --- /dev/null +++ b/build.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +show_help() { + echo "Build + A script to build the web app's docker images. + + Usage: build.sh [OPTIONS] + + Options: + -r|--render-templates: Run the render_docker_templates script before building the images. + -h|--help: Shows this help message. + " +} + +cur_dir="$(cd "$( dirname "$0")" && pwd)" +docker_compose_file="$cur_dir/docker/docker-compose.yaml" +render_template_script="$cur_dir/docker/bin/render_docker_templates.sh" +venv_dir="$cur_dir/venv/bin/activate" +render_templates=false + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit + ;; + -r|--render-templates) + render_templates=true + shift 1 + ;; + *) + echo "Invalid argument given ($1)" + exit 1 + ;; + esac +done + +source "$venv_dir" + +# if we need to render the docker templates, then we should do that +if "$render_templates"; then + $render_template_script +fi + +if [[ ! -f "$docker_compose_file" ]]; then + echo "The docker-compose file does not exist (expected at $docker_compose_file)." + echo "Run with --render-templates to render the docker-compose file." + exit 1 +fi + +sudo docker-compose -f "$docker_compose_file" build diff --git a/docker/bin/render_docker_templates.sh b/docker/bin/render_docker_templates.sh new file mode 100644 index 0000000..677d9c1 --- /dev/null +++ b/docker/bin/render_docker_templates.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +line_sep="===============================" + +proj_root_dir="$(cd "$(dirname "$0")/../.." && pwd)" + +render_tmpl_script="$proj_root_dir/scripts/render_templates.py" +docker_root_dir="$proj_root_dir/docker" +mongo_pass="$(uuidgen | tr -d '-')" +flask_secret_key="$(uuidgen | tr -d '-')" + +kwargs="app_root_dir=$proj_root_dir,frontend_build_dir=$proj_root_dir/quarterformsdist,quarter_log_dir=${QUARTER_LOG_DIR:-"$proj_root_dir/quarter_logs"},mongo_pwd=$mongo_pass,secret_key=$flask_secret_key" + +if [[ -z "${IS_DEV}" ]]; then + kwargs="$kwargs,expose_port=27017" +fi + +echo "Calling render_templates.py... +$line_sep +" + +set -e +"$render_tmpl_script" --overwrite --kwargs "$kwargs" "$docker_root_dir/templates" "$docker_root_dir" +set +e + +echo " +$line_sep +Templates rendered successfully!" diff --git a/docker/docker-compose.yaml.example b/docker/docker-compose.yaml.example new file mode 100644 index 0000000..82faeb1 --- /dev/null +++ b/docker/docker-compose.yaml.example @@ -0,0 +1,68 @@ +version: '3.3' +services: + + quarter_web: + build: + context: .. + dockerfile: docker/quarter_web.Dockerfile + container_name: quarter_web + restart: unless-stopped + env_file: + - env + environment: + APP_DEBUG: "False" + MONGODB_DATABASE: quarter + MONGODB_USERNAME: quarter + MONGODB_HOSTNAME: quarter_mongodb + volumes: + - ~/quarter_logs:/var/log/www/ + depends_on: + - mongodb + networks: + - frontend + - backend + + mongodb: + image: mongo:4.4.4 + container_name: quarter_mongodb + restart: unless-stopped + command: [--auth] + env_file: + - env + environment: + MONGO_INITDB_ROOT_USERNAME: "admin" + MONGO_INITDB_ROOT_PASSWORD: "ijfijfijf++" + MONGO_INITDB_DATABASE: "quarter" + MONDODB_LOG_DIR: /dev/null + networks: + - backend + volumes: + - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + - ./mongo/mongo-data:/data/db/ + + webserver: + build: + context: .. + dockerfile: docker/nginx.Dockerfile + image: digitalocean.com/webserver:latest + container_name: webserver + restart: unless-stopped + env_file: + - env + environment: + APP_NAME: "quarter_nginx" + APP_DEBUG: "true" + SERVICE_NAME: "quarter_nginx" + ports: + - "80:80" + - "443:443" + depends_on: + - quarter_web + networks: + - frontend + +networks: + frontend: + driver: bridge + backend: + driver: bridge diff --git a/docker/nginx.Dockerfile b/docker/nginx.Dockerfile new file mode 100644 index 0000000..534cb48 --- /dev/null +++ b/docker/nginx.Dockerfile @@ -0,0 +1,9 @@ +FROM nginx:1.19.7 + +LABEL MAINTAINER="Chris Diesch " + +RUN rm /etc/nginx/conf.d/default.conf + +RUN apt-get update + +COPY docker/nginx.conf /etc/nginx/conf.d/quarter_web.conf diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..c14fff9 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name quarter_web; + + client_max_body_size 264M; + charset utf-8; + + location / { + include uwsgi_params; + uwsgi_param request_rev_msec $msec; + uwsgi_pass quarter_web:5000; + uwsgi_send_timeout 300; + uwsgi_read_timeout 300; + } +} \ No newline at end of file diff --git a/docker/quarter-web.ini b/docker/quarter-web.ini new file mode 100644 index 0000000..02594e3 --- /dev/null +++ b/docker/quarter-web.ini @@ -0,0 +1,16 @@ + +[uwsgi] +socket = 0.0.0.0:5000 +wsgi-file = /var/www/quarter_web.wsgi + +master = true +processes = 5 + +buffer-size = 40960 + +plugins = python + +disable-logging=True +log-5xx=True + +mule = /var/www/app/mules/JWTCleanMule.py diff --git a/docker/quarter_web.Dockerfile b/docker/quarter_web.Dockerfile new file mode 100644 index 0000000..ef7bf40 --- /dev/null +++ b/docker/quarter_web.Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.9.2 + +LABEL MAINTAINER="Chris Diesch " + +WORKDIR /var/www + +RUN apt-get update + +ADD ./app/docker-entrypoint.sh /var/docker-entrypoint.sh +ADD requirements.txt /var/www/requirements.txt + +RUN chmod +x /var/docker-entrypoint.sh + +RUN pip install -Ur /var/www/requirements.txt + +EXPOSE 5000 + +ENTRYPOINT ["/var/docker-entrypoint.sh"] diff --git a/docker/templates/docker-compose.yaml.tmpl b/docker/templates/docker-compose.yaml.tmpl new file mode 100644 index 0000000..0e522ff --- /dev/null +++ b/docker/templates/docker-compose.yaml.tmpl @@ -0,0 +1,75 @@ +version: '3.3' +services: + + quarter_web: + build: + context: .. + dockerfile: docker/quarter_web.Dockerfile + container_name: quarter_web + restart: unless-stopped + env_file: + - env + environment: + APP_DEBUG: "False" + MONGODB_DATABASE: quarter + MONGODB_USERNAME: quarter + MONGODB_HOSTNAME: quarter_mongodb + volumes: + - {{ quarter_log_dir }}:/var/log/www/ + - {{ app_root_dir }}:/var/www/ + - {{ frontend_build_dir }}:/var/www/app/static/webpack/ + - {{ frontend_build_dir }}/index.html:/var/www/app/templates/apply/index.html + depends_on: + - mongodb + networks: + - frontend + - backend + + mongodb: + image: mongo:4.4.4 + container_name: quarter_mongodb + restart: unless-stopped + command: [--auth] + env_file: + - env + environment: + MONGO_INITDB_ROOT_USERNAME: "admin" + MONGO_INITDB_ROOT_PASSWORD: "ijfijfijf++" + MONGO_INITDB_DATABASE: "quarter" + MONDODB_LOG_DIR: /dev/null + networks: + - backend + {% if expose_port %} + ports: + - "27017:{{ expose_port }}" + {% endif %} + volumes: + - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + - ./mongo/mongo-data:/data/db/ + + webserver: + build: + context: .. + dockerfile: docker/nginx.Dockerfile + image: digitalocean.com/webserver:latest + container_name: webserver + restart: unless-stopped + env_file: + - env + environment: + APP_NAME: "quarter_nginx" + APP_DEBUG: "true" + SERVICE_NAME: "quarter_nginx" + ports: + - "80:80" + - "443:443" + depends_on: + - quarter_web + networks: + - frontend + +networks: + frontend: + driver: bridge + backend: + driver: bridge diff --git a/docker/templates/env.tmpl b/docker/templates/env.tmpl new file mode 100644 index 0000000..250b7d9 --- /dev/null +++ b/docker/templates/env.tmpl @@ -0,0 +1,2 @@ +MONGODB_PASSWORD={{ mongo_pwd }} +SECRET_KEY={{ secret_key }} diff --git a/docker/templates/mongo-init.js.tmpl b/docker/templates/mongo-init.js.tmpl new file mode 100644 index 0000000..a2265dd --- /dev/null +++ b/docker/templates/mongo-init.js.tmpl @@ -0,0 +1,19 @@ +user = db.runCommand({usersInfo: {user: "quarter", db: "quarter"}}).users; + +// if the user exists update the password +if (user.length > 0) { + db.changeUserPassword("quarter", "{{ mongo_pwd }}"); +} +// if the user doesn't exist, create it. +else { + db.createUser({ + user: "quarter", + pwd: "{{ mongo_pwd }}", + roles: [ + { + role: "readWrite", + db: "quarter" + } + ] + }); +} \ No newline at end of file diff --git a/docker/uwsgi_entrypoint.sh b/docker/uwsgi_entrypoint.sh new file mode 100644 index 0000000..dea0304 --- /dev/null +++ b/docker/uwsgi_entrypoint.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +uwsgi --ini /var/www/quarter-web.ini diff --git a/quarter_web.wsgi b/quarter_web.wsgi new file mode 100644 index 0000000..46f9578 --- /dev/null +++ b/quarter_web.wsgi @@ -0,0 +1,4 @@ +from app import create_app + +application=create_app("QUARTER_WEB") + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..dd19850 --- /dev/null +++ b/readme.md @@ -0,0 +1,141 @@ +# Quarter Application Backend + +## Development Machine Setup + +There is a helper script that can be run on the inital clone of this repo that will automate several of the instructions below, it is run with: +```shell +./scripts/initial_setup.sh +``` +If that script fails to build the python virtual environments, ensure that `python3.9-dev` is installed on you machine, it can be installed with: +```shell +sudo apt-get install python3.9-dev +``` + +Otherwise after that script is run you can jump to [Building the Docker Images](#building-the-docker-images). + +### Install Pre-requisites +This web app uses several tools to run: + - docker + - docker-compose + - python3.9 + +Docker can be installed by following the instructions on [docker's website](https://docs.docker.com/engine/install/ubuntu/) + +As this application uses python 3.9, that must also be installed along with the python development tools + +### Set up the python virtual environments +This application makes use of python virtual environment, and all the scripts associated with it expect that you are running in the pyton virtual environment so these must also be set up: +```shell +# install the python virutalenv module (only needs to be done if you have not already installed the virtualenv package) +python3.9 -m pip install virtualenv +# create a python virutalenv +python3.9 -m virtualenv venv +# activate the python virtual environment +source venv/bin/activate +# install the requirements +pip install -Ur requirements.txt +``` + + +**NOTE**: + - Whenever the `reuirements.txt` file is updated, the virutal environment must also be updated by running `pip install -Ur requirements.txt` from within the virtual environment. + - All the scripts in this repo are dependant on this virtual environment, so you must activate the virtual environment before trying to run any of them. + + +### Creating the docker-compose environment file +Docker compose uses an environment file which holds environment specific values that needs to be created, there is a helper script for creating this environment file (like all scripts this must be run from the previously created venv): +```shell +./docker/bin/create_env.py +``` + +### Rendering the docker-compose.yaml file +This app is run inside several docker containers, which simplifies both the initial environment set up and deployment of the application, this is dictated by a [docker-compose file](https://docs.docker.com/compose/compose-file/compose-file-v3/). +Since this file contains several variables that are machine-specific, instead a "template" file is stored in the git repository as well a script which will "render" this template. + +To render this template you must run: +```shell +./docker/bin/render_docker_templates.sh +``` + +Once those templates have been rendered, you can build the docker images + +## Building the Docker Images +In order to build the docker images that this app uses, there is a helper script you need to run: +```shell +./build.sh +``` +There is an optional argument that can be included if the docker-compose file needs to be re-rendered: +```shell +./build.sh --render-templates +``` + +This script will build the docker images, and does not to be run for every code change that is made, instead it needs to be run if: + - The `requirements.txt` file is updated. + - Any changes are made to the dockerfiles which define these images. + +In order to start these containers after they have been built, there is another helper script (`./srart.sh`) that is used to start containers from the docker images. This script has two optional arguments that can be included: + - `-b|--build-webpack` Builds the frontend webpack from the `UI` submodule (this is required if the `UI` submodule has been updated). + - `-i|--npm-install`Runs an `npm install` on the webpack in the `UI` submodule (only works if the `--build-webpack` flag is included, is only required if the requirements for the frontend webpack have been updated). + +For the initial startup of the docker containers, both flags must be included: +```shell +./start.sh --build-webpack --npm-install +``` + +**NOTE**: It is not required to stop and start the docker conatiners for front-end only changes (changes to either the frontend webpack code in the `UI` submodule, or the flask template/static files). All that is required is + +However after that these flags should only be included if the frontend submodule code has been changed. + +There is also a helper script for stopping the containers (`./stop.sh`). + +The final helper script is to restart all the containers for the application (`./restart.sh`), this it is required to restart the containers whenever there is a change to the code, but it is not required to do so for changes to the `html` templates or the +frontend webpack (though this webpack does need to be rebuilt). + + +## Building the Frontend Webpack +As mentioned above the frontend code is a submodule of this repository, when making changes to the frontend, there is a helper script that will build the webpack with the appropriate arguments: +```shell +./scripts/build_frontend_webpack.sh +``` + +This script does not by default install any npm packages that the frontend needs, if that is required (either on the first build or if the npm package requirements have changed) it can be run with the `--npm-install` flag: +```shell +./scripts/build_frontend_webpack.sh --npm-install +``` + +After building the frontend webpack, the new website will be viewable at `/apply/home`, it is **NOT** required to restart the docker containers to update this page, however you will need to refresh the page (possibly more than once) to view the new page. An example of this would be: + - Start the docker containers once + ```shell + ./start.sh + ``` + - Changes are made to the frontend code + - Rebuild the frontend webpack + ```shell + ./scripts/build_frontend_webpack.sh + ``` + - navigating to `http://localhost/apply/home` will not display the new webpack. + +Note that the docker containers are never stopped in the above example since the compiled webpack is treated as "tempalte files" by the flask web app. + +## Using git submodules +This repository uses git submodules for the frontend components, these components are held in a [separate git repository](https://github.com/QuarterHomes/Quarterforms) which is set up as a submodule in the `UI` folder. + +### Updating the Submodules +When you checkout or pull this repository, it is important to also make sure you are updating the submodules it has, this can be done with the following command: +```shell +git submodule update --init +``` +**NOTE:** The `--init` flag is only required if the submodule directory (`UI`) is empty. + +When a change is made to the code in the `UI` repository, and it needs to be included in this repository, you must commit the updated submodule. This is the same as adding/committing a file: +```shell +cd UI +# update to the latest "main" branch in the repository to include new changes +git pull +cd .. +# add and commit the new frontend code +git add UI/ +git commit -m "Update the frontend" +``` + +For more information about submodules, see [this article](https://www.atlassian.com/git/tutorials/git-submodule). diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..74fb9d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,29 @@ +web3==5.16.0 +flask==1.1.2 +flask-restx==0.2.0 +wtforms==2.3.3 +pymongo==3.11.3 +voluptuous==0.12.1 +flask-user==1.0.2.2 +flask-mongoengine==1.0.0 +mongoengine==0.23.0 +uwsgi==2.0.19.1 +werkzeug==1.0.1 +pyjwt==2.0.1 +flask-jwt-extended==4.0.2 +google-api-python-client==1.12.8 +google-auth==1.27.0 +google-api-core==1.26.0 +google-auth-httplib2==0.0.4 +googleapis-common-protos==1.53.0 +requests==2.25.1 +flask-cors==3.0.10 +pytest==6.2.2 +pytest-mock==3.5.1 +jsonschema==3.2.0 +jsonref==0.2 +mongomock==3.22.1 +deepdiff==5.2.3 +coverage==5.5 +jinja2==2.11.3 +python-dateutil==2.8.1 diff --git a/restart.sh b/restart.sh new file mode 100644 index 0000000..3748114 --- /dev/null +++ b/restart.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +show_help() { + echo "Restart + The script for restarting the web app. This script calls the necessary commands to restart the containers which house the web app. + + Usage restart.sh [OPTIONS] + + Options: + -b|--build: Runs the build script as part of restarting the web app. + -h|--help: Shows this help message + " +} + +run_build=false + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -b|--build) + run_build=true + shift 1 + ;; + -h|--help) + show_help + exit + ;; + *) + echo "Invalid argument given ($1)" + exit 1 + ;; + esac +done + +cur_dir="$(cd "$( dirname "$0")" && pwd)" + +start_script="$cur_dir/start.sh" +build_script="$cur_dir/build.sh" +stop_script="$cur_dir/stop.sh" + +# any of these failing should cause this script to fail +set -e +$stop_script +# run a build if we are supposed to +if "$run_build"; then + $build_script +fi +$start_script diff --git a/scripts/build_frontend_webpack.sh b/scripts/build_frontend_webpack.sh new file mode 100644 index 0000000..3ae39ae --- /dev/null +++ b/scripts/build_frontend_webpack.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +line_sep="===============================" + +cur_dir="$(cd "$( dirname "$0")" && pwd)" +cur_dir="$(cd $(dirname "$cur_dir") && pwd)" + +run_npm_install=false + +show_help() { + echo "Frontend Webpack Builder + The script for building the frontend webpack to be used by the web app. + + Usage: build_frontend_webpack.sh [options] + + Options: + -i|--npm-install: Runs an npm install before building the webpack. + -h|--help: Shows this help message. + " +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -i|--npm-install) + run_npm_install=true + shift 1 + ;; + -h|--help) + show_help + exit + ;; + *) + echo "Invalid argument given ($1)" + exit 1 + ;; + esac +done + +echo "Building frontend webpack in $cur_dir/UI" + +# Run from the UI directory +cd "$cur_dir/UI" || (echo "UI directory does not exist" && exit 1) + +if "$run_npm_install"; then + echo "$line_sep" + echo "Running NPM install" + echo "$line_sep" + + npm install + + echo "$line_sep" + echo "NPM install complete" + echo "$line_sep" +fi +echo "$line_sep +Running NPM build +$line_sep" +# run the NPM build now +npm run build --production +echo "$line_sep +NPM build complete +$line_sep" + + + + diff --git a/scripts/check_py_dev.py b/scripts/check_py_dev.py new file mode 100644 index 0000000..18c0a96 --- /dev/null +++ b/scripts/check_py_dev.py @@ -0,0 +1,6 @@ +from distutils.sysconfig import get_makefile_filename as m +from os.path import isfile +import sys + + +sys.exit(not isfile(m())) diff --git a/scripts/init_venv.sh b/scripts/init_venv.sh new file mode 100644 index 0000000..e5ff362 --- /dev/null +++ b/scripts/init_venv.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +show_help() { + echo "Init venv + A script to initialize the python virtual environment used for development. + + Usage: init_venv.sh [OPTIONS] + + Options: + -h|--help: Shows this help message. + " +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit + ;; + *) + echo "Invalid argument given ($1)" + exit 1 + ;; + esac +done + +cur_dir="$(cd "$( dirname "$0")" && pwd)" +proj_dir="$(cd "$(dirname "$cur_dir")" && pwd)" +# create the venv +pushd "$proj_dir" || (echo "Unable to access the project directory ($proj_dir)." && exit 1) +python3.9 -m virtualenv venv +# install the requirements +source venv/bin/activate +pip install -Ur requirements.txt diff --git a/scripts/initial_setup.sh b/scripts/initial_setup.sh new file mode 100644 index 0000000..f5f343d --- /dev/null +++ b/scripts/initial_setup.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +line_sep="===============================" + +cur_dir="$(cd "$( dirname "$0")" && pwd)" +proj_dir="$(cd "$(dirname "$cur_dir")" && pwd)" +init_venv_script="$proj_dir/scripts/init_venv.sh" +venv_activate_path="$proj_dir/venv/bin/activate" +check_python_dev_script="$proj_dir/scripts/check_py_dev.py" +render_docker_compose_template_script="$proj_dir/docker/bin/render_docker_templates.sh" +python="python3.9" + +# is python 3.9 installed? +if ! command -v $python &> /dev/null; then + echo "$line_sep" + echo "Installing python 3.9..." + echo "$line_sep" + # fail if something goes wrong + sudo apt-get install python3.9 python3.9-dev || (echo "Failed to install python3.9" && exit 1) + + echo "$line_sep" + echo "Installed python 3.9" + echo "$line_sep" +fi + +# is python dev installed? +if ! command -v "$check_python_dev_script" &> /dev/null; then + echo "python3.9-dev already installed" +# install python3.9-dev +else + echo "Installing python3.9-dev..." + sudo apt-get install python3.9-dev || (echo "Failed to install python3.9-dev" && exit 1) + + echo "$line_sep" + echo "Installed python 3.9-dev" + echo "$line_sep" +fi + + +# is pip installed? +if [[ $(python3.9 -m pip 2>&1) == *"No module named pip" ]]; then + echo "Installing python3 pip..." + curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py || (echo "Failed to download get-pip.py" && exit 1) + $python /tmp/get-pip.py || (echo "get-pip.py failed to run" && exit 1) +fi + + +# make sure that the virtualenv package is installed +virtualenv_exists="$($python -m pip list | grep -F "virtualenv" | xargs)" +virtualenv_exists=${#virtualenv_exists} + +# if the python virtualenv package in not installed, install it +if [[ "$virtualenv_exists" -eq 0 ]]; then + echo "$line_sep" + echo "Installing python virtualenv..." + echo "$line_sep" + + $python -m pip install virtualenv + + echo "$line_sep" + echo "Installed python virtualenv" + echo "$line_sep" +fi + +echo " +$line_sep +Creating python virtual environment +$line_sep" + +$init_venv_script || (echo "Failed to create virtual environment. Is python3.9-dev installed?" && exit 1) + +echo " +$line_sep +Done creating python virtual environment +$line_sep + +$line_sep +Activating the python virtual environment +$line_sep" + +source "$venv_activate_path" || (echo "Failed to source the python virtual environment" && exit 1) + +echo " +$line_sep +Python virtual environment activated +$line_sep + +$line_sep +Rendering the docker compose template +$line_sep" + +$render_docker_compose_template_script || (echo "Failed to render the docker compose file" && exit 1) + +echo " +$line_sep +Rendered the docker compose file +$line_sep + +Done!" diff --git a/scripts/render_templates.py b/scripts/render_templates.py new file mode 100644 index 0000000..c55f1a8 --- /dev/null +++ b/scripts/render_templates.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +import os +import argparse +import traceback +from jinja2 import Environment, FileSystemLoader + + +def render_template(template_file, out_path, **kwargs): + folder, file = os.path.split(template_file) + template_path = os.path.abspath(folder) + env = Environment(loader=FileSystemLoader(template_path), trim_blocks=True, lstrip_blocks=True) + template = env.get_template(file) + + with open(out_path, 'w+') as save: + save.write(template.render(**kwargs)) + + +if __name__ == '__main__': + docker_root_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + def_template_path = os.path.join(docker_root_dir, 'templates') + parser = argparse.ArgumentParser(prog='render_templates.py', description='A script for rendering docker templates', add_help=True) + parser.add_argument('template_path', help='The path to the directory of template files or individual template file to render.') + parser.add_argument('output_path', help='The path to output the rendered template files to.') + parser.add_argument('-k', '--kwargs', required=False, type=str, default='', help='The kwargs to render the template for (format: "Key1=value1,Key2=value2...)') + parser.add_argument('-o', '--overwrite', action='store_true', help='If the existing template renders should be overwritten') + + args = parser.parse_args() + args.kwargs = {a.split('=')[0]: a.split('=')[1] for a in args.kwargs.split(',')} if args.kwargs else {} + + if os.path.isdir(args.template_path) and not os.path.exists(args.template_path): + print(f'The given path "{args.template_path}" does not exist...') + exit(1) + + args.template_path = [os.path.join(args.template_path, p) for p in os.listdir(args.template_path) if p.endswith('.tmpl')] if os.path.isdir(args.template_path) else [args.template_path] + + if not args.template_path: + print('No template (.tmpl) files found') + exit(1) + + if os.path.isdir(args.output_path) and not os.path.exists(args.output_path): + os.makedirs(args.output_dir) + + for tmpl_file in args.template_path: + out_file_name = os.path.split(tmpl_file)[1].replace('.tmpl', '') + out_path = os.path.join(args.output_path, out_file_name) if os.path.isdir(args.output_path) else args.output_path + print(f'Rendering template "{os.path.split(tmpl_file)[1]}" to "{out_path}" with kwargs: {args.kwargs}') + if os.path.exists(out_path) and not args.overwrite: + print(f'Skipping template file because it has already been rendered (use ---overwrite to overwrite the template file).') + continue + try: + render_template(tmpl_file, out_path, **args.kwargs) + print(f'Template rendered successfully') + except Exception as ex: + print(f'Failed to render template (error={str(ex)}).\n{traceback.format_exc()}') diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..ee8df33 --- /dev/null +++ b/start.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +build_webpack=false +run_install=false + +show_help() { + echo "Start + The script for starting the web app. This script calls the necessary commands to start the web app containers from their built images. + + Usage: start.sh [OPTIONS] + + Options: + -b|--build-webpack: Builds the frontend webpack before starting the web app docker containers. + -i|--npm-install: Runs an npm install before building the frontend webpack. + -h|--help: Shows this help message. + " +} + + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -b|--build-webpack) + build_webpack=true + shift 1 + ;; + -i|--npm-install) + run_install=true + shift 1 + ;; + -h|--help) + show_help + exit + ;; + *) + echo "Invalid argument given ($1)" + exit 1 + ;; + esac +done + +cur_dir="$(cd "$( dirname "$0")" && pwd)" +required_files=("$cur_dir/docker/env" "$cur_dir/docker/mongo-init.js") + +line_sep="===============================" + +for file in "${required_files[@]}"; do + if [[ ! -f "$file" ]]; then + echo "A required file is missing (did you run create_env.py and render_docker_templates.sh?). Missing file path: \"$file\"" + exit 1 + fi +done + +if "$build_webpack"; then + build_cmd="$cur_dir/scripts/build_frontend_webpack.sh" + + if "$run_install"; then + build_cmd="$build_cmd --npm-install" + fi + echo "$line_sep" + echo "Running frontend build script ($build_cmd)" + echo "$line_sep" + $build_cmd +else + echo "" + echo "$line_sep" + echo "Not building webpack" + echo "$line_sep + " +fi + +# if there is no built webpack, don't start the containers +if [[ ! -d "$cur_dir/quarterformsdist" ]]; then + echo "The webpack does not appear to be built (run with --build-webpack to build it)." + exit 1 +elif [[ ! -f "$cur_dir/quarterformsdist/index.html" ]]; then + echo "There is no index.html in the webpack build directory!" + exit 1 +fi + +sudo docker-compose -f "$cur_dir/docker/docker-compose.yaml" up -d diff --git a/stop.sh b/stop.sh new file mode 100644 index 0000000..d94a91b --- /dev/null +++ b/stop.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +show_help() { + echo "Stop + A script to stop the web app's docker containers + + Usage: stop.sh [OPTIONS] + + Options: + -h|--help: Shows this help message. + " +} + +while [[ "$#" -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit + ;; + *) + echo "Invalid argument given ($1)" + exit 1 + ;; + esac +done + +cur_dir="$(cd "$( dirname "$0")" && pwd)" + +docker-compose -f "$cur_dir/docker/docker-compose.yaml" down diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..d07067c --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +cur_dir="$(cd "$( dirname "$0")" && pwd)" + +echo "Current directory: $cur_dir" + +coverage run --rcfile "$cur_dir/.coveragerc" -m py pytest "$@" +coverage report --rcfile "$cur_dir/.coveragerc" diff --git a/test/.coverage b/test/.coverage new file mode 100644 index 0000000..8e9e47a Binary files /dev/null and b/test/.coverage differ diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..32ecff2 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,196 @@ +import pytest +import json +from deepdiff import DeepDiff +from collections import namedtuple +from datetime import datetime +from app import create_app, model + +UserMock = namedtuple('UserMock', ['user_id', 'first_name', 'last_name', 'email', 'roles', 'join_date', 'date_of_birth', 'phone_number', 'password']) + +MOCK_USER_1 = UserMock(user_id='test', + first_name='unit', + last_name='test', + email='test@local.com', + roles=[model.Roles.APPLICANT], + join_date=datetime.strptime('2021-03-18', '%Y-%m-%d'), + date_of_birth=datetime.strptime('2003-03-18', '%Y-%m-%d'), + phone_number='123-456-7890', + password='test-local') + +MOCK_ADMIN_USER = UserMock(user_id='test-admin', + first_name='test', + last_name='admin', + email='test@admin.com', + roles=[model.Roles.ADMIN], + join_date=datetime.strptime('2021-03-18', '%Y-%m-%d'), + date_of_birth=datetime.strptime('2003-03-18', '%Y-%m-%d'), + phone_number='111-111-1111', + password='test-admin') + +MOCK_NETWORK_ADMIN_USER = UserMock(user_id='test-network-admin', + first_name='network', + last_name='admin', + email='test@network-admin.com', + roles=[model.Roles.NETWORK_ADMIN], + join_date=datetime.strptime('2021-03-18', '%Y-%m-%d'), + date_of_birth=datetime.strptime('2003-03-18', '%Y-%m-%d'), + phone_number='222-222-2222', + password='test-admin') + +CONFIG_OVERRIDE = { + 'MONGODB_HOST': 'mongomock://localhost', + 'MONGODB_USERNAME': None, + 'MONGODB_PASSWORD': None, + 'TESTING': True, + 'PASSWORD_RESET_EXP_SEC': 1 +} + + +def _assert(value, invert=False, message=None): + value = not value if invert else value + + assert value, message + + +def assert_true(value, message=None): + message = message or 'Value is False' + _assert(value, message=message) + + +def assert_false(value, message=None): + message = message or 'Value is True' + _assert(value, invert=True, message=message) + + +def assert_in(item, container, message=None): + message = message or f'Value missing from container (value={item}, container={container}).' + assert_true(item in container, message=message) + + +def assert_not_in(item, container, message=None): + message = message or f'Unexpected value in container (value={item}, container={container}).' + assert_false(item in container, message=message) + + +def assert_not_none(value, message=None): + message = message or 'Value is None' + assert_false(value is None, message=message) + + +def assert_none(value, message=None): + message = message or f'Value is not None (value={value}).' + assert_true(value is None, message=message) + + +def assert_equal(item_1, item_2, message=None): + message = message or f'Items are not equal (item_1={item_1}, item_2={item_2}).' + assert_true(item_1 == item_2, message=message) + + +def assert_not_equal(item_1, item_2, message=None): + message = message or f'Items are equal (item_1={item_1}, item_2={item_2}).' + assert_false(item_1 == item_2, message=message) + + +def assert_empty(iterable, message=None): + message = message or f'Iterable is not empty (iterable={iterable}).' + assert_equal(len(iterable), 0, message=message) + + +def _assert_status_code(response, status_code, message=None): + message = message or f'Response status code is incorrect (response_code={response.status_code}, expected={status_code}).' + assert_equal(response.status_code, status_code, message=message) + + +def assert_200(response, message=None): + _assert_status_code(response, 200, message=message) + + +def assert_400(response, message=None): + _assert_status_code(response, 400, message=message) + + +def assert_404(response, message=None): + _assert_status_code(response, 404, message=message) + + +def assert_401(response, message=None): + _assert_status_code(response, 401, message=message) + + +def assert_402(response, message=None): + _assert_status_code(response, 402, message=message) + + +def assert_405(response, message=None): + _assert_status_code(response, 405, message=message) + + +def assert_406(response, message=None): + _assert_status_code(response, 406, message=message) + + +def assert_message(response, message, msg=None): + msg = msg or f'The response message did not match (message="{response.json.get("message")}", expected={message}' + assert_equal(response.json.get('message'), message, message=msg) + + +def assert_deep_diff_diffs(item_1, item_2, expected_diff, message=None, **kwargs): + diffs = DeepDiff(item_1, item_2, **kwargs) + assert_response_json_equal(diffs.to_dict(), expected_diff, message=message) + + +def assert_deep_diff_equal(item_1, item_2, message=None, **kwargs): + if 'ignore_order' not in kwargs: + kwargs['ignore_order'] = True + diffs = DeepDiff(item_1, item_2, **kwargs) + assert_empty(diffs, message) + + +def assert_response_json_equal(response, expected, message=None, **kwargs): + if 'ignore_order' not in kwargs: + kwargs['ignore_order'] = True + diffs = DeepDiff(response.json, expected, **kwargs) + message = message or f'Unexpected diffs: {diffs.to_json(indent=2)}' + assert_empty(diffs, message=message) + + +def assert_error_type(response, expected_type, message=None): + error_type = response.json.get('error_type') + message = message or f'Response error type "{error_type}" is incorrect (expected: {expected_type}).' + assert_equal(error_type, expected_type, message=message) + + +def setup_db(): + for mock_user in [MOCK_USER_1, MOCK_ADMIN_USER, MOCK_NETWORK_ADMIN_USER]: + user = model.User( + user_id=mock_user.user_id, + email=mock_user.email, + first_name=mock_user.first_name, + last_name=mock_user.last_name, + join_date=mock_user.join_date, + date_of_birth=mock_user.date_of_birth, + phone_number=mock_user.phone_number, + roles=mock_user.roles, + active=True + ) + user.set_password(mock_user.password) + user.save() + + +def clean_db(): + for user in model.User.objects().all(): + user.delete() + + for app in model.Application.objects().all(): + app.delete() + + +@pytest.fixture +def client(): + app = create_app(config_override=CONFIG_OVERRIDE) + setup_db() + with app.test_client() as test_client: + with app.app_context(): + yield test_client + clean_db() diff --git a/test/test_admin.py b/test/test_admin.py new file mode 100644 index 0000000..ac440e1 --- /dev/null +++ b/test/test_admin.py @@ -0,0 +1,28 @@ +from flask_jwt_extended import create_access_token, decode_token +from app.model import Roles +from .conftest import MOCK_USER_1, MOCK_ADMIN_USER, assert_200, assert_in, assert_equal, assert_response_json_equal, assert_401 + + +def test_access_token(client): + good_token = create_access_token(MOCK_ADMIN_USER) + url = '/api/admin/ajax/access_token' + + # We should get a token for an applicant user if we call this endpoint + headers = {'Auth-Token': f'Bearer {good_token}'} + resp = client.get(url, headers=headers, data={'user_id': MOCK_USER_1.user_id}) + assert_200(resp) + assert_in('access_token', resp.json) + retrieved_token = resp.json['access_token'] + decoded = decode_token(retrieved_token) + assert_equal(decoded['roles'], Roles.APPLICANT) + + # This token should work for applicant endpoints... + resp = client.post('/api/auth/ajax/test_access_token', headers={'Auth-Token': f'Bearer {retrieved_token}'}) + assert_200(resp) + assert_response_json_equal(resp, {'user_id': MOCK_USER_1.user_id}) + + # if we revoke the token, things should fail + resp = client.delete(url, headers=headers, data={'access_token': retrieved_token}) + assert_200(resp) + resp = client.post('/api/auth/ajax/test_access_token', headers={'Auth-Token': f'Bearer {retrieved_token}'}) + assert_401(resp) diff --git a/test/test_applicant_api.py b/test/test_applicant_api.py new file mode 100644 index 0000000..fd6f12b --- /dev/null +++ b/test/test_applicant_api.py @@ -0,0 +1,362 @@ +from flask_jwt_extended import create_access_token +import json +from app import model +from .conftest import MOCK_USER_1, assert_200, assert_response_json_equal, assert_not_none, assert_equal, assert_400, assert_402 + + +def test_get_application(client): + url = '/api/ajax/application' + headers = {'Auth-Token': f'Bearer {create_access_token(MOCK_USER_1)}'} + + resp = client.get(url, headers=headers) + assert_200(resp) + assert_response_json_equal(resp, { + 'applicants': [ + { + 'email': MOCK_USER_1.email, + 'firstName': MOCK_USER_1.first_name, + 'middleInitial': '', + 'lastName': MOCK_USER_1.last_name, + 'numberDependents': 0, + 'agesDependents': [], + 'marriageStatus': 'single', + 'dateOfBirth': MOCK_USER_1.date_of_birth.date().strftime('%b %d, %Y'), + 'liabilities': [], + 'otherIncomes': [], + 'homePhone': MOCK_USER_1.phone_number, + 'previousAddresses': [], + 'previousJobs': [], + 'assets': { + 'assets': [{ + 'depositHeldBy': '', + 'value': 0.0, + 'description': '' + }], + 'checkingSavings': [], + 'lifeInsurance': [], + 'propertyOwned': [], + 'retirementFunds': [], + 'stocksBonds': [], + 'vehiclesOwned': [], + 'otherAssets': '' + } + } + ], + 'finalized': False, + 'propertyInfo': { + 'existingLiens': [], + 'fundingPurpose': 'purchase' + }, + 'filingType': 'single', + 'userID': MOCK_USER_1.user_id + }) + + # There should now be an application object with this user in it + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_not_none(application) + application.property_info = model.Property(original_cost=100000.00, address1='123 Example Lane', city='Boulder', state='CO', zip='80303', number_of_units=1) + applicant = model.Applicant(email='test@test.com', + first_name='another', + last_name='applicant', + assets=model.Assets(life_insurance=[model.LifeInsurance(value=10000.0, face_amount=0.0)]), + previous_jobs=[model.Job(date_from='Mar 10, 2020', date_to='Apr 10, 2021')]) + + application.applicants.append(applicant) + application.save() + resp = client.get(url, headers=headers) + assert_200(resp) + assert_response_json_equal(resp, { + 'applicants': [ + { + 'email': MOCK_USER_1.email, + 'firstName': MOCK_USER_1.first_name, + 'middleInitial': '', + 'lastName': MOCK_USER_1.last_name, + 'numberDependents': 0, + 'agesDependents': [], + 'marriageStatus': 'single', + 'dateOfBirth': MOCK_USER_1.date_of_birth.date().strftime('%b %d, %Y'), + 'liabilities': [], + 'otherIncomes': [], + 'homePhone': MOCK_USER_1.phone_number, + 'previousAddresses': [], + 'previousJobs': [], + 'assets': { + 'assets': [{ + 'depositHeldBy': '', + 'description': '', + 'value': 0.0 + }], + 'checkingSavings': [], + 'lifeInsurance': [], + 'propertyOwned': [], + 'retirementFunds': [], + 'stocksBonds': [], + 'vehiclesOwned': [], + 'otherAssets': '' + } + }, + { + 'email': 'test@test.com', + 'firstName': 'another', + 'lastName': 'applicant', + 'numberDependents': 0, + 'agesDependents': [], + 'liabilities': [], + 'otherIncomes': [], + 'previousAddresses': [], + 'previousJobs': [{ + 'dateFrom': 'Mar 10, 2020', + 'dateTo': 'Apr 10, 2021', + 'yearsMonthsOnJob': '1 Year(s), 1 Month(s)' + }], + 'marriageStatus': 'single', + 'assets': { + 'assets': [{ + 'depositHeldBy': '', + 'description': '', + 'value': 0.0 + }], + 'lifeInsurance': [{ + 'value': 10000.0, + 'faceAmount': 0.0 + }], + 'propertyOwned': [], + 'retirementFunds': [], + 'stocksBonds': [], + 'vehiclesOwned': [], + 'checkingSavings': [], + 'otherAssets': '' + } + } + ], + 'finalized': False, + 'propertyInfo': { + 'fundingPurpose': 'purchase', + 'originalCost': 100000.00, + 'address1': '123 Example Lane', + 'city': 'Boulder', + 'state': 'CO', + 'zip': '80303', + 'numberOfUnits': 1, + 'existingLiens': [] + }, + 'filingType': 'single', + 'userID': MOCK_USER_1.user_id + }) + + # clean up the applications from this test + for app in model.Application.objects().all(): + app.delete() + + +def test_post_application(client): + url = 'api/ajax/application' + headers = {'Auth-Token': f'Bearer {create_access_token(MOCK_USER_1)}'} + test_post_data = { + "filingType": "single", + "depositHeldBy": "", + "applicants": [ + { + "userName": "", + "firstName": "", + "middleInitial": "", + "lastName": "", + "email": f'{MOCK_USER_1.email}', + "pin": "", + "role": "", + "socialSecurityNumber": "", + "currentAddress": "", + "homePhone": "", + "dateOfBirth": "", + "yearsSchool": "", + "degreeEarned": "", + "schoolName": "", + "marriageStatus": "married", + "numberDependants": "", + "agesDependents": [], + "previousJobs": [{ + "employerInfo": "", + "selfEmployed": False, + "yearsOnJob": "", + "dateFrom": "", + "dateTo": "", + "monthlyIncome": None, + "positionTitle": "", + "busPhone": "" + }], + "previousAddresses": [{ + "ownOrRent": "", + "years": "" + }], + "expenseInfo": { + "baseEmpInc": None, + "overTime": None, + "bonuses": None, + "commissions": None, + "dividendsInterest": None, + "netRentalIncome": None, + "other": { + "description": "", + "value": None + } + }, + "otherIncomes": [ + { + "description": "", + "value": None + } + ], + "assets": { + "assets": [], + "checkingSavings": [{ + "bankSlCu": "", + "acctNumber": "", + "value": None + }], + "stocksBonds": [{ + "description": "", + "value": None + }], + "lifeInsurance": [{ + "faceAmount": None, + "value": None + } + ], + "retirementFunds": [{ + "description": "", + "value": None + }], + "vehiclesOwned": [{ + "description": "", + "value": None + }], + "propertyOwned": [{ + "address": "", + "status": "", + "type": "", + "presentMarketValue": None, + "amountOfMortgages": None, + "grossRentalIncome": None, + "mortgagePayments": None, + "insuranceMainTaxesMisc": None, + "netRentalIncome": None + }] + }, + "liabilities": [{ + "description": "", + "value": None + }], + "declarations": { + "anyJudgements": False, + "declaredBankruptcy": False, + "propertyForeclosed": False, + "partyToLawsuit": False, + "obligatedLoanForeclosure": False, + "delinquentOrDefault": False, + "alimonyChildSupportMaintenance": False, + "downPaymentBorrowed": False, + "comakerOrEndorser": False, + "intendPrimaryResidence": False + }, + "acknowledgeAndAgree": { + "signature": None, + "date": "" + }, + "governmentInfo": { + "willNotFurnish": False, + "ethnicity": False, + "sex": "", + "race": "" + } + } + ], + "propertyInfo": {}, + "finalized": False, + "userID": MOCK_USER_1.user_id + } + + resp = client.post(url, headers=headers, data=json.dumps(test_post_data)) + assert_200(resp) + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_not_none(application) + resp = client.get(url, headers=headers) + assert_200(resp) + + # If we send bad data, we should get a 400 and a nice message back + test_post_data['applicants'].append({ + 'email': 'someone@somewhere.com', + 'firstName': 'another', + 'lastName': 'applicant', + 'dateOfBirth': 'Mar 17, 1995', + 'expenseInfo': {'other': {"value": "bad-value"}} + }) + resp = client.post(url, headers=headers, data=json.dumps(test_post_data)) + assert_400(resp) + assert_equal(resp.json['error'], 'Field "applicants.[1].expenseInfo.other.value": \'bad-value\' is not of type \'number\'') + # if we remove the bad data, we should be able to add a new applicant to the application. + test_post_data['applicants'][1].pop('expenseInfo') + resp = client.post(url, headers=headers, data=json.dumps(test_post_data)) + assert_200(resp) + applications = list(model.Application.objects().all()) + # we should have edited an application instead of crating a new one + assert_equal(len(applications), 1) + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_not_none(application) + assert_equal(len(application.applicants), 2) + assert_equal(application.applicants[1].first_name, 'another') + assert_equal(application.applicants[1].last_name, 'applicant') + assert_equal(application.applicants[1].email, 'someone@somewhere.com') + + # we should be able to post JSON data too + test_post_data['applicants'][1]['middleInitial'] = 'G' + resp = client.post(url, headers=headers, json=test_post_data) + assert_200(resp) + # this should have updated the second applicant... + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_equal(len(application.applicants), 2) + assert_equal(application.applicants[1].middle_init_or_name, 'G') + assert_equal(application.applicants[1].email, 'someone@somewhere.com') + + # we should be able to update the property info + test_post_data['propertyInfo'] = { + 'yearBuilt': '1990', + 'yearAcquired': '2000' + } + resp = client.post(url, headers=headers, json=test_post_data) + assert_200(resp) + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_not_none(application) + assert_equal(application.property_info.year_built, '1990') + assert_equal(application.property_info.year_acquired, '2000') + + # we should be able to update the first applicant... + test_post_data['applicants'][0]['dateOfBirth'] = 'Jan 01, 1995' + resp = client.post(url, headers=headers, json=test_post_data) + assert_200(resp) + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_equal(application.applicants[0].date_of_birth, 'Jan 01, 1995') + # trying to post an applicant with no email should cause a 400 + email = test_post_data['applicants'][1].pop('email') + resp = client.post(url, headers=headers, json=test_post_data) + assert_400(resp) + assert_equal(resp.json['error'], 'Field "applicants.[1]": \'email\' is a required property') + # if the request is finalized, it shouldn't be editable + test_post_data['applicants'][1]['email'] = email + resp = client.post(url, headers=headers, json=test_post_data) + assert_200(resp) + + # removing an applicant should be final + del test_post_data['applicants'][1] + resp = client.post(f'{url}/true', headers=headers, json=test_post_data) + assert_200(resp) + application = model.Application.objects(applicants__match={'email': MOCK_USER_1.email}).first() + assert_equal(len(application.applicants), 1) + + # after finalizing, the request should fail + resp = client.post(url, headers=headers, json=test_post_data) + assert_402(resp) + + # clean up after the test + for app in model.Application.objects().all(): + app.delete() diff --git a/test/test_auth_api.py b/test/test_auth_api.py new file mode 100644 index 0000000..5f390b1 --- /dev/null +++ b/test/test_auth_api.py @@ -0,0 +1,317 @@ +from datetime import datetime, timedelta +from time import sleep +from flask_jwt_extended import create_access_token, create_refresh_token +from .conftest import MOCK_USER_1, assert_200, assert_401, assert_404, assert_400, assert_message, assert_response_json_equal, assert_405, assert_in, assert_true, assert_false, \ + assert_equal, assert_402, assert_not_in, assert_error_type, assert_406 +from app.model import Roles, User +from app_common.const import NOT_UNIQUE, BAD_USER_ROLES, MALFORMED_DATA, USER_NOT_ACTIVE_MESSAGE + + +def test_login(client): + url = '/api/auth/ajax/login' + # we should get a 200 with valid user login data but no refresh token by default + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': MOCK_USER_1.password}) + assert_200(resp) + assert_in('access_token', resp.json) + assert_not_in('refresh_token', resp.json) + # if we set remember to false we shouldn't get a refresh token + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': MOCK_USER_1.password, 'remember': False}) + assert_200(resp) + assert_in('access_token', resp.json) + assert_not_in('refresh_token', resp.json) + + # if we set remember to true, we should get a refresh token + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': MOCK_USER_1.password, 'remember': True}) + assert_200(resp) + assert_in('access_token', resp.json) + assert_in('refresh_token', resp.json) + + # if the user is already logged in, we should get an error + headers = {'Auth-Token': f'Bearer {resp.json.get("access_token")}'} + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': MOCK_USER_1.password}, headers=headers) + assert_401(resp) + + # we should get a 401 and a message if the password is incorrect + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': 'bad-password'}) + assert_401(resp) + assert_message(resp, 'Incorrect Password') + + # we should get a 404 if the user does not exist + resp = client.post(url, data={'user_id': 'not-a-user', 'password': 'bad-password'}) + assert_404(resp) + + # if malformed data is passed, we should get an error + resp = client.post(url, data={'this': 'is', 'bad': 'data'}) + assert_400(resp) + assert_error_type(resp, MALFORMED_DATA) + + # trying to post with a non-active user should produce a 406 message and have the correct message + user = User.objects(user_id=MOCK_USER_1.user_id).first() + user.active = False + user.save() + assert_false(User.objects(user_id=MOCK_USER_1.user_id).first().active) + # this should be the case for requests with headers, though that should be impossible without accidentally modifying the database + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': MOCK_USER_1.password}, headers=headers) + assert_406(resp) + assert_message(resp, USER_NOT_ACTIVE_MESSAGE) + # this is the test case that should actually happen + resp = client.post(url, data={'user_id': MOCK_USER_1.user_id, 'password': MOCK_USER_1.password}) + assert_406(resp) + assert_message(resp, USER_NOT_ACTIVE_MESSAGE) + + +def test_register(client, mocker): + # we want to mock sending the confirmation email. + mocker.patch('app.auth.email.send_email_to_user') + url = '/api/auth/ajax/register' + data = { + 'user_id': 'foo', + 'email': 'foo@bar.com', + 'first_name': 'foo', + 'last_name': 'bar', + 'date_of_birth': datetime.utcnow().strftime('%b %d, %Y'), + 'password': '123', + 'phone_number': '222-333-4444', + 'roles': Roles.APPLICANT.value + } + resp = client.post(url, data=data) + assert_200(resp) + + # if the user is already logged in, we should get an error + headers = {'Auth-Token': f'Bearer {create_access_token(MOCK_USER_1)}'} + resp = client.post(url, data=data, headers=headers) + assert_401(resp) + + # we should get an error trying to register the same user... + resp = client.post(url, data=data) + assert_400(resp) + assert_response_json_equal(resp, {'fields': ['user_id', 'email'], 'error_type': NOT_UNIQUE}, exclude_paths=["root['message']"]) + # we should only see the duplicate fields in the response + data['user_id'] = 'bar' + resp = client.post(url, data=data) + assert_400(resp) + assert_response_json_equal(resp, {'fields': ['email'], 'error_type': NOT_UNIQUE}, exclude_paths=["root['message']"]) + # now we should only see the phone number as bad + data['email'] = 'foo@baz.com' + resp = client.post(url, data=data) + assert_200(resp) + + # sending an invalid username should give an error + data['user_id'] = 'Invalid user ID' + data['email'] = 'new@unique.com' + resp = client.post(url, data=data) + assert_400(resp) + assert_error_type(resp, MALFORMED_DATA) + + # sending a bad role should produce a 400 error + data['user_id'] = 'valid_user_id' + data['roles'] = 'bad,roles' + resp = client.post(url, data=data) + assert_400(resp) + assert_error_type(resp, BAD_USER_ROLES) + + # if malformed data is passed, we should get an error + data = { + 'this': 'is', + 'not': 'valid', + 'data': 'to_pass' + } + resp = client.post(url, data=data) + assert_400(resp) + assert_error_type(resp, MALFORMED_DATA) + + +def test_update_password(client, mocker): + url = '/api/auth/ajax/update_password' + access_token = create_access_token(MOCK_USER_1) + headers = {'Auth-Token': f'Bearer {access_token}'} + data = {'old_password': MOCK_USER_1.password, 'new_password': 'foo'} + + resp = client.post(url, data=data, headers=headers) + assert_200(resp) + + # if the password is incorrect, we should get a 401 error + data['old_password'] = 'wrong' + resp = client.post(url, data=data, headers=headers) + assert_401(resp) + + # if there is no access token given, we should get a 401 + resp = client.post(url, data=data) + assert_401(resp) + + # if the access token is revoked, we should get a 401 + mocker.patch('app.auth._is_token_revoked', return_value=False) + resp = client.post(url, data=data, headers=headers) + assert_401(resp) + + +def test_refresh_token(client): + url = '/api/auth/ajax/refresh_access_token' + refresh_token = create_refresh_token(MOCK_USER_1) + headers = {'Auth-Token': f'Bearer {refresh_token}'} + + # if the refresh token is valid, we should get and a new access token that works + resp = client.get(url, headers=headers) + assert_200(resp) + assert_in('access_token', resp.json) + access_token = resp.json['access_token'] + # the new access token should be valid + resp = client.post('/api/auth/ajax/test_access_token', headers={'Auth-Token': f'Bearer {access_token}'}) + assert_200(resp) + + +def test_expired_access_token(client): + url = '/api/auth/ajax/test_access_token' + access_token = create_access_token(MOCK_USER_1, expires_delta=timedelta(seconds=1)) + headers = {'Auth-Token': f'Bearer {access_token}'} + resp = client.post(url, headers=headers) + assert_200(resp) + # sleep for 2 seconds to let the token expire + sleep(2) + resp = client.post(url, headers=headers) + assert_405(resp) + + +def test_logout(client): + url = '/api/auth/ajax/logout' + access_token = create_access_token(MOCK_USER_1) + headers = {'Auth-Token': f'Bearer {access_token}'} + + # If we hit the logout endpoint, we should get a 200 + resp = client.post(url, headers=headers) + assert_200(resp) + # the access token should now be blacklisted and we should get a 401 error + resp = client.post(url, headers=headers) + assert_401(resp) + # if no authentication header is given, we should get a 401 + resp = client.post(url) + assert_401(resp) + + +def test_password_reset(client, mocker): + url = '/api/auth/ajax/request_password_reset' + mock_send_email = mocker.patch('app.auth.api.send_password_reset_email') + + def get_reset_url(): + # the call count for sending the password reset email should go up by 1 + previous_mocked_count = mock_send_email.call_count + response = client.post(url, data={'user_id': MOCK_USER_1.user_id}) + assert_200(response) + assert mock_send_email.call_count == previous_mocked_count + 1 + token = mock_send_email.call_args.args[1] + return f'/api/auth/ajax/reset_password?token={token}' + + # sending a user ID should cause a call to the mocked function + reset_url = get_reset_url() + resp = client.post(reset_url, data={'new_password': 'foobar'}) + assert_200(resp) + # make sure the new user password is what is expected + user = User.objects(user_id=MOCK_USER_1.user_id).first() + assert user.check_password('foobar') + # Trying to reuse the token should fail + resp = client.post(reset_url, data={'new_password': 'foobar1'}) + assert_401(resp) + # if we sleep for over 1 second (the test config has a 1 second timeout) the we should get a 401 for a fresh token + reset_url = get_reset_url() + sleep(2) + resp = client.post(reset_url, data={'new_password': 'foobar1'}) + assert_401(resp) + + # if the password is being set to the same, we should get a 400 + reset_url = get_reset_url() + resp = client.post(reset_url, data={'new_password': 'foobar'}) + assert_400(resp) + assert_in('password_reset_token', resp.json) + assert_error_type(resp, MALFORMED_DATA) + # using the new password reset token should work + reset_url = f'/api/auth/ajax/reset_password?token={resp.json["password_reset_token"]}' + resp = client.post(reset_url, data={'new_password': 'foobar1'}) + assert_200(resp) + + +def test_activate(client): + base_url = 'api/auth/ajax/activate' + + user = User.objects(user_id=MOCK_USER_1.user_id).first() + user.active = False + user.save() + + # make sure the user isn't active + assert_false(user.active) + + token = user.get_activation_token(secret_key=client.application.config['SECRET_KEY']) + url = f'{base_url}?token={token}' + resp = client.post(url) + assert_200(resp) + # the user should now be active + user = User.objects(user_id=MOCK_USER_1.user_id).first() + assert_true(user.active) + + # if the user is already active, we should get a 401 error + resp = client.post(url) + assert_402(resp) + + user.active = False + user.save() + # if the token expires, we should get a 401 error + token = user.get_activation_token(expire_secs=1, secret_key=client.application.config['SECRET_KEY']) + sleep(2) + url = f'{base_url}?token={token}' + resp = client.post(url) + assert_401(resp) + # the user should not have been activated + user = User.objects(user_id=MOCK_USER_1.user_id).first() + assert_false(user.active) + + +def test_send_confirm_email(client, mocker): + mock_send_email = mocker.patch('app.auth.api.send_activate_account_email') + prev_call_count = mock_send_email.call_count + url = '/api/auth/ajax/send_confirm_email' + + user = User.objects(user_id=MOCK_USER_1.user_id).first() + user.active = False + user.save() + assert_false(user.active) + + resp = client.post(url, data={'email': MOCK_USER_1.email}) + assert_200(resp) + assert_equal(mock_send_email.call_count, prev_call_count + 1) + prev_call_count = mock_send_email.call_count + # the token we got should allow us to activate the correct user account + token = mock_send_email.call_args.args[1] + user = user.from_activation_token(token, secret_key=client.application.config['SECRET_KEY']) + assert_equal(user.user_id, MOCK_USER_1.user_id) + # if no user can be found we should get a 404 and no email should be sent + resp = client.post(url, data={'email': 'bad@email.com'}) + assert_404(resp) + assert_equal(prev_call_count, mock_send_email.call_count) + + +def test_send_forgot_user_id_email(client, mocker): + mock_send_email = mocker.patch('app.auth.api.send_forgot_user_id_email') + prev_call_count = mock_send_email.call_count + url = '/api/auth/ajax/forgot_user_id' + + user = User.objects(user_id=MOCK_USER_1.user_id).first() + data = {'email': user.email} + + resp = client.post(url, data=data) + assert_200(resp) + # an email should have been sent + assert_equal(mock_send_email.call_count, prev_call_count + 1) + prev_call_count = mock_send_email.call_count + # the email should have been sent to the correct user's email address + user = mock_send_email.call_args.args[0] + assert_equal(user.email, MOCK_USER_1.email) + + # providing a bad user email should result in a 404 + data['email'] = 'DNE@fake.com' + resp = client.post(url, data=data) + assert_404(resp) + assert_equal(mock_send_email.call_count, prev_call_count) + + # providing a non-email address value should provide a 400 error + data['email'] = 'not an email address' + resp = client.post(url, data=data) + assert_400(resp) diff --git a/test/test_network_admin_api.py b/test/test_network_admin_api.py new file mode 100644 index 0000000..6efd02e --- /dev/null +++ b/test/test_network_admin_api.py @@ -0,0 +1,156 @@ +from datetime import datetime +from flask_jwt_extended import create_access_token +from app.model import Roles, User, Application, Applicant +from app_common.const import BAD_USER_ROLES, MALFORMED_DATA +from .conftest import MOCK_NETWORK_ADMIN_USER, MOCK_USER_1, assert_200, assert_401, assert_404, assert_response_json_equal, assert_in, assert_not_in, assert_400, assert_error_type, \ + assert_equal, assert_message + + +def test_user(client): + url = f'/api/network_admin/ajax/user/{MOCK_USER_1.user_id}' + access_token = create_access_token(MOCK_NETWORK_ADMIN_USER) + headers = {'Auth-Token': f'Bearer {access_token}'} + + resp = client.get(url, headers=headers) + expected = { + 'user_id': MOCK_USER_1.user_id, + 'roles': MOCK_USER_1.roles, + 'email': MOCK_USER_1.email, + 'join_date': MOCK_USER_1.join_date.strftime('%b %d, %Y'), + 'date_of_birth': MOCK_USER_1.date_of_birth.strftime('%b %d, %Y'), + 'phone_number': MOCK_USER_1.phone_number, + 'first_name': MOCK_USER_1.first_name, + 'last_name': MOCK_USER_1.last_name, + 'middle_init_or_name': '' + } + assert_response_json_equal(resp, expected) + + # we should be able to edit the user + data = {k: v for k, v in expected.items()} + data.pop('user_id') + data['email'] = 'new@email.com' + resp = client.post(url, data=data, headers=headers) + assert_200(resp) + # if we get the user, it should be different + resp = client.get(url, headers=headers) + assert_200(resp) + expected.update(data) + assert_response_json_equal(resp, expected) + + # ONLY the network admin users (or admin users) should be able to call this endpoint + bad_access_token = create_access_token(MOCK_USER_1) + headers = {'Auth-Token': f'Bearer {bad_access_token}'} + for func in [client.get, client.put, client.delete, client.post]: + resp = func(url, headers=headers) + assert_401(resp) + + # if we delete the user, it should go away + headers = {'Auth-Token': f'Bearer {access_token}'} + resp = client.delete(url, headers=headers) + assert_200(resp) + for func in [client.get, client.delete, client.post]: + # trying any call should not result in a 404 + resp = func(url, headers=headers) + assert_404(resp) + + # we should now be able to PUT the user + data['password'] = 'foobar' + resp = client.put(url, headers=headers, data=data) + assert_200(resp) + # we should now get the expected data to be set now + data.pop('password') + resp = client.get(url, data=data, headers=headers) + assert_200(resp) + data['join_date'] = datetime.utcnow().date().strftime('%b %d, %Y') + expected.update(data) + assert_response_json_equal(resp, expected) + + +def test_user_roles(client): + user = User.objects(user_id=MOCK_USER_1.user_id).first() + url = f'/api/network_admin/ajax/user/{MOCK_USER_1.user_id}/roles' + data = {'roles': Roles.NETWORK_ADMIN} + access_token = create_access_token(MOCK_NETWORK_ADMIN_USER) + headers = {'Auth-Token': f'Bearer {access_token}'} + assert_not_in(Roles.NETWORK_ADMIN, user.roles) + + # bad roles should produce a "BAD_USER_ROLES" error type + data['roles'] = 'invalid_role' + resp = client.post(url, headers=headers, data=data) + assert_400(resp) + assert_error_type(resp, BAD_USER_ROLES) + + data['roles'] = str(Roles.NETWORK_ADMIN) + # The user should not be a network admin + resp = client.post(url, headers=headers, data=data) + assert_200(resp) + user = User.objects(user_id=MOCK_USER_1.user_id).first() + assert_in(Roles.NETWORK_ADMIN, user.roles) + + # if we do a force, any previous roles should be removed + data['force_match'] = True + resp = client.post(url, headers=headers, data=data) + assert_200(resp) + user = User.objects(user_id=MOCK_USER_1.user_id).first() + assert_equal(user.roles, data['roles'].split(',')) + + # now that user should be able to call the endpoint to change it's role + user_acc_token = create_access_token(user) + data = {'roles': Roles.NETWORK_ADMIN} + headers = {'Auth-Token': f'Bearer {user_acc_token}'} + resp = client.delete(url, headers=headers, data=data) + assert_200(resp) + user = User.objects(user_id=MOCK_USER_1.user_id).first() + assert_not_in(Roles.NETWORK_ADMIN, user.roles) + + # now the user should no longer be able to access that endpoint + user_acc_token = create_access_token(user) + headers = {'Auth-Token': f'Bearer {user_acc_token}'} + resp = client.post(url, headers=headers, data=data) + assert_401(resp) + + # we should get a 404 if we try to do anything with a non existant user + bad_url = f'/api/network_admin/ajax/user/not-a-user/roles' + headers = {'Auth-Token': f'Bearer {access_token}'} + resp = client.get(bad_url, headers=headers) + assert_404(resp) + + +def test_get_users(client): + url = 'api/network_admin/users' + headers = {'Auth-Token': f'Bearer {create_access_token(MOCK_NETWORK_ADMIN_USER)}'} + resp = client.get(url, headers=headers) + assert_200(resp) + assert_equal(len(resp.json), 3) + # now try filtering by only applicants (should return 1 user) + data = {'roles': 'applicant'} + resp = client.get(url, headers=headers, data=data) + assert_200(resp) + assert_equal(len(resp.json), 1) + + # if we also allow network admins, we should get a response of len 2... + data['roles'] = 'applicant,network_admin' + resp = client.get(url, headers=headers, data=data) + assert_equal(len(resp.json), 2) + + # if we send an invalid role + data['roles'] = 'applicant,invalid_role' + resp = client.get(url, headers=headers, data=data) + assert_400(resp) + assert_error_type(resp, BAD_USER_ROLES) + + +def test_get_user_application(client): + url = f'api/network_admin/ajax/user/{MOCK_USER_1.user_id}/application' + headers = {'Auth-Token': f'Bearer {create_access_token(MOCK_NETWORK_ADMIN_USER)}'} + resp = client.get(url, headers=headers) + assert_404(resp) + assert_in('No application found for user', resp.json['message']) + + # add an application and see if we get it + application = Application(applicants=[Applicant(email=MOCK_USER_1.email)], user_id=MOCK_USER_1.user_id) + application.save() + resp = client.get(url, headers=headers) + assert_200(resp) + assert_in('user', resp.json) + assert_in('application', resp.json) diff --git a/uwsgi.py b/uwsgi.py new file mode 100644 index 0000000..2eedf4d --- /dev/null +++ b/uwsgi.py @@ -0,0 +1,4 @@ +from app import create_app + + +application = create_app('QUARTER_WEB')