├── .flaskenv ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── appname ├── __init__.py ├── assets.py ├── controllers │ ├── __init__.py │ └── main.py ├── extensions.py ├── forms.py ├── models.py ├── settings.py ├── static │ ├── css │ │ ├── main.css │ │ └── vendor │ │ │ ├── bootstrap.min.css │ │ │ └── helper.css │ ├── js │ │ ├── main.js │ │ └── vendor │ │ │ ├── bootstrap.min.js │ │ │ └── jquery.min.js │ └── public │ │ └── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff └── templates │ ├── base.html │ ├── index.html │ ├── login.html │ └── restricted.html ├── manage.py ├── requirements.txt └── tests ├── __init__.py ├── conftest.py ├── test_config.py ├── test_login.py ├── test_models.py └── test_urls.py /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP="appname:create_app('appname.settings.DevConfig')" 2 | FLASK_ENV=development 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.swp 4 | *~ 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build/ 11 | eggs 12 | bin 13 | sdist 14 | 15 | #Sphinx builds 16 | _build 17 | 18 | # Installer logs 19 | pip-log.txt 20 | 21 | # Flake8 violation file 22 | violations.flake8.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | __pycache__ 29 | database.db 30 | 31 | # Sphinx 32 | docs/_build 33 | 34 | # Virtual environments 35 | env 36 | env* 37 | 38 | .webassets-cache 39 | .DS_Store 40 | *.sublime-* 41 | .cache/ 42 | 43 | appname/static/public/* 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | - "3.9" 5 | - "pypy" 6 | # command to install dependencies 7 | install: "pip install -r requirements.txt" 8 | # command to run tests 9 | script: make test 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.2 4 | * Added Flask-Login 5 | * Added Modernizr 6 | * updated css and js libraries 7 | * removed typelate 8 | 9 | ## 1.1 10 | * switched to py.test for tests 11 | * form tests 12 | * url tests 13 | * testing database submitting on model tests 14 | * added documentation on how to deploy your application 15 | 16 | ## 1.0 17 | * MVC with blueprints, SQLAlchemy models, and templates 18 | * A makefile 19 | * nose tests 20 | * Flask-Assets css and js management 21 | * Flask-Cache for caching jinja templates 22 | * A Flask-Script management script 23 | * Flask-WTF form management -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Bootstrapy License 2 | ------------------ 3 | 4 | Copyright (c) Kiran Gangadharan, All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 8 | 9 | Flask Foundation License 10 | ------------------------ 11 | 12 | Copyright (c) 2013 Jack Stouffer All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 15 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs test 2 | 3 | help: 4 | @echo " env create a development environment using virtualenv" 5 | @echo " deps install dependencies using pip" 6 | @echo " clean remove unwanted files like .pyc's" 7 | @echo " lint check style with flake8" 8 | @echo " test run all your tests using py.test" 9 | 10 | env: 11 | sudo easy_install pip && \ 12 | pip install virtualenv && \ 13 | virtualenv env && \ 14 | . env/bin/activate && \ 15 | make deps 16 | 17 | deps: 18 | pip install -r requirements.txt 19 | 20 | clean: 21 | python manage.py clean 22 | 23 | lint: 24 | flake8 --exclude=env . 25 | 26 | test: 27 | py.test tests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask Foundation 2 | [![Build Status](https://travis-ci.org/JackStouffer/Flask-Foundation.png)](https://travis-ci.org/JackStouffer/Flask-Foundation) 3 | 4 | There is a cookiecutter version of this repo at [https://github.com/JackStouffer/cookiecutter-Flask-Foundation](https://github.com/JackStouffer/cookiecutter-Flask-Foundation). 5 | 6 | Documentation is located at [https://jackstouffer.com/flask-foundation/](https://jackstouffer.com/flask-foundation/) 7 | 8 | Flask Foundation is a solid foundation for flask applications, built with best practices, that you can easily construct your website/webapp off of. Flask Foundation is different from most Flask frameworks as it does not assume anything about your development or production environments. Flask Foundation is platform agnostic in this respect. 9 | 10 | Built off of the [bootstrapy project](https://github.com/kirang89/bootstrapy) 11 | 12 | Best practices were learned from: 13 | 14 | * [Creating Websites With Flask](http://maximebf.com/blog/2012/10/building-websites-in-python-with-flask/) 15 | * [Getting Bigger With Flask](http://maximebf.com/blog/2012/11/getting-bigger-with-flask/) 16 | * [Larger Applications With Flask](http://flask.pocoo.org/docs/patterns/packages/). 17 | 18 | ## License 19 | 20 | Flask-Foundation is licensed under the BSD license. For more info see LICENSE.md 21 | 22 | # Want to learn more about Flask? 23 | 24 | I also wrote a book on Flask! You can order it [here](https://www.packtpub.com/web-development/mastering-flask). 25 | -------------------------------------------------------------------------------- /appname/__init__.py: -------------------------------------------------------------------------------- 1 | #! ../env/bin/python 2 | 3 | import click 4 | from flask import Flask, current_app, url_for 5 | from flask.cli import with_appcontext 6 | from webassets.loaders import PythonLoader as PythonAssetsLoader 7 | 8 | from appname import assets 9 | from appname.models import db 10 | from appname.controllers.main import main 11 | 12 | from appname.extensions import ( 13 | cache, 14 | assets_env, 15 | debug_toolbar, 16 | login_manager 17 | ) 18 | 19 | 20 | def create_app(object_name): 21 | """ 22 | An flask application factory, as explained here: 23 | http://flask.pocoo.org/docs/patterns/appfactories/ 24 | 25 | Arguments: 26 | object_name: the python path of the config object, 27 | e.g. appname.settings.ProdConfig 28 | """ 29 | 30 | app = Flask(__name__) 31 | 32 | app.config.from_object(object_name) 33 | 34 | # initialize the cache 35 | cache.init_app(app) 36 | 37 | # initialize the debug tool bar 38 | debug_toolbar.init_app(app) 39 | 40 | # initialize SQLAlchemy 41 | db.init_app(app) 42 | 43 | login_manager.init_app(app) 44 | 45 | # Import and register the different asset bundles 46 | assets_env.init_app(app) 47 | assets_loader = PythonAssetsLoader(assets) 48 | for name, bundle in assets_loader.load_bundles().items(): 49 | assets_env.register(name, bundle) 50 | 51 | # register our blueprints 52 | app.register_blueprint(main) 53 | 54 | return app 55 | -------------------------------------------------------------------------------- /appname/assets.py: -------------------------------------------------------------------------------- 1 | from flask_assets import Bundle 2 | 3 | common_css = Bundle( 4 | 'css/vendor/bootstrap.min.css', 5 | 'css/vendor/helper.css', 6 | 'css/main.css', 7 | filters='cssmin', 8 | output='public/css/common.css' 9 | ) 10 | 11 | common_js = Bundle( 12 | 'js/vendor/jquery.min.js', 13 | 'js/vendor/bootstrap.min.js', 14 | Bundle( 15 | 'js/main.js', 16 | filters='jsmin' 17 | ), 18 | output='public/js/common.js' 19 | ) 20 | -------------------------------------------------------------------------------- /appname/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackStouffer/Flask-Foundation/8504bb5c504e958cef794788554c9f7b1f4942da/appname/controllers/__init__.py -------------------------------------------------------------------------------- /appname/controllers/main.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, flash, request, redirect, url_for 2 | from flask_login import login_user, logout_user, login_required 3 | 4 | from appname.extensions import cache 5 | from appname.forms import LoginForm 6 | from appname.models import User 7 | 8 | main = Blueprint('main', __name__) 9 | 10 | 11 | @main.route('/') 12 | @cache.cached(timeout=1000) 13 | def home(): 14 | return render_template('index.html') 15 | 16 | 17 | @main.route("/login", methods=["GET", "POST"]) 18 | def login(): 19 | form = LoginForm() 20 | 21 | if form.validate_on_submit(): 22 | user = User.query.filter_by(username=form.username.data).one() 23 | login_user(user) 24 | 25 | flash("Logged in successfully.", "success") 26 | return redirect(request.args.get("next") or url_for(".home")) 27 | 28 | return render_template("login.html", form=form) 29 | 30 | 31 | @main.route("/logout") 32 | def logout(): 33 | logout_user() 34 | flash("You have been logged out.", "success") 35 | 36 | return redirect(url_for(".home")) 37 | 38 | 39 | @main.route("/restricted") 40 | @login_required 41 | def restricted(): 42 | return render_template("restricted.html") 43 | -------------------------------------------------------------------------------- /appname/extensions.py: -------------------------------------------------------------------------------- 1 | from flask_caching import Cache 2 | from flask_debugtoolbar import DebugToolbarExtension 3 | from flask_login import LoginManager 4 | from flask_assets import Environment 5 | 6 | from appname.models import User 7 | 8 | # Setup flask cache 9 | cache = Cache() 10 | 11 | # init flask assets 12 | assets_env = Environment() 13 | 14 | debug_toolbar = DebugToolbarExtension() 15 | 16 | login_manager = LoginManager() 17 | login_manager.login_view = "main.login" 18 | login_manager.login_message_category = "warning" 19 | 20 | 21 | @login_manager.user_loader 22 | def load_user(userid): 23 | return User.query.get(userid) 24 | -------------------------------------------------------------------------------- /appname/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField 3 | from wtforms import validators 4 | 5 | from .models import User 6 | 7 | 8 | class LoginForm(FlaskForm): 9 | username = StringField(u'Username', validators=[validators.DataRequired()]) 10 | password = PasswordField(u'Password', validators=[validators.optional()]) 11 | 12 | def validate(self): 13 | check_validate = super(LoginForm, self).validate() 14 | 15 | # if our validators do not pass 16 | if not check_validate: 17 | return False 18 | 19 | # Does our the exist 20 | user = User.query.filter_by(username=self.username.data).first() 21 | if not user: 22 | self.username.errors.append('Invalid username or password') 23 | return False 24 | 25 | # Do the passwords match 26 | if not user.check_password(self.password.data): 27 | self.username.errors.append('Invalid username or password') 28 | return False 29 | 30 | return True 31 | -------------------------------------------------------------------------------- /appname/models.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from flask_login import UserMixin, AnonymousUserMixin 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | 5 | db = SQLAlchemy() 6 | 7 | 8 | class User(db.Model, UserMixin): 9 | id = db.Column(db.Integer(), primary_key=True) 10 | username = db.Column(db.String()) 11 | password = db.Column(db.String()) 12 | 13 | def __init__(self, username, password): 14 | self.username = username 15 | self.set_password(password) 16 | 17 | def set_password(self, password): 18 | self.password = generate_password_hash(password) 19 | 20 | def check_password(self, value): 21 | return check_password_hash(self.password, value) 22 | 23 | @property 24 | def is_authenticated(self): 25 | if isinstance(self, AnonymousUserMixin): 26 | return False 27 | else: 28 | return True 29 | 30 | def is_active(self): 31 | return True 32 | 33 | def is_anonymous(self): 34 | if isinstance(self, AnonymousUserMixin): 35 | return True 36 | else: 37 | return False 38 | 39 | def get_id(self): 40 | return self.id 41 | 42 | def __repr__(self): 43 | return '' % self.username 44 | -------------------------------------------------------------------------------- /appname/settings.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | db_file = tempfile.NamedTemporaryFile() 3 | 4 | 5 | class Config(object): 6 | SECRET_KEY = 'REPLACE ME' 7 | 8 | 9 | class ProdConfig(Config): 10 | SERVER_NAME = 'replace me' 11 | ENV = 'prod' 12 | SQLALCHEMY_DATABASE_URI = 'sqlite:///../database.db' 13 | SQLALCHEMY_TRACK_MODIFICATIONS = False 14 | 15 | CACHE_TYPE = 'simple' 16 | 17 | 18 | class DevConfig(Config): 19 | SERVER_NAME = 'localhost:5000' 20 | ENV = 'dev' 21 | DEBUG = True 22 | DEBUG_TB_INTERCEPT_REDIRECTS = False 23 | 24 | SQLALCHEMY_DATABASE_URI = 'sqlite:///../database.db' 25 | SQLALCHEMY_TRACK_MODIFICATIONS = False 26 | 27 | CACHE_TYPE = 'null' 28 | ASSETS_DEBUG = True 29 | 30 | 31 | class TestConfig(Config): 32 | SERVER_NAME = 'localhost:5000' 33 | ENV = 'test' 34 | DEBUG = True 35 | DEBUG_TB_INTERCEPT_REDIRECTS = False 36 | 37 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + db_file.name 38 | SQLALCHEMY_ECHO = True 39 | SQLALCHEMY_TRACK_MODIFICATIONS = False 40 | 41 | CACHE_TYPE = 'null' 42 | WTF_CSRF_ENABLED = False 43 | -------------------------------------------------------------------------------- /appname/static/css/main.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 60px; 3 | } -------------------------------------------------------------------------------- /appname/static/css/vendor/helper.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Image replacement 3 | */ 4 | 5 | .ir { 6 | background-color: transparent; 7 | border: 0; 8 | overflow: hidden; 9 | /* IE 6/7 fallback */ 10 | *text-indent: -9999px; 11 | } 12 | 13 | .ir:before { 14 | content: ""; 15 | display: block; 16 | width: 0; 17 | height: 150%; 18 | } 19 | 20 | /* 21 | * Hide from both screenreaders and browsers: h5bp.com/u 22 | */ 23 | 24 | .hidden { 25 | display: none !important; 26 | visibility: hidden; 27 | } 28 | 29 | /* 30 | * Hide only visually, but have it available for screenreaders: h5bp.com/v 31 | */ 32 | 33 | .visuallyhidden { 34 | border: 0; 35 | clip: rect(0 0 0 0); 36 | height: 1px; 37 | margin: -1px; 38 | overflow: hidden; 39 | padding: 0; 40 | position: absolute; 41 | width: 1px; 42 | } 43 | 44 | /* 45 | * Extends the .visuallyhidden class to allow the element to be focusable 46 | * when navigated to via the keyboard: h5bp.com/p 47 | */ 48 | 49 | .visuallyhidden.focusable:active, 50 | .visuallyhidden.focusable:focus { 51 | clip: auto; 52 | height: auto; 53 | margin: 0; 54 | overflow: visible; 55 | position: static; 56 | width: auto; 57 | } 58 | 59 | /* 60 | * Hide visually and from screenreaders, but maintain layout 61 | */ 62 | 63 | .invisible { 64 | visibility: hidden; 65 | } 66 | 67 | /* 68 | * Clearfix: contain floats 69 | * 70 | * For modern browsers 71 | * 1. The space content is one way to avoid an Opera bug when the 72 | * `contenteditable` attribute is included anywhere else in the document. 73 | * Otherwise it causes space to appear at the top and bottom of elements 74 | * that receive the `clearfix` class. 75 | * 2. The use of `table` rather than `block` is only necessary if using 76 | * `:before` to contain the top-margins of child elements. 77 | */ 78 | 79 | .clearfix:before, 80 | .clearfix:after { 81 | content: " "; /* 1 */ 82 | display: table; /* 2 */ 83 | } 84 | 85 | .clearfix:after { 86 | clear: both; 87 | } 88 | 89 | /* 90 | * For IE 6/7 only 91 | * Include this rule to trigger hasLayout and contain floats. 92 | */ 93 | 94 | .clearfix { 95 | *zoom: 1; 96 | } 97 | 98 | /* ========================================================================== 99 | EXAMPLE Media Queries for Responsive Design. 100 | These examples override the primary ('mobile first') styles. 101 | Modify as content requires. 102 | ========================================================================== */ 103 | 104 | @media only screen and (min-width: 35em) { 105 | /* Style adjustments for viewports that meet the condition */ 106 | } 107 | 108 | @media print, 109 | (-o-min-device-pixel-ratio: 5/4), 110 | (-webkit-min-device-pixel-ratio: 1.25), 111 | (min-resolution: 120dpi) { 112 | /* Style adjustments for high resolution devices */ 113 | } 114 | 115 | /* ========================================================================== 116 | Print styles. 117 | Inlined to avoid required HTTP connection: h5bp.com/r 118 | ========================================================================== */ 119 | 120 | @media print { 121 | * { 122 | background: transparent !important; 123 | color: #000 !important; /* Black prints faster: h5bp.com/s */ 124 | box-shadow: none !important; 125 | text-shadow: none !important; 126 | } 127 | 128 | a, 129 | a:visited { 130 | text-decoration: underline; 131 | } 132 | 133 | a[href]:after { 134 | content: " (" attr(href) ")"; 135 | } 136 | 137 | abbr[title]:after { 138 | content: " (" attr(title) ")"; 139 | } 140 | 141 | /* 142 | * Don't show links for images, or javascript/internal links 143 | */ 144 | 145 | .ir a:after, 146 | a[href^="javascript:"]:after, 147 | a[href^="#"]:after { 148 | content: ""; 149 | } 150 | 151 | pre, 152 | blockquote { 153 | border: 1px solid #999; 154 | page-break-inside: avoid; 155 | } 156 | 157 | thead { 158 | display: table-header-group; /* h5bp.com/t */ 159 | } 160 | 161 | tr, 162 | img { 163 | page-break-inside: avoid; 164 | } 165 | 166 | img { 167 | max-width: 100% !important; 168 | } 169 | 170 | @page { 171 | margin: 0.5cm; 172 | } 173 | 174 | p, 175 | h2, 176 | h3 { 177 | orphans: 3; 178 | widows: 3; 179 | } 180 | 181 | h2, 182 | h3 { 183 | page-break-after: avoid; 184 | } 185 | } -------------------------------------------------------------------------------- /appname/static/js/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackStouffer/Flask-Foundation/8504bb5c504e958cef794788554c9f7b1f4942da/appname/static/js/main.js -------------------------------------------------------------------------------- /appname/static/js/vendor/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.1.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;var e=Object.create(null);return t&&Object.keys(t).forEach((function(i){if("default"!==i){var s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:function(){return t[i]}})}})),e.default=t,Object.freeze(e)}var i=e(t);const s=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i="#"+i.split("#")[1]),e=i&&"#"!==i?i.trim():null}return e},n=t=>{const e=s(t);return e&&document.querySelector(e)?e:null},o=t=>{const e=s(t);return e?document.querySelector(e):null},r=t=>{t.dispatchEvent(new Event("transitionend"))},a=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),l=t=>a(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,c=(t,e,i)=>{Object.keys(i).forEach(s=>{const n=i[s],o=e[s],r=o&&a(o)?"element":null==(l=o)?""+l:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(n).test(r))throw new TypeError(`${t.toUpperCase()}: Option "${s}" provided type "${r}" but expected type "${n}".`)})},h=t=>!(!a(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},g=()=>{},p=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},_=[],m=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(_.length||document.addEventListener("DOMContentLoaded",()=>{_.forEach(t=>t())}),_.push(e)):e()},v=t=>{"function"==typeof t&&t()},y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const o=({target:i})=>{i===e&&(n=!0,e.removeEventListener("transitionend",o),v(t))};e.addEventListener("transitionend",o),setTimeout(()=>{n||r(e)},s)},w=(t,e,i,s)=>{let n=t.indexOf(e);if(-1===n)return t[!i&&s?t.length-1:0];const o=t.length;return n+=i?1:-1,s&&(n=(n+o)%o),t[Math.max(0,Math.min(n,o-1))]},E=/[^.]*(?=\..*)\.|.*/,A=/\..*/,T=/::\d+$/,C={};let k=1;const L={mouseenter:"mouseover",mouseleave:"mouseout"},S=/^(mouseenter|mouseleave)/i,O=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function N(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function D(t){const e=N(t);return t.uidEvent=e,C[e]=C[e]||{},C[e]}function I(t,e,i=null){const s=Object.keys(t);for(let n=0,o=s.length;nfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};s?s=t(s):i=t(i)}const[o,r,a]=x(e,i,s),l=D(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=N(r,e.replace(E,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return n.delegateTarget=r,s.oneOff&&H.off(t,n.type,e,i),i.apply(r,[n]);return null}}(t,i,s):function(t,e){return function i(s){return s.delegateTarget=t,i.oneOff&&H.off(t,s.type,e),e.apply(t,[s])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function M(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function j(t){return t=t.replace(A,""),L[t]||t}const H={on(t,e,i,s){P(t,e,i,s,!1)},one(t,e,i,s){P(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=x(e,i,s),a=r!==e,l=D(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void M(t,l,r,o,n?i:null)}c&&Object.keys(l).forEach(i=>{!function(t,e,i,s){const n=e[i]||{};Object.keys(n).forEach(o=>{if(o.includes(s)){const s=n[o];M(t,e,i,s.originalHandler,s.delegationSelector)}})}(t,l,i,e.slice(1))});const h=l[r]||{};Object.keys(h).forEach(i=>{const s=i.replace(T,"");if(!a||e.includes(s)){const e=h[i];M(t,l,r,e.originalHandler,e.delegationSelector)}})},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f(),n=j(e),o=e!==n,r=O.has(n);let a,l=!0,c=!0,h=!1,d=null;return o&&s&&(a=s.Event(e,i),s(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(n,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(d,t,{get:()=>i[t]})}),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},B=new Map;var z={set(t,e,i){B.has(t)||B.set(t,new Map);const s=B.get(t);s.has(e)||0===s.size?s.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,e)=>B.has(t)&&B.get(t).get(e)||null,remove(t,e){if(!B.has(t))return;const i=B.get(t);i.delete(e),0===i.size&&B.delete(t)}};class R{constructor(t){(t=l(t))&&(this._element=t,z.set(this._element,this.constructor.DATA_KEY,this))}dispose(){z.remove(this._element,this.constructor.DATA_KEY),H.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach(t=>{this[t]=null})}_queueCallback(t,e,i=!0){y(t,e,i)}static getInstance(t){return z.get(l(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.0"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return"bs."+this.NAME}static get EVENT_KEY(){return"."+this.DATA_KEY}}const F=(t,e="hide")=>{const i="click.dismiss"+t.EVENT_KEY,s=t.NAME;H.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=o(this)||this.closest("."+s);t.getOrCreateInstance(n)[e]()}))};class W extends R{static get NAME(){return"alert"}close(){if(H.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),H.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}F(W,"close"),b(W);class $ extends R{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=$.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function U(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}H.on(document,"click.bs.button.data-api",'[data-bs-toggle="button"]',t=>{t.preventDefault();const e=t.target.closest('[data-bs-toggle="button"]');$.getOrCreateInstance(e).toggle()}),b($);const K={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+U(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+U(e))},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter(t=>t.startsWith("bs")).forEach(i=>{let s=i.replace(/^bs/,"");s=s.charAt(0).toLowerCase()+s.slice(1,s.length),e[s]=q(t.dataset[i])}),e},getDataAttribute:(t,e)=>q(t.getAttribute("data-bs-"+U(e))),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let s=t.parentNode;for(;s&&s.nodeType===Node.ELEMENT_NODE&&3!==s.nodeType;)s.matches(e)&&i.push(s),s=s.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>t+':not([tabindex^="-"])').join(", ");return this.find(e,t).filter(t=>!d(t)&&h(t))}},X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z};class et extends R{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return"carousel"}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(r(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(".active.carousel-item",this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void H.one(this._element,"slid.bs.carousel",()=>this.to(t));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...K.getDataAttributes(this._element),..."object"==typeof t?t:{}},c("carousel",t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&H.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(H.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),H.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},e=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},i=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach(t=>{H.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(H.on(this._element,"pointerdown.bs.carousel",e=>t(e)),H.on(this._element,"pointerup.bs.carousel",t=>i(t)),this._element.classList.add("pointer-event")):(H.on(this._element,"touchstart.bs.carousel",e=>t(e)),H.on(this._element,"touchmove.bs.carousel",t=>e(t)),H.on(this._element,"touchend.bs.carousel",t=>i(t)))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return w(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),s=this._getItemIndex(V.findOne(".active.carousel-item",this._element));return H.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:s,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove("active"),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{H.trigger(this._element,"slid.bs.carousel",{relatedTarget:o,direction:d,from:n,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),p(o),s.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add("active"),s.classList.remove("active",h,c),this._isSliding=!1,setTimeout(u,0)};this._queueCallback(t,s,!0)}else s.classList.remove("active"),o.classList.add("active"),this._isSliding=!1,u();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=et.getOrCreateInstance(t,e);let{_config:s}=i;"object"==typeof e&&(s={...s,...e});const n="string"==typeof e?e:s.slide;if("number"==typeof e)i.to(e);else if("string"==typeof n){if(void 0===i[n])throw new TypeError(`No method named "${n}"`);i[n]()}else s.interval&&s.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){et.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=o(this);if(!e||!e.classList.contains("carousel"))return;const i={...K.getDataAttributes(e),...K.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),et.carouselInterface(e,i),s&&et.getInstance(e).to(s),t.preventDefault()}}H.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",et.dataApiClickHandler),H.on(window,"load.bs.carousel.data-api",()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element);null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return it}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(".collapse .collapse",this._config.parent);e=V.find(".show, .collapsing",this._config.parent).filter(e=>!t.includes(e))}const i=V.findOne(this._selector);if(e.length){const s=e.find(t=>i!==t);if(t=s?nt.getInstance(s):null,t&&t._isTransitioning)return}if(H.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach(e=>{i!==e&&nt.getOrCreateInstance(e,{toggle:!1}).hide(),t||z.set(e,"bs.collapse",null)});const s=this._getDimension();this._element.classList.remove("collapse"),this._element.classList.add("collapsing"),this._element.style[s]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const n="scroll"+(s[0].toUpperCase()+s.slice(1));this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse","show"),this._element.style[s]="",H.trigger(this._element,"shown.bs.collapse")},this._element,!0),this._element.style[s]=this._element[n]+"px"}hide(){if(this._isTransitioning||!this._isShown())return;if(H.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",p(this._element),this._element.classList.add("collapsing"),this._element.classList.remove("collapse","show");const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove("collapsing"),this._element.classList.add("collapse"),H.trigger(this._element,"hidden.bs.collapse")},this._element,!0)}_isShown(t=this._element){return t.classList.contains("show")}_getConfig(t){return(t={...it,...K.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=l(t.parent),c("collapse",t,st),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(".collapse .collapse",this._config.parent);V.find('[data-bs-toggle="collapse"]',this._config.parent).filter(e=>!t.includes(e)).forEach(t=>{const e=o(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))})}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach(t=>{e?t.classList.remove("collapsed"):t.classList.add("collapsed"),t.setAttribute("aria-expanded",e)})}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=nt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}H.on(document,"click.bs.collapse.data-api",'[data-bs-toggle="collapse"]',(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=n(this);V.find(e).forEach(t=>{nt.getOrCreateInstance(t,{toggle:!1}).toggle()})})),b(nt);const ot=new RegExp("ArrowUp|ArrowDown|Escape"),rt=m()?"top-end":"top-start",at=m()?"top-start":"top-end",lt=m()?"bottom-end":"bottom-start",ct=m()?"bottom-start":"bottom-end",ht=m()?"left-start":"right-start",dt=m()?"right-start":"left-start",ut={offset:[0,2],boundary:"clippingParents",reference:"toggle",display:"dynamic",popperConfig:null,autoClose:!0},gt={offset:"(array|string|function)",boundary:"(string|element)",reference:"(string|element|object)",display:"string",popperConfig:"(null|object|function)",autoClose:"(boolean|string)"};class pt extends R{constructor(t,e){super(t),this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar()}static get Default(){return ut}static get DefaultType(){return gt}static get NAME(){return"dropdown"}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown(this._menu))return;const t={relatedTarget:this._element};if(H.trigger(this._element,"show.bs.dropdown",t).defaultPrevented)return;const e=pt.getParentFromElement(this._element);this._inNavbar?K.setDataAttribute(this._menu,"popper","none"):this._createPopper(e),"ontouchstart"in document.documentElement&&!e.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>H.on(t,"mouseover",g)),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add("show"),this._element.classList.add("show"),H.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(d(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){H.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>H.off(t,"mouseover",g)),this._popper&&this._popper.destroy(),this._menu.classList.remove("show"),this._element.classList.remove("show"),this._element.setAttribute("aria-expanded","false"),K.removeDataAttribute(this._menu,"popper"),H.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...K.getDataAttributes(this._element),...t},c("dropdown",t,this.constructor.DefaultType),"object"==typeof t.reference&&!a(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError("dropdown".toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.');return t}_createPopper(t){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:a(this._config.reference)?e=l(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const s=this._getPopperConfig(),n=s.modifiers.find(t=>"applyStyles"===t.name&&!1===t.enabled);this._popper=i.createPopper(e,this._menu,s),n&&K.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains("show")}_getMenuElement(){return V.next(this._element,".dropdown-menu")[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ht;if(t.classList.contains("dropstart"))return dt;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?at:rt:e?ct:lt}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(h);i.length&&w(i,e,"ArrowDown"===t,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=pt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find('[data-bs-toggle="dropdown"]');for(let i=0,s=e.length;ie+t),this._setElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight",e=>e+t),this._setElementAttributes(".sticky-top","marginRight",e=>e-t)}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t)[e];t.style[e]=i(Number.parseFloat(n))+"px"})}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top","paddingRight"),this._resetElementAttributes(".sticky-top","marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&K.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=K.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(K.removeDataAttribute(t,e),t.style[e]=i)})}_applyManipulationCallback(t,e){a(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const _t={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mt={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"};class bt{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&p(this._getElement()),this._getElement().classList.add("show"),this._emulateAnimation(()=>{v(t)})):v(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove("show"),this._emulateAnimation(()=>{this.dispose(),v(t)})):v(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={..._t,..."object"==typeof t?t:{}}).rootElement=l(t.rootElement),c("backdrop",t,mt),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),H.on(this._getElement(),"mousedown.bs.backdrop",()=>{v(this._config.clickCallback)}),this._isAppended=!0)}dispose(){this._isAppended&&(H.off(this._element,"mousedown.bs.backdrop"),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const vt={trapElement:null,autofocus:!0},yt={trapElement:"element",autofocus:"boolean"};class wt{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),H.off(document,".bs.focustrap"),H.on(document,"focusin.bs.focustrap",t=>this._handleFocusin(t)),H.on(document,"keydown.tab.bs.focustrap",t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,H.off(document,".bs.focustrap"))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const s=V.focusableChildren(i);0===s.length?i.focus():"backward"===this._lastTabNavDirection?s[s.length-1].focus():s[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?"backward":"forward")}_getConfig(t){return t={...vt,..."object"==typeof t?t:{}},c("focustrap",t,yt),t}}const Et={backdrop:!0,keyboard:!0,focus:!0},At={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"};class Tt extends R{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new ft}static get Default(){return Et}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||H.trigger(this._element,"show.bs.modal",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add("modal-open"),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),H.on(this._dialog,"mousedown.dismiss.bs.modal",()=>{H.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(){if(!this._isShown||this._isTransitioning)return;if(H.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove("show"),H.off(this._element,"click.dismiss.bs.modal"),H.off(this._dialog,"mousedown.dismiss.bs.modal"),this._queueCallback(()=>this._hideModal(),this._element,t)}dispose(){[window,this._dialog].forEach(t=>H.off(t,".bs.modal")),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new wt({trapElement:this._element})}_getConfig(t){return t={...Et,...K.getDataAttributes(this._element),..."object"==typeof t?t:{}},c("modal",t,At),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&p(this._element),this._element.classList.add("show"),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,H.trigger(this._element,"shown.bs.modal",{relatedTarget:t})},this._dialog,e)}_setEscapeEvent(){this._isShown?H.on(this._element,"keydown.dismiss.bs.modal",t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):H.off(this._element,"keydown.dismiss.bs.modal")}_setResizeEvent(){this._isShown?H.on(window,"resize.bs.modal",()=>this._adjustDialog()):H.off(window,"resize.bs.modal")}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove("modal-open"),this._resetAdjustments(),this._scrollBar.reset(),H.trigger(this._element,"hidden.bs.modal")})}_showBackdrop(t){H.on(this._element,"click.dismiss.bs.modal",t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())}),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(H.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,s=e>document.documentElement.clientHeight;!s&&"hidden"===i.overflowY||t.contains("modal-static")||(s||(i.overflowY="hidden"),t.add("modal-static"),this._queueCallback(()=>{t.remove("modal-static"),s||this._queueCallback(()=>{i.overflowY=""},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=e+"px"),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=e+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Tt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}H.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=o(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),H.one(e,"show.bs.modal",t=>{t.defaultPrevented||H.one(e,"hidden.bs.modal",()=>{h(this)&&this.focus()})}),Tt.getOrCreateInstance(e).toggle(this)})),F(Tt),b(Tt);const Ct={backdrop:!0,keyboard:!0,scroll:!1},kt={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"};class Lt extends R{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return"offcanvas"}static get Default(){return Ct}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||H.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new ft).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),this._queueCallback(()=>{this._config.scroll||this._focustrap.activate(),H.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(H.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),this._backdrop.hide(),this._queueCallback(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new ft).reset(),H.trigger(this._element,"hidden.bs.offcanvas")},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ct,...K.getDataAttributes(this._element),..."object"==typeof t?t:{}},c("offcanvas",t,kt),t}_initializeBackDrop(){return new bt({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new wt({trapElement:this._element})}_addEventListeners(){H.on(this._element,"keydown.dismiss.bs.offcanvas",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()})}static jQueryInterface(t){return this.each((function(){const e=Lt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}H.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=o(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;H.one(e,"hidden.bs.offcanvas",()=>{h(this)&&this.focus()});const i=V.findOne(".offcanvas.show");i&&i!==e&&Lt.getInstance(i).hide(),Lt.getOrCreateInstance(e).toggle(this)})),H.on(window,"load.bs.offcanvas.data-api",()=>V.find(".offcanvas.show").forEach(t=>Lt.getOrCreateInstance(t).show())),F(Lt),b(Lt);const St=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Ot=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,Nt=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Dt=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!St.has(i)||Boolean(Ot.test(t.nodeValue)||Nt.test(t.nodeValue));const s=e.filter(t=>t instanceof RegExp);for(let t=0,e=s.length;t{Dt(t,a)||i.removeAttribute(t.nodeName)})}return s.body.innerHTML}const xt=new Set(["sanitize","allowList","sanitizeFn"]),Pt={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Mt={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},jt={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},Ht={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"};class Bt extends R{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return jt}static get NAME(){return"tooltip"}static get Event(){return Ht}static get DefaultType(){return Pt}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains("show"))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),H.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.remove(),this._popper&&this._popper.destroy(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=H.trigger(this._element,this.constructor.Event.SHOW),e=u(this._element),s=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!s)return;const n=this.getTipElement(),o=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",o),this._element.setAttribute("aria-describedby",o),this._config.animation&&n.classList.add("fade");const r="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,a=this._getAttachment(r);this._addAttachmentClass(a);const{container:l}=this._config;z.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(l.append(n),H.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=i.createPopper(this._element,n,this._getPopperConfig(a)),n.classList.add("show");const c=this._resolvePossibleFunction(this._config.customClass);c&&n.classList.add(...c.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{H.on(t,"mouseover",g)});const h=this.tip.classList.contains("fade");this._queueCallback(()=>{const t=this._hoverState;this._hoverState=null,H.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)},this.tip,h)}hide(){if(!this._popper)return;const t=this.getTipElement();if(H.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove("show"),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>H.off(t,"mouseover",g)),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains("fade");this._queueCallback(()=>{this._isWithActiveTrigger()||("show"!==this._hoverState&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),H.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))},this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove("fade","show"),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".tooltip-inner")}_sanitizeAndSetContent(t,e,i){const s=V.findOne(i,t);e||!s?this.setElementContent(s,e):s.remove()}setElementContent(t,e){if(null!==t)return a(e)?(e=l(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=It(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Mt[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach(t=>{if("click"===t)H.on(this._element,this.constructor.Event.CLICK,this._config.selector,t=>this.toggle(t));else if("manual"!==t){const e="hover"===t?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i="hover"===t?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;H.on(this._element,e,this._config.selector,t=>this._enter(t)),H.on(this._element,i,this._config.selector,t=>this._leave(t))}}),this._hideModalHandler=()=>{this._element&&this.hide()},H.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e.getTipElement().classList.contains("show")||"show"===e._hoverState?e._hoverState="show":(clearTimeout(e._timeout),e._hoverState="show",e._config.delay&&e._config.delay.show?e._timeout=setTimeout(()=>{"show"===e._hoverState&&e.show()},e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?"focus":"hover"]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e._config.delay&&e._config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=K.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{xt.has(t)&&delete e[t]}),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:l(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),c("tooltip",t,this.constructor.DefaultType),t.sanitize&&(t.template=It(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map(t=>t.trim()).forEach(e=>t.classList.remove(e))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}static jQueryInterface(t){return this.each((function(){const e=Bt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Bt);const zt={...Bt.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},Rt={...Bt.DefaultType,content:"(string|element|function)"},Ft={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class Wt extends Bt{static get Default(){return zt}static get NAME(){return"popover"}static get Event(){return Ft}static get DefaultType(){return Rt}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=Wt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Wt);const $t={offset:10,method:"auto",target:""},qt={offset:"number",method:"string",target:"(string|element)"},Ut=".nav-link, .list-group-item, .dropdown-item";class Kt extends R{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,H.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return $t}static get NAME(){return"scrollspy"}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":"position",e="auto"===this._config.method?t:this._config.method,i="position"===e?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(Ut,this._config.target).map(t=>{const s=n(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[K[e](o).top+i,s]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){H.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...$t,...K.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=l(t.target)||document.documentElement,c("scrollspy",t,qt),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`),i=V.findOne(e.join(","),this._config.target);i.classList.add("active"),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add("active"):V.parents(i,".nav, .list-group").forEach(t=>{V.prev(t,".nav-link, .list-group-item").forEach(t=>t.classList.add("active")),V.prev(t,".nav-item").forEach(t=>{V.children(t,".nav-link").forEach(t=>t.classList.add("active"))})}),H.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(Ut,this._config.target).filter(t=>t.classList.contains("active")).forEach(t=>t.classList.remove("active"))}static jQueryInterface(t){return this.each((function(){const e=Kt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}H.on(window,"load.bs.scrollspy.data-api",()=>{V.find('[data-bs-spy="scroll"]').forEach(t=>new Kt(t))}),b(Kt);class Vt extends R{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains("active"))return;let t;const e=o(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?":scope > li > .active":".active";t=V.find(e,i),t=t[t.length-1]}const s=t?H.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(H.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const n=()=>{H.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),H.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,n):n()}_activate(t,e,i){const s=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,".active"):V.find(":scope > li > .active",e))[0],n=i&&s&&s.classList.contains("fade"),o=()=>this._transitionComplete(t,s,i);s&&n?(s.classList.remove("show"),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove("active");const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove("active"),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),p(t),t.classList.contains("fade")&&t.classList.add("show");let s=t.parentNode;if(s&&"LI"===s.nodeName&&(s=s.parentNode),s&&s.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach(t=>t.classList.add("active")),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=Vt.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}H.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||Vt.getOrCreateInstance(this).show()})),b(Vt);const Xt={animation:"boolean",autohide:"boolean",delay:"number"},Yt={animation:!0,autohide:!0,delay:5e3};class Qt extends R{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Xt}static get Default(){return Yt}static get NAME(){return"toast"}show(){H.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove("hide"),p(this._element),this._element.classList.add("show"),this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.remove("showing"),H.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this._element.classList.contains("show")&&(H.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add("showing"),this._queueCallback(()=>{this._element.classList.add("hide"),this._element.classList.remove("showing"),this._element.classList.remove("show"),H.trigger(this._element,"hidden.bs.toast")},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains("show")&&this._element.classList.remove("show"),super.dispose()}_getConfig(t){return t={...Yt,...K.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},c("toast",t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){H.on(this._element,"mouseover.bs.toast",t=>this._onInteraction(t,!0)),H.on(this._element,"mouseout.bs.toast",t=>this._onInteraction(t,!1)),H.on(this._element,"focusin.bs.toast",t=>this._onInteraction(t,!0)),H.on(this._element,"focusout.bs.toast",t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Qt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return F(Qt),b(Qt),{Alert:W,Button:$,Carousel:et,Collapse:nt,Dropdown:pt,Modal:Tt,Offcanvas:Lt,Popover:Wt,ScrollSpy:Kt,Tab:Vt,Toast:Qt,Tooltip:Bt}})); -------------------------------------------------------------------------------- /appname/static/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackStouffer/Flask-Foundation/8504bb5c504e958cef794788554c9f7b1f4942da/appname/static/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /appname/static/public/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /appname/static/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackStouffer/Flask-Foundation/8504bb5c504e958cef794788554c9f7b1f4942da/appname/static/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /appname/static/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackStouffer/Flask-Foundation/8504bb5c504e958cef794788554c9f7b1f4942da/appname/static/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /appname/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | 12 | {% block meta %} 13 | 14 | 15 | {% endblock %} 16 | 17 | {% assets "common_css" %} 18 | 19 | {% endassets %} 20 | 21 | {% block css %} 22 | {% endblock %} 23 | 24 | 25 | 26 | 55 | 56 |
57 | {% with messages = get_flashed_messages(with_categories=true) %} 58 | {% if messages %} 59 | {% for category, message in messages %} 60 | 64 | {% endfor %} 65 | {% endif %} 66 | {% endwith %} 67 | 68 | {% block body %} 69 | {% endblock %} 70 |
71 | 72 | {% assets "common_js" %} 73 | 74 | {% endassets %} 75 | 76 | {% block js %} 77 | {% endblock %} 78 | 79 | 80 | -------------------------------------------------------------------------------- /appname/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Home{% endblock %} 4 | 5 | {% block body %} 6 |
7 |

Flask Foundation

8 |

The unopinionated flask framework

9 |
10 | 11 |
12 |
13 |

What's Included

14 |
15 |
16 | 17 |
18 |
19 |

MVC Structure

20 |

While there are many other flask templates out there, far too many are either structured poorly or are tied down to a specific method of deployment, or both.

21 |

So Flask Foundation has a best practices setup with blueprints and SQLAlchemy models that will allow you to grow cleanly

22 |
23 | 24 |
25 |

Flask Extensions

26 |

Several flask extensions are included to handle the common tasks of web developers. The included extensions are flask-login, flask-assets, flask-WTF, flask-caching, and flask-sqlalchemy.

27 |
28 | 29 |
30 |

Management Scripts

31 |

Using the Makefile, you can easily setup your development environment with a few simple commands. With the power of flask-script you can create your own commands with the flask app context setup for you.

32 |
33 | 34 |
35 |

Tests

36 |

Example tests, built with py.test, are included in to demonstrate how to test a flask app.

37 |
38 | 39 |
40 |

Common Libraries

41 |

The CSS and JS files for Bootstrap are included, along with jQuery.

42 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /appname/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Flask Login Example{% endblock %} 4 | 5 | {% block body %} 6 |

Flask Login Example

7 | 8 |
9 |
10 |
11 |
12 | {{ form.hidden_tag() }} 13 | 14 |
15 | {{ form.username.label(class_="form-label") }} 16 | {{ form.username(class_="form-control") }} 17 | {% if form.username.errors %} 18 | {% for e in form.username.errors %} 19 |

{{ e }}

20 | {% endfor %} 21 | {% endif %} 22 |
23 |
24 | {{ form.password.label(class_="form-label") }} 25 | {{ form.password(class_="form-control") }} 26 | {% if form.password.errors %} 27 | {% for e in form.password.errors %} 28 |

{{ e }}

29 | {% endfor %} 30 | {% endif %} 31 |
32 | 33 | 34 |
35 |
36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /appname/templates/restricted.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Restricted{% endblock %} 4 | 5 | {% block body %} 6 |

This page is restricted!

7 | {% endblock %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import urllib 5 | 6 | import click 7 | from flask import current_app, url_for 8 | from flask.cli import FlaskGroup 9 | 10 | from appname import create_app 11 | from appname.models import db 12 | 13 | 14 | def create_app_with_config(*args): 15 | # default to dev config because no one should use this in 16 | # production anyway 17 | env = os.environ.get('APPNAME_ENV', 'dev') 18 | return create_app('appname.settings.%sConfig' % env.capitalize()) 19 | 20 | 21 | @click.group(cls=FlaskGroup, create_app=create_app_with_config) 22 | def cli(): 23 | pass 24 | 25 | 26 | @cli.command() 27 | def create_all(): 28 | """ Creates a database with all of the tables defined in 29 | your SQLAlchemy models 30 | """ 31 | 32 | db.create_all() 33 | 34 | 35 | @cli.command() 36 | def show_urls(): 37 | """ List all of the endpoints on the app and the supportted methods 38 | """ 39 | output = [] 40 | for rule in current_app.url_map.iter_rules(): 41 | options = {} 42 | for arg in rule.arguments: 43 | options[arg] = "<{0}>".format(arg) 44 | 45 | methods = ','.join(rule.methods) 46 | url = url_for(rule.endpoint, **options) 47 | line = urllib.parse.unquote("{:45s} {:30s} {}".format(rule.endpoint, methods, url)) 48 | output.append(line) 49 | 50 | for line in sorted(output): 51 | print(line) 52 | 53 | 54 | if __name__ == "__main__": 55 | cli() 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.0.1 2 | 3 | # Flask Extensions 4 | Flask-Assets==2.0 5 | Flask-Caching==1.10.1 6 | Flask-DebugToolbar==0.11.0 7 | Flask-Login==0.5.0 8 | Flask-SQLAlchemy==2.5.1 9 | Flask-WTF==0.15.1 10 | 11 | # Other 12 | itsdangerous==2.0.1 13 | cssmin==0.2.0 14 | jsmin==2.2.2 15 | python-dotenv==0.19.0 16 | 17 | # Testing 18 | pytest==6.2.4 19 | pytest-cov==2.12.1 20 | mccabe==0.6.1 21 | flake8==3.9.2 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackStouffer/Flask-Foundation/8504bb5c504e958cef794788554c9f7b1f4942da/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from appname import create_app 4 | from appname.models import db, User 5 | 6 | 7 | @pytest.fixture() 8 | def testapp(request): 9 | app = create_app('appname.settings.TestConfig') 10 | client = app.test_client() 11 | 12 | db.app = app 13 | db.create_all() 14 | 15 | if getattr(request.module, "create_user", True): 16 | admin = User('admin', 'supersafepassword') 17 | db.session.add(admin) 18 | db.session.commit() 19 | 20 | def teardown(): 21 | db.session.remove() 22 | db.drop_all() 23 | 24 | request.addfinalizer(teardown) 25 | 26 | return client 27 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | #! ../env/bin/python 2 | # -*- coding: utf-8 -*- 3 | from appname import create_app 4 | 5 | 6 | class TestConfig: 7 | def test_dev_config(self): 8 | """ Tests if the development config loads correctly """ 9 | 10 | app = create_app('appname.settings.DevConfig') 11 | 12 | assert app.config['DEBUG'] is True 13 | assert app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///../database.db' 14 | assert app.config['CACHE_TYPE'] == 'null' 15 | 16 | def test_test_config(self): 17 | """ Tests if the test config loads correctly """ 18 | 19 | app = create_app('appname.settings.TestConfig') 20 | 21 | assert app.config['DEBUG'] is True 22 | assert app.config['SQLALCHEMY_ECHO'] is True 23 | assert app.config['CACHE_TYPE'] == 'null' 24 | 25 | def test_prod_config(self): 26 | """ Tests if the production config loads correctly """ 27 | 28 | app = create_app('appname.settings.ProdConfig') 29 | 30 | assert app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///../database.db' 31 | assert app.config['CACHE_TYPE'] == 'simple' 32 | -------------------------------------------------------------------------------- /tests/test_login.py: -------------------------------------------------------------------------------- 1 | #! ../env/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pytest 5 | 6 | create_user = True 7 | 8 | 9 | @pytest.mark.usefixtures("testapp") 10 | class TestLogin: 11 | def test_login(self, testapp): 12 | """ Tests if the login form functions """ 13 | 14 | rv = testapp.post('/login', data=dict( 15 | username='admin', 16 | password="supersafepassword" 17 | ), follow_redirects=True) 18 | 19 | assert rv.status_code == 200 20 | assert 'Logged in successfully.' in str(rv.data) 21 | 22 | def test_login_fail(self, testapp): 23 | """ Tests if the login form fails correctly """ 24 | 25 | rv = testapp.post('/login', data=dict( 26 | username='admin', 27 | password="" 28 | ), follow_redirects=True) 29 | 30 | assert rv.status_code == 200 31 | assert 'Invalid username or password' in str(rv.data) 32 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | #! ../env/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pytest 5 | 6 | from appname.models import db, User 7 | 8 | create_user = False 9 | 10 | 11 | @pytest.mark.usefixtures("testapp") 12 | class TestModels: 13 | def test_user_save(self, testapp): 14 | """ Test Saving the user model to the database """ 15 | 16 | admin = User('admin', 'supersafepassword') 17 | db.session.add(admin) 18 | db.session.commit() 19 | 20 | user = User.query.filter_by(username="admin").first() 21 | assert user is not None 22 | 23 | def test_user_password(self, testapp): 24 | """ Test password hashing and checking """ 25 | 26 | admin = User('admin', 'supersafepassword') 27 | 28 | assert admin.username == 'admin' 29 | assert admin.check_password('supersafepassword') 30 | -------------------------------------------------------------------------------- /tests/test_urls.py: -------------------------------------------------------------------------------- 1 | #! ../env/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pytest 5 | 6 | create_user = True 7 | 8 | 9 | @pytest.mark.usefixtures("testapp") 10 | class TestURLs: 11 | def test_home(self, testapp): 12 | """ Tests if the home page loads """ 13 | 14 | rv = testapp.get('/') 15 | assert rv.status_code == 200 16 | 17 | def test_login(self, testapp): 18 | """ Tests if the login page loads """ 19 | 20 | rv = testapp.get('/login') 21 | assert rv.status_code == 200 22 | 23 | def test_logout(self, testapp): 24 | """ Tests if the logout page loads """ 25 | 26 | rv = testapp.get('/logout') 27 | assert rv.status_code == 302 28 | 29 | def test_restricted_logged_out(self, testapp): 30 | """ Tests if the restricted page returns a 302 31 | if the user is logged out 32 | """ 33 | 34 | rv = testapp.get('/restricted') 35 | assert rv.status_code == 302 36 | 37 | def test_restricted_logged_in(self, testapp): 38 | """ Tests if the restricted page returns a 200 39 | if the user is logged in 40 | """ 41 | 42 | testapp.post('/login', data=dict( 43 | username='admin', 44 | password="supersafepassword" 45 | ), follow_redirects=True) 46 | 47 | rv = testapp.get('/restricted') 48 | assert rv.status_code == 200 49 | --------------------------------------------------------------------------------