├── Procfile ├── static ├── favicon.ico ├── spent-background.jpg ├── spent-map-screenshot.png ├── spent-login-screenshot.png ├── spent-modal-screenshot.png ├── spent-widget-screenshot.png ├── spent-dashboard-screenshot.png ├── bootstrap-3.3.6 │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── js │ │ ├── npm.js │ │ └── bootstrap.min.js │ └── css │ │ ├── bootstrap-theme.min.css.map │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap-theme.css ├── js │ ├── intercom-shutdown.js │ ├── charts.js │ ├── delete-expenditure.js │ ├── submit-new-account-info.js │ ├── submit-budget.js │ ├── map.js │ └── submit-expenditure.js └── style.css ├── seed_data ├── categories.csv ├── users.csv ├── budget.csv └── expenditures.csv ├── .gitignore ├── requirements.txt ├── templates ├── base.html ├── homepage.html └── dashboard.html ├── README.md ├── tools.py ├── model.py ├── seed.py ├── tests.py └── server.py /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn server:app -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/spent-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/spent-background.jpg -------------------------------------------------------------------------------- /seed_data/categories.csv: -------------------------------------------------------------------------------- 1 | 1|Online Purchase 2 | 2|Travel 3 | 3|Food 4 | 4|Groceries 5 | 5|Clothing 6 | 6|Entertainment -------------------------------------------------------------------------------- /static/spent-map-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/spent-map-screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | *.pyc 3 | .DS_Store 4 | spent.sublime-* 5 | htmlcov/* 6 | .coverage 7 | env 8 | instance/ 9 | secrets.sh -------------------------------------------------------------------------------- /static/spent-login-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/spent-login-screenshot.png -------------------------------------------------------------------------------- /static/spent-modal-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/spent-modal-screenshot.png -------------------------------------------------------------------------------- /static/spent-widget-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/spent-widget-screenshot.png -------------------------------------------------------------------------------- /static/spent-dashboard-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/spent-dashboard-screenshot.png -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /seed_data/users.csv: -------------------------------------------------------------------------------- 1 | 1|Emily|emily@emily.com|emily 2 | 2|Hello Kitty|hello@kitty.com|password 3 | 3|Sailor Moon|sailor@moon.com|password 4 | 4|Dog|dog@dog.com|dog 5 | 5|Mu|mu@mu.com|mu -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emilydowgialo/Spent/HEAD/static/bootstrap-3.3.6/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /seed_data/budget.csv: -------------------------------------------------------------------------------- 1 | 1|1000|1|1|2016-04-07|2016-05-07 2 | 2|500|2|4|2016-04-07|2016-05-07 3 | 3|280|3|5|2016-04-07|2016-05-07 4 | 4|700|4|3|2016-04-07|2016-05-07 5 | 5|900|5|2|2016-04-07|2016-05-07 -------------------------------------------------------------------------------- /static/js/intercom-shutdown.js: -------------------------------------------------------------------------------- 1 | 2 | function shutdownIntercom (evt) { 3 | 4 | evt.preventDefault(); 5 | 6 | Intercom('shutdown'); 7 | window.location = '/logout'; 8 | 9 | } 10 | 11 | // Button click event 12 | $('#sign-out').click(shutdownIntercom); -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.4 2 | Flask==0.10.1 3 | Flask-DebugToolbar==0.10.0 4 | Flask-SQLAlchemy==2.1 5 | itsdangerous==0.24 6 | Jinja2==2.8 7 | MarkupSafe==0.23 8 | pprintpp==0.2.3 9 | psycopg2==2.6.1 10 | requests==2.10.0 11 | SQLAlchemy==1.0.12 12 | Werkzeug==0.11.9 13 | gunicorn -------------------------------------------------------------------------------- /seed_data/expenditures.csv: -------------------------------------------------------------------------------- 1 | 1|1|14.00|2016-04-07|1|Whole Foods|food for the week|| 2 | 2|2|20.00|2016-04-08|5|Amazon|new leash|| 3 | 3|4|60.50|2016-04-09|3|Forever21|cool new shirt|| 4 | 4|3|550.00|2016-04-20|4|United|plane tix to Hawaii|| 5 | 5|5|20.00|2016-04-23|2|Century Theater|tickets to the Avengers|9374889676090040179500|usps -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /static/js/charts.js: -------------------------------------------------------------------------------- 1 | 2 | // Charts are from chart.js 3 | 4 | function charts() { 5 | 6 | var options = { 7 | responsive: true 8 | }; 9 | 10 | var ctx_donut = $("#donutChart").get(0).getContext("2d"); 11 | 12 | // Gets info from this route in server.py and sends to the donut chart 13 | $.get("/expenditure-types.json", function (data) { 14 | var myDonutChart = new Chart(ctx_donut).Doughnut(data.expenditures, options); 15 | $('#donutLegend').html(myDonutChart.generateLegend()); 16 | }); 17 | 18 | var ctx_line = $("#barChart").get(0).getContext("2d"); 19 | 20 | // Gets info from this route in server.py and sends to the bar chart 21 | $.get("/total-spent.json", function (data) { 22 | var myBarChart = new Chart(ctx_line).Bar(data, options); 23 | $("#BarLegend").html(myBarChart.generateLegend()); 24 | }); 25 | 26 | } 27 | 28 | // Call the function so the charts display upon page load 29 | charts(); -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spent 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% block headerstuff %} 19 | 20 | {% endblock %} 21 | 22 | 23 | 24 | 25 | {% block content %} 26 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /static/js/delete-expenditure.js: -------------------------------------------------------------------------------- 1 | function deleteExpenOnPage(result) { 2 | // This function will delete the expenditure row on the dashboard 3 | 4 | // Get the expenditure id 5 | var id = result; 6 | console.log("this is id"); 7 | console.log(id); 8 | 9 | expenId = id.expenditure_id; 10 | 11 | console.log("this is expenId"); 12 | console.log(expenId); 13 | 14 | // Remove that expenditure row 15 | $('#expenditure-row-' + String(expenId)).remove(); 16 | 17 | console.log("delete expen on page function ran"); 18 | } 19 | 20 | function deleteExpenditure(evt) { 21 | evt.preventDefault(); 22 | 23 | // Get the expenditure ID 24 | var id = $(evt.target).data("expenditureid"); 25 | 26 | console.log("this is evt target"); 27 | console.log(evt.target); 28 | 29 | console.log("this is id"); 30 | console.log(id); 31 | 32 | // Post the id information to the remove expenditure route and call 33 | // the function that will remove that row from the front end 34 | $.post("/remove-expenditure/" + String(id), deleteExpenOnPage); 35 | console.log("ran deleteExpenditure"); 36 | } 37 | 38 | // When this button is pressed, call the deleteExpenditure function 39 | $(".delete-expenditure").click(deleteExpenditure); -------------------------------------------------------------------------------- /static/js/submit-new-account-info.js: -------------------------------------------------------------------------------- 1 | /* global $*/ 2 | "use strict"; 3 | 4 | function replaceInfo(results) { 5 | 6 | // Close the modal via Javascript when the event is triggered 7 | $('#add-account-modal').modal('toggle'); 8 | 9 | // The results contain the new name and email, if provided 10 | var newInfo = results; 11 | 12 | console.log(newInfo); 13 | 14 | var newName = String(newInfo.name); 15 | var newEmail = String(newInfo.email); 16 | 17 | // This is the element to edit 18 | var nameElement = $('#user-name'); 19 | var emailElement = $('#user-name'); 20 | 21 | // Changes the info 22 | nameElement.html(newName); 23 | emailElement.html(newEmail); 24 | console.log("finished replaceInfo"); 25 | } 26 | 27 | function updateAccountInfo(evt) { 28 | evt.preventDefault(); 29 | 30 | // Close the modal via Javascript when the event is triggered 31 | $('#editProfile').modal('toggle'); 32 | 33 | // Gather info from the form 34 | var info = $('#form-profile-edit').serialize(); 35 | 36 | // Parse form information, which returns information jsonified 37 | $.post('/profile-edit', info, replaceInfo); 38 | console.log("Finished sending AJAX"); 39 | } 40 | 41 | // Submit button click event 42 | $('#submit-new-account-info').click(updateAccountInfo); -------------------------------------------------------------------------------- /static/js/submit-budget.js: -------------------------------------------------------------------------------- 1 | /* global $*/ 2 | "use strict"; 3 | 4 | function replaceBudget(results) { 5 | 6 | // Callback 7 | updateBudgetMinusExpenses(results); 8 | updateProgressBars(results); 9 | 10 | // The results budget 11 | var budget = results; 12 | 13 | // This is the category at hand 14 | var stringToAppend = String(budget.budget); 15 | 16 | // This is the element to edit 17 | var budgetElement = $('#budget-' + String(budget.category_id)); 18 | 19 | // Appends the info to the element 20 | budgetElement.html(stringToAppend); 21 | console.log("finished replaceBudget"); 22 | } 23 | 24 | function updateProgressBars(results) { 25 | 26 | // This contains info about the new budget 27 | var progBarInfo = results; 28 | 29 | console.log("this is progbarinfo"); 30 | console.log(progBarInfo); 31 | 32 | // These are the elements on dashboard.html we want to change 33 | var divInfo = $('#progbar-' + String(progBarInfo.category_id)); 34 | var progNum = $('#prognum-' + String(progBarInfo.category_id)); 35 | 36 | console.log("this is prognum and progbar"); 37 | console.log(progNum); 38 | console.log(divInfo); 39 | 40 | // Target the elements on dashboard.html 41 | divInfo.css( "width", String(progBarInfo.category_progress) + "%" ); 42 | progNum.html("$" + String(progBarInfo.cat_budget_minus_expenses)); 43 | } 44 | 45 | function updateBudgetMinusExpenses(resp) { 46 | 47 | // The results budget 48 | var budget = resp; 49 | 50 | // This is the category at hand 51 | var stringToAppend = String(budget.cat_budget_minus_expenses); 52 | 53 | // This is the element to edit 54 | var budgetElement = $('#budget-left-' + String(budget.category_id)); 55 | 56 | // Appends the info to the element 57 | budgetElement.html(stringToAppend); 58 | console.log("finished updateBudgetMinusExpenses"); 59 | 60 | } 61 | 62 | function updateBudget(evt) { 63 | evt.preventDefault(); 64 | 65 | window.Intercom('update'); 66 | 67 | // Close the modal via Javascript when the event is triggered 68 | $('#addBudget').modal('toggle'); 69 | 70 | // Gather info from the budget form 71 | var budget = $('#budget-form').serialize(); 72 | 73 | // Parse form information, which returns information jsonified 74 | $.post('/add-budget', budget, replaceBudget); 75 | console.log("Finished sending AJAX"); 76 | } 77 | 78 | // Submit button click event 79 | $('#budget-submit').click(updateBudget); 80 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | /*MAP*/ 2 | #map { 3 | height: 300px; 4 | } 5 | 6 | /*LOGIN DIV*/ 7 | div.login-div { 8 | margin: 0; 9 | /*background: yellow;*/ 10 | position: absolute; 11 | top: 50%; 12 | left: 50%; 13 | margin-right: -50%; 14 | transform: translate(-50%, -50%) 15 | } 16 | 17 | body { 18 | background-color: #e6e9f0; 19 | } 20 | 21 | /*HOMEPAGE BACKGROUND IMAGE*/ 22 | .homepage-container-div { 23 | background-image: url("/static/spent-background.jpg"); 24 | background-size: 100%; 25 | } 26 | 27 | /*PANEL HEADINGS*/ 28 | .panel > .panel-heading { 29 | background-image: none; 30 | background-color: ##3b3a36; 31 | color: #5a5c51; 32 | 33 | } 34 | 35 | /*REMOVES ROUNDED EDGES*/ 36 | /** { 37 | border-radius: 0 !important; 38 | }*/ 39 | 40 | /*REMOVES PANEL DROP SHADOW*/ 41 | .panel { 42 | border: 0; 43 | } 44 | 45 | /*REMOVES PANEL DROP SHADOW*/ 46 | .table > thead > tr > th, .table > thead > tr > td { 47 | border: 0; 48 | } 49 | 50 | /*ADD DROP SHADOW*/ 51 | .panel-default { 52 | box-shadow: 2px 2px; 53 | color: #5a5c51; 54 | } 55 | 56 | /*LINK COLORS*/ 57 | a { 58 | color: #9fa8a3 !important; 59 | } 60 | /*END OF LINK COLORS*/ 61 | 62 | /*FORM FOCUS*/ 63 | .form-control:focus { 64 | border-color: #c0dfd9; 65 | box-shadow: inset 0 1px 1px #c0dfd9, 0 0 8px #c0dfd9; 66 | } 67 | 68 | /*BUTTONS*/ 69 | .btn { 70 | padding: 14px 24px; 71 | border: 0 none; 72 | font-weight: 700; 73 | letter-spacing: 1px; 74 | text-transform: uppercase; 75 | } 76 | 77 | .btn:focus, .btn:active:focus, .btn.active:focus { 78 | outline: 0 none; 79 | } 80 | 81 | .btn-primary { 82 | background: #9fa8a3; 83 | color: #ffffff; 84 | } 85 | 86 | .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { 87 | background: #F37257; 88 | } 89 | 90 | .btn-primary:active, .btn-primary.active { 91 | background: #AFC1CC; 92 | box-shadow: none; 93 | } 94 | /*END BUTTONS*/ 95 | 96 | /* enable absolute positioning */ 97 | .inner-addon { 98 | position: relative; 99 | } 100 | 101 | /*PROGRESS BARS*/ 102 | .progress { 103 | background: rgba(245, 245, 245, 1); 104 | border: 0px solid rgba(245, 245, 245, 1); 105 | border-radius: 4px; 106 | height: 20px; 107 | background-color: #AFC1CC; 108 | } 109 | 110 | .progress-bar-custom { 111 | background: #F37257; 112 | } 113 | 114 | table .progress { 115 | margin-bottom: 0; 116 | } 117 | 118 | /* style icon */ 119 | .inner-addon .glyphicon { 120 | position: absolute; 121 | padding: 10px; 122 | pointer-events: none; 123 | } 124 | 125 | /* align icon */ 126 | .left-addon .glyphicon { left: 0px;} 127 | .right-addon .glyphicon { right: 0px;} 128 | 129 | /* add padding */ 130 | .left-addon input { padding-left: 30px; } 131 | .right-addon input { padding-right: 30px; } 132 | 133 | /*Glyphicon style*/ 134 | .btn-custom { 135 | background-color: rgba(0, 0, 0, 0.0); 136 | position: absolute; 137 | padding: 10px; 138 | border-color: #CCCCCC; 139 | color: #333333; 140 | } 141 | 142 | .btn-custom:hover { 143 | background-color: gba(0, 0, 0, 0.0); 144 | position: absolute; 145 | padding: 10px; 146 | border-color: #fff; 147 | color: #333333; 148 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spent 2 | 3 | ## Summary 4 | 5 | **Spent** is a practical and versatile budget tracking and money management app, offering users an easy-to-read visualization of their finances and a detailed log of their spending habits. With Spent, users can create budgets, set spending goals, and store information about recent purchases. 6 | 7 | 8 | ## About the Developer 9 | 10 | Spent was created by Emily Dowgialo. Learn more about the developer on [LinkedIn](https://www.linkedin.com/in/emilydowgialo). 11 | 12 | 13 | ## Technologies 14 | 15 | **Tech Stack:** 16 | 17 | - Python 18 | - Flask 19 | - SQLAlchemy 20 | - Jinja2 21 | - HTML 22 | - CSS 23 | - Javascript 24 | - JQuery 25 | - AJAX 26 | - JSON 27 | - Bootstrap 28 | - Python unittest module 29 | - Google Maps API 30 | - Shippo API 31 | 32 | Spent is an app built on a Flask server with a PostgreSQL database, with SQLAlchemy as the ORM. The front end templating uses Jinja2, the HTML was built using Bootstrap, and the Javascript uses JQuery and AJAX to interact with the backend. The graphs are rendered using Chart.js. The map is built using the Google Maps API, which works in tandem with the Shippo package tracking API. Server routes are tested using the Python unittest module. 33 | 34 | 35 | ## Features 36 | 37 | ![alt text](https://github.com/emilydowgialo/Spent/blob/master/static/spent-login-screenshot.png "Spent Login") 38 | 39 | 40 | The interactive dashboard features graphs, which segment the user’s expenditures into categories, and auto-updating widgets, which illustrate spending metrics and statistics. The user enters budgets for categories along with a date range that is used to display spending stats over that particular time period. 41 | 42 | The bar graph and the donut chart show stats based on the user's spending habits within the last 30 days. The bar graph shows the average amount spent per category, while the donut chart displays the total amount spent. 43 | 44 | 45 | ![alt text](https://github.com/emilydowgialo/Spent/blob/master/static/spent-dashboard-screenshot.png "Spent dashboard") 46 | 47 | The Budget Remaining widget's progress bars graphically represent the remaining money in the user's budget. Two more widgets display the average and total amounts spent per category within the specified date range. Everything on the dashboard updates in real time as the user adds a new budget or a new expenditure. 48 | 49 | 50 | ![alt text](https://github.com/emilydowgialo/Spent/blob/master/static/spent-widget-screenshot.png "Spent widgets") 51 | 52 | 53 | Spent's dynamic package tracking feature monitors online purchases, which displays the last place a package was scanned, its delivery status, and plots its current location on a map. When the user saves a new purchase, they are given the option to input tracking information. If the user has tracking information saved, a paper airplane icon is displayed next to the corresponding purchase, and the user simply has to click on the icon to track their package. 54 | 55 | 56 | ![alt text](https://github.com/emilydowgialo/Spent/blob/master/static/spent-map-screenshot.png "Spent package track") 57 | 58 | 59 | Spent is a one-page dashboard. There is beauty and functionality in simplicity, and the user's flow is kept direct and clean. The user inputs budget and expenditure information in modal window forms that do not take them away from the main dashboard, keeping the user experience focused. 60 | 61 | 62 | ![alt text](https://github.com/emilydowgialo/Spent/blob/master/static/spent-modal-screenshot.png "Spent modals") 63 | 64 | 65 | ## For Version 2.0 66 | 67 | - **More chart control:** Ability to customize the categories and timeframes the charts display 68 | - **Badges:** Badges for certain milestones, such as staying under budget for a given period of time 69 | - **Password hashing:** Passwords will be hashed before being saved to the database 70 | -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | from model import Expenditure, Budget 2 | 3 | from datetime import datetime 4 | 5 | 6 | ########## THIS FILE CONTAINS WIDELY USED FUNCTIONS ########### 7 | 8 | 9 | def expenditure_function(category_id, id, start, end): 10 | """ Calculate the total amount and avg spent in one particular category """ 11 | 12 | # List of expenditure objects 13 | expenditures = Expenditure.query.filter_by( 14 | category_id=category_id, expenditure_userid=id).filter( 15 | Expenditure.date_of_expenditure.between(start, end)).all() 16 | 17 | # Initialize the total price at 0 18 | total_price = 0 19 | 20 | # Increase the total price by the price of each expenditure 21 | for expenditure in expenditures: 22 | expenditure_price = expenditure.price 23 | total_price += expenditure_price 24 | 25 | # This gets the average price; if there is an error due to no 26 | # expenditures, it returns the value of "0" 27 | try: 28 | avg_expenditures = total_price/len(expenditures) 29 | except ZeroDivisionError: 30 | avg_expenditures = "0" 31 | 32 | return float(total_price), float(avg_expenditures) 33 | 34 | 35 | def get_dates_for_budget(category_id, id): 36 | """ Get the start and end date for a budget """ 37 | 38 | # Get budget object 39 | budget = Budget.query.filter_by(category_id=category_id, budget_userid=id).all() 40 | 41 | if len(budget) > 0: 42 | start_date = budget[0].budget_start_date 43 | end_date = budget[0].budget_end_date 44 | else: 45 | start_date = datetime.now() 46 | end_date = start_date 47 | 48 | # Return start and end dates 49 | return start_date, end_date 50 | 51 | 52 | def budget_totals(category_id, id, total_price): 53 | """ Calculate budget minus expenditures made """ 54 | 55 | # This is the expenditure object 56 | expenditure_budget = Budget.query.filter_by(category_id=category_id, budget_userid=id).first() 57 | 58 | # Initializes the budget at 0 59 | expenditure_budget_minus_expenses = 0 60 | 61 | # If there is a budget, this subtracts the total expenses from it, or 62 | # returns a statement about the user not inputting a budget yet 63 | if expenditure_budget is not None: 64 | budget_total = expenditure_budget.budget 65 | expenditure_budget_minus_expenses = float(budget_total) - float(total_price) 66 | 67 | else: 68 | expenditure_budget_minus_expenses = 0 69 | 70 | return expenditure_budget_minus_expenses 71 | 72 | 73 | def get_budget_per_category(category_id, id): 74 | """ Gets budget for particular category """ 75 | 76 | # Query the database for budgets associated with the user at hand 77 | budget = Budget.query.filter_by(budget_userid=id, category_id=category_id).all() 78 | 79 | # If a budget exists, return it, otherwise return 0 80 | if len(budget) > 0: 81 | budget = budget[0].budget 82 | else: 83 | budget = 0 84 | 85 | return budget 86 | 87 | 88 | def get_total_for_category(cat, lst): 89 | """ Gets the total amount per category """ 90 | 91 | # Set total to 0 92 | total = 0 93 | queries = lst 94 | 95 | # Extract expenditures by id and add the price to the total 96 | for query in queries: 97 | if query.category_id == cat: 98 | total += query.price 99 | 100 | return total 101 | 102 | 103 | def get_progress(cat_minus_expenses, budget): 104 | """ Get the progress bar percentage """ 105 | 106 | # Get perentage for progress bar and account for possible divide by 0 error 107 | try: 108 | progress = (float(cat_minus_expenses)/float(budget)) 109 | except ZeroDivisionError: 110 | progress = 0 111 | 112 | # Multiply above total by 100 for a percentage between 1 and 100 113 | cat_progress = str(progress * 100) 114 | 115 | return cat_progress 116 | -------------------------------------------------------------------------------- /static/js/map.js: -------------------------------------------------------------------------------- 1 | var map; 2 | function initMap(thing) { 3 | 4 | var address = thing; 5 | console.log("this is initMap address"); 6 | console.log(address); 7 | 8 | // Custom map styling 9 | var customMapType = new google.maps.StyledMapType([ 10 | { 11 | stylers: [ 12 | {hue: '#F68D5C'}, 13 | {visibility: 'simplified'}, 14 | {gamma: 0.5}, 15 | {weight: 0.5} 16 | ] 17 | }, 18 | { 19 | elementType: 'labels', 20 | stylers: [{visibility: 'on'}] 21 | }, 22 | { 23 | featureType: 'water', 24 | stylers: [{color: '#AFC1CC'}] 25 | } 26 | ], { 27 | name: 'Custom Style' 28 | }); 29 | 30 | var customMapTypeId = 'custom_style'; 31 | 32 | var map = new google.maps.Map(document.getElementById('map'), { 33 | center: {lat: 37.7749, lng: -122.4194}, 34 | zoom: 12, 35 | mapTypeControlOptions: { 36 | mapTypeIds: [google.maps.MapTypeId.ROADMAP, customMapTypeId] 37 | } 38 | }); 39 | 40 | map.mapTypes.set(customMapTypeId, customMapType); 41 | map.setMapTypeId(customMapTypeId); 42 | 43 | var geocoder = new google.maps.Geocoder(); 44 | 45 | geocodeAddress(geocoder, map, address); 46 | } 47 | 48 | // test 9374889676090040179500 49 | // This function gets information about the address to display 50 | 51 | function geocodeAddress(geocoder, resultsMap, addressToUse) { 52 | 53 | // if addressToUse exists statement would fix the error 54 | var addressYay = String(addressToUse.city) + String(addressToUse.state); 55 | 56 | geocoder.geocode({'address': addressYay}, function(results, status) { 57 | if (status === google.maps.GeocoderStatus.OK) { 58 | resultsMap.setCenter(results[0].geometry.location); 59 | var marker = new google.maps.Marker({ 60 | map: resultsMap, 61 | position: results[0].geometry.location 62 | }); 63 | } else { 64 | alert('Geocode was not successful for the following reason: ' + status); 65 | } 66 | }); 67 | } 68 | 69 | function appendTrackingInfo(info) { 70 | 71 | // This is the tracking info object 72 | var trackingInfo = info; 73 | console.log("this is append tracking info trackingInfo"); 74 | console.log(trackingInfo); 75 | 76 | // Define variables for info we need 77 | var trackingStatus = String(trackingInfo.tracking_status); 78 | var trackingCity = trackingInfo.city; 79 | var trackingState = trackingInfo.state; 80 | var trackingZipcode = trackingInfo.zipcode; 81 | var trackingCountry = trackingInfo.country; 82 | 83 | console.log("this is tracking status"); 84 | console.log(trackingStatus); 85 | 86 | // Display the information on the front end 87 | console.log(trackingStatus); 88 | 89 | $('#tracking-status').html(trackingStatus); 90 | $('#tracking-city').html(trackingCity); 91 | $('#tracking-state').html(trackingState); 92 | $('#tracking-zipcode').html(trackingZipcode); 93 | $('#tracking-country').html(trackingCountry); 94 | console.log("finished with appendTrackingInfo"); 95 | 96 | } 97 | 98 | function trackingInfo(results) { 99 | 100 | var address = results; 101 | console.log("this is address"); 102 | console.log(address); 103 | 104 | // Call the map function and the tracking info function 105 | initMap(address); 106 | appendTrackingInfo(address); 107 | 108 | } 109 | 110 | function updateAddress(evt) { 111 | evt.preventDefault(); 112 | 113 | // When you have an event handler, the value of this is assigned by 114 | // JQuery to be whatever triggered the event 115 | console.log($(this)); 116 | console.log($(this).data('trackingnum')); 117 | 118 | // this is the button 119 | // data is the data embedded in the button assigned in the HTML 120 | var trackingNum = $(this).data('trackingnum'); 121 | 122 | $.post('/tracking/' + trackingNum, trackingInfo); 123 | console.log("Finished sending AJAX"); 124 | } 125 | 126 | $('.submit-tracking').click(updateAddress); -------------------------------------------------------------------------------- /static/js/submit-expenditure.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function showExpenditureResults(result) { 4 | 5 | // Callbacks 6 | updateTotalSpent(result); 7 | updateAverageSpent(result); 8 | appendExpenditure(result); 9 | updateProgressBars(result); 10 | charts(); 11 | } 12 | 13 | function submitExpenditure(evt) { 14 | evt.preventDefault(); 15 | 16 | // Close the modal via Javascript when the event is triggered 17 | $('#addExpenditureModal').modal('toggle'); 18 | 19 | // Gather data from the form 20 | var formInputs = { 21 | "category": $("#category-field").val(), 22 | "price": $("#price").val(), 23 | "date": $("#date").val(), 24 | "wherebought": $("#wherebought").val(), 25 | "description": $("#description").val(), 26 | "tracking-num": $("#tracking-num").val(), 27 | "tracking-num-carrier": $("#tracking-num-carrier").val() 28 | }; 29 | 30 | // Post info to this route to add to the database 31 | $.post("/add-expenditure-to-db", 32 | formInputs, 33 | showExpenditureResults 34 | ); 35 | } 36 | 37 | function appendExpenditure(result) { 38 | // This function appends a new expenditure in the Expenditures widget 39 | 40 | var expenditure = result; 41 | 42 | // Begin HTML to append 43 | var stringToAppend = '' + 44 | '' + 45 | String(expenditure.category) + 46 | '' + 47 | '' + 48 | String(expenditure.price) + 49 | '' + 50 | '' + 51 | String(expenditure.date_of_expenditure) + 52 | '' + 53 | '' + 54 | String(expenditure.where_bought) + 55 | '' + 56 | '' + 57 | String(expenditure.description) + 58 | '' + 59 | ''; 60 | 61 | // If a tracking number exists, append this 62 | if (expenditure.tracking_num) { 63 | 64 | stringToAppend += '
' 70 | } 71 | 72 | // Continue the HTML to append 73 | stringToAppend += '' + 74 | '
' + 77 | '' + 80 | '
' + 81 | ''; 82 | 83 | // Append the HTML at this ID 84 | console.log(stringToAppend); 85 | $('#table-expenditure-table').append(stringToAppend); 86 | console.log("finished with appendExpenditure"); 87 | 88 | } 89 | 90 | function updateTotalSpent(result) { 91 | // This function updates the total amount spent on the front end 92 | 93 | // Contains information about the expenditure 94 | var expenditure = result; 95 | console.log(expenditure.total_cat_price); 96 | var stringToAppend = String(expenditure.total_cat_price); 97 | 98 | // This is the place to append the info 99 | var expenditureElement = $('#total-spent-' + String(expenditure.category_id)); 100 | 101 | expenditureElement.html(stringToAppend); 102 | console.log("finished updateTotalSpent"); 103 | 104 | } 105 | 106 | function updateAverageSpent(result) { 107 | // This function updates the average amount spent on the front end 108 | 109 | // Contains info about the average 110 | var average = result; 111 | console.log(average.avg_cat_expenditures); 112 | var stringToAppend = String(average.avg_cat_expenditures); 113 | 114 | // This is the place to append the info 115 | var avgElement = $('#avg-' + String(average.category_id)); 116 | 117 | avgElement.html(stringToAppend); 118 | console.log("finished updateAverageSpent"); 119 | 120 | } 121 | 122 | // Event listener 123 | $("#expenditure-form").on("submit", submitExpenditure); -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | import os 5 | 6 | db = SQLAlchemy() 7 | 8 | 9 | class User(db.Model): 10 | """ This is the user of the web app """ 11 | 12 | __tablename__ = "users" 13 | 14 | id = db.Column(db.Integer, autoincrement=True, primary_key=True) 15 | name = db.Column(db.String(64)) 16 | email = db.Column(db.String(64)) 17 | password = db.Column(db.String(64)) 18 | 19 | 20 | class Category(db.Model): 21 | """ This is the category table """ 22 | 23 | __tablename__ = "categories" 24 | 25 | id = db.Column(db.Integer, autoincrement=True, primary_key=True) 26 | category = db.Column(db.String(64)) 27 | 28 | 29 | class Budget(db.Model): 30 | """ This is the user's budget """ 31 | 32 | __tablename__ = "budget" 33 | 34 | id = db.Column(db.Integer, autoincrement=True, primary_key=True) 35 | # the data type of the budget should match the data type of the price 36 | budget = db.Column(db.Numeric(15, 2)) 37 | category_id = db.Column(db.Integer, db.ForeignKey('categories.id')) 38 | budget_userid = db.Column(db.Integer, db.ForeignKey('users.id')) 39 | budget_start_date = db.Column(db.DateTime) 40 | budget_end_date = db.Column(db.DateTime) 41 | 42 | user = db.relationship("User", backref=db.backref('budget')) 43 | 44 | category = db.relationship("Category", backref=db.backref('budget')) 45 | 46 | def __repr__(self): 47 | """ Provide useful info """ 48 | 49 | return "" % ( 50 | self.id, self.budget, self.budget_userid, self.category, self.budget_start_date, self.budget_end_date) 51 | 52 | 53 | class Expenditure(db.Model): 54 | """ This contains expenditures """ 55 | 56 | __tablename__ = "expenditures" 57 | 58 | id = db.Column(db.Integer, autoincrement=True, primary_key=True) 59 | category_id = db.Column(db.Integer, db.ForeignKey('categories.id')) 60 | price = db.Column(db.Numeric(15, 2)) 61 | date_of_expenditure = db.Column(db.DateTime) 62 | expenditure_userid = db.Column(db.Integer, db.ForeignKey('users.id')) 63 | where_bought = db.Column(db.String(100)) 64 | description = db.Column(db.UnicodeText) 65 | tracking_num = db.Column(db.String, nullable=True) 66 | tracking_num_carrier = db.Column(db.String(100), nullable=True) 67 | 68 | user = db.relationship("User", backref=db.backref('expenditures')) 69 | 70 | category = db.relationship("Category", backref=db.backref('expenditures')) 71 | 72 | 73 | def connect_to_db(app, spent_database): 74 | """ Connect the database to our Flask app. """ 75 | 76 | # Configure to use the database 77 | app.config['SQLALCHEMY_DATABASE_URI'] = spent_database 78 | app.config['SQLALCHEMY_ECHO'] = True 79 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True 80 | db.app = app 81 | db.init_app(app) 82 | 83 | 84 | def example_data(): 85 | """Create example data for the test database.""" 86 | 87 | fakeuser = User(name="Mu", email="mu@mu.com", password="mu") 88 | fakebudget = Budget(budget=1000, category_id=3, budget_start_date="2016-05-07", budget_end_date="2016-06-15") 89 | fakecat = Category(category="Food", id=3) 90 | fakecat2 = Category(category="Travel", id=2) 91 | fakeexpenditure = Expenditure(category_id=2, price=500, 92 | date_of_expenditure="2016-05-07", 93 | expenditure_userid=fakeuser.id, 94 | where_bought="train station", 95 | description="Amtrak ticket") 96 | 97 | db.session.add_all([fakeuser, fakebudget, fakeexpenditure, fakecat, fakecat2]) 98 | db.session.commit() 99 | 100 | # Add the budget_userid to the database, otherwise budget_userid is None 101 | # because the budget is not associated with a user 102 | fakebudget.budget_userid = fakeuser.id 103 | db.session.add(fakebudget) 104 | db.session.commit() 105 | 106 | 107 | if __name__ == "__main__": 108 | # As a convenience, if we run this module interactively, it will leave 109 | # you in a state of being able to work with the database directly. 110 | 111 | # So that we can use Flask-SQLAlchemy, we'll make a Flask app 112 | from flask import Flask 113 | 114 | app = Flask(__name__) 115 | 116 | spent_database = os.getenv('POSTGRES_DB_URL', 'postgres:///spending') 117 | 118 | connect_to_db(app, spent_database) 119 | print "Connected to DB." 120 | -------------------------------------------------------------------------------- /templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | 5 | 6 | {% with messages = get_flashed_messages() %} 7 | {% if messages %} 8 | 13 | {% endif %} 14 | {% endwith %} 15 | 16 |
17 | 18 | 51 | 52 | 53 | 89 | 90 |
91 | 92 | 93 | 94 | 100 | 101 | 102 | 103 | 104 | {% endblock %} -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} -------------------------------------------------------------------------------- /seed.py: -------------------------------------------------------------------------------- 1 | """ Utility file to seed spending database in seed_data/ """ 2 | 3 | from sqlalchemy import func 4 | from model import User, Expenditure, Budget, Category 5 | 6 | from model import connect_to_db, db 7 | from server import app 8 | 9 | import os 10 | 11 | 12 | def load_users(): 13 | """ Load users from users.csv into database """ 14 | 15 | print "Users" 16 | 17 | # Delete all rows in table, so if we need to run this a second time, 18 | # we won't be trying to add duplicate users 19 | User.query.delete() 20 | 21 | # Read users.csv file and insert data into the session 22 | for row in open("seed_data/users.csv"): 23 | row = row.rstrip() 24 | user_data = row.split("|") 25 | id = user_data[0] 26 | name = user_data[1] 27 | email = user_data[2] 28 | password = user_data[3] 29 | 30 | user = User(id=id, 31 | name=name, 32 | email=email, 33 | password=password) 34 | 35 | # We need to add to the session or it won't ever be stored 36 | db.session.add(user) 37 | 38 | # Once we're done, we should commit our work 39 | db.session.commit() 40 | 41 | 42 | def load_categories(): 43 | """ Load categories from categories.csv into database """ 44 | 45 | print "Categories" 46 | 47 | # Delete all rows in table, so if we need to run this a second time, 48 | # we won't be trying to add duplicate categories 49 | Category.query.delete() 50 | 51 | # Read categories.csv file and insert data into the session 52 | for row in open("seed_data/categories.csv"): 53 | row = row.rstrip() 54 | categories_data = row.split("|") 55 | id = categories_data[0] 56 | category = categories_data[1] 57 | 58 | category_model = Category(id=id, 59 | category=category) 60 | 61 | # We need to add to the session or it won't ever be stored 62 | db.session.add(category_model) 63 | 64 | db.session.commit() 65 | 66 | 67 | def load_budget(): 68 | """ Load budget from budget.csv into database """ 69 | 70 | print "Budget" 71 | 72 | # Delete all rows in table, so if we need to run this a second time, 73 | # we won't be trying to add duplicate users 74 | Budget.query.delete() 75 | 76 | # Read users.csv file and insert data into the session 77 | for row in open("seed_data/budget.csv"): 78 | row = row.rstrip() 79 | budget_data = row.split("|") 80 | id = budget_data[0] 81 | budget = budget_data[1] 82 | category_id = budget_data[2] 83 | budget_userid = budget_data[3] 84 | budget_start_date = budget_data[4] 85 | budget_end_date = budget_data[5] 86 | 87 | budget = Budget(id=id, 88 | budget=budget, 89 | category_id=category_id, 90 | budget_userid=budget_userid, 91 | budget_start_date=budget_start_date, 92 | budget_end_date=budget_end_date) 93 | 94 | # We need to add to the session or it won't ever be stored 95 | db.session.add(budget) 96 | 97 | # Once we're done, we should commit our work 98 | db.session.commit() 99 | 100 | 101 | def load_expenditures(): 102 | """ Load expenditures from expenditures.csv into database """ 103 | 104 | print "Expenditures" 105 | Expenditure.query.delete() 106 | 107 | for row in open("seed_data/expenditures.csv"): 108 | row = row.rstrip() 109 | expenditure_data = row.split("|") 110 | id = expenditure_data[0] 111 | category_id = expenditure_data[1] 112 | price = expenditure_data[2] 113 | date_of_expenditure = expenditure_data[3] 114 | expenditure_userid = expenditure_data[4] 115 | where_bought = expenditure_data[5] 116 | description = expenditure_data[6] 117 | tracking_num = expenditure_data[7] 118 | tracking_num_carrier = expenditure_data[8] 119 | 120 | expenditure = Expenditure(id=id, 121 | category_id=category_id, 122 | price=price, 123 | date_of_expenditure=date_of_expenditure, 124 | expenditure_userid=expenditure_userid, 125 | where_bought=where_bought, 126 | description=description, 127 | tracking_num=tracking_num, 128 | tracking_num_carrier=tracking_num_carrier) 129 | 130 | db.session.add(expenditure) 131 | 132 | db.session.commit() 133 | 134 | 135 | def set_val_user_id(): 136 | """ Set value for the next user id after seeding database """ 137 | 138 | # Get the Max user id in the database 139 | result = db.session.query(func.max(User.id)).one() 140 | 141 | max_id = int(result[0]) 142 | 143 | # Set the value for the next user_id to be max_id + 1 144 | # Note to self: the 'users_id_seq' variable is based on the Users table 145 | query = "SELECT setval('users_id_seq', :new_id)" 146 | 147 | db.session.execute(query, {'new_id': max_id + 1}) 148 | db.session.commit() 149 | 150 | 151 | def set_val_expenditure_id(): 152 | """ Set value for the next expenditure id after seeding database """ 153 | 154 | # Get the Max expenditure id in the database 155 | result = db.session.query(func.max(Expenditure.id)).one() 156 | 157 | max_id = int(result[0]) 158 | 159 | # Set the value for the next expenditure id to be max_id + 1 160 | query = "SELECT setval('expenditures_id_seq', :new_id)" 161 | 162 | db.session.execute(query, {'new_id': max_id + 1}) 163 | db.session.commit() 164 | 165 | 166 | def set_val_budget_id(): 167 | """ Set value for the next budget id after seeding database """ 168 | 169 | # Get the Max user id in the database 170 | result = db.session.query(func.max(Budget.id)).one() 171 | 172 | max_id = int(result[0]) 173 | 174 | # Set the value for the next user_id to be max_id + 1 175 | query = "SELECT setval('budget_id_seq', :new_id)" 176 | 177 | db.session.execute(query, {'new_id': max_id + 1}) 178 | db.session.commit() 179 | 180 | 181 | def set_val_category_id(): 182 | """ Set value for the next category id after seeding database """ 183 | 184 | # Get the Max user id in the database 185 | result = db.session.query(func.max(Category.id)).one() 186 | 187 | max_id = int(result[0]) 188 | 189 | # Set the value for the next user_id to be max_id + 1 190 | query = "SELECT setval('categories_id_seq', :new_id)" 191 | 192 | db.session.execute(query, {'new_id': max_id + 1}) 193 | db.session.commit() 194 | 195 | 196 | if __name__ == "__main__": 197 | spent_database = os.getenv('POSTGRES_DB_URL', 'postgres:///spending') 198 | connect_to_db(app, spent_database) 199 | 200 | # In case tables haven't been created, create them 201 | db.create_all() 202 | 203 | # Import different types of data 204 | load_users() 205 | load_categories() 206 | load_expenditures() 207 | load_budget() 208 | set_val_user_id() 209 | set_val_category_id() 210 | set_val_expenditure_id() 211 | set_val_budget_id() 212 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from datetime import datetime 4 | 5 | from server import app 6 | from model import db, connect_to_db, User, example_data, Budget, Expenditure 7 | 8 | 9 | class SpentDatabaseTests(unittest.TestCase): 10 | """ Flask tests that use the database """ 11 | 12 | def setUp(self): 13 | """ Stuff to do before every test """ 14 | 15 | app.config['TESTING'] = True 16 | app.config['SECRET_KEY'] = 'key123' 17 | self.client = app.test_client() 18 | 19 | # Connect to test database (uncomment when testing database) 20 | connect_to_db(app, "postgresql:///testdb") 21 | 22 | # Create tables and add sample data (uncomment when testing database) 23 | db.create_all() 24 | example_data() 25 | 26 | def tearDown(self): 27 | """ Do at end of every test - drop the database """ 28 | 29 | # (uncomment when testing database) 30 | db.session.close() 31 | db.drop_all() 32 | 33 | def test_signup_creates_new_user(self): 34 | """ Test that registering creates a new user """ 35 | 36 | # Adding a new test user to the database 37 | result = self.client.post("/sign-up", data=dict( 38 | name="kitty", 39 | email="kitty@kitty.com", 40 | password="kitty"), follow_redirects=True) 41 | 42 | # Checking is parameter a is in b 43 | self.assertIn("You have successfully signed up", result.data) 44 | 45 | # Query the database for the newly added user 46 | user_test = User.query.filter_by(name="kitty").first() 47 | 48 | # Verify that the user email is kitty@kitty.com - it will register as True 49 | self.assertTrue(user_test.email == "kitty@kitty.com") 50 | 51 | def test_signup_fail_user_already_exists(self): 52 | """ Test for an error in signing up as a user that already exists """ 53 | 54 | # Try to sign up as an already existing user 55 | result = self.client.post("/sign-up", data=dict( 56 | name="Mu", 57 | email="mu@mu.com", 58 | password="mu"), follow_redirects=True) 59 | 60 | # This should flash if a user already exists in the database 61 | self.assertIn("A user by this name already exists", result.data) 62 | 63 | def test_signin_fail_wrong_password(self): 64 | """ Test for an error in logging in with the incorrect password """ 65 | 66 | # Try signing in using the incorrect password 67 | result = self.client.post("/login-form", data=dict( 68 | email="mu@mu.com", 69 | password="m"), follow_redirects=True) 70 | 71 | # Check if the error phrase comes up 72 | self.assertIn("Error in logging in", result.data) 73 | 74 | def test_signin_fail_wrong_email(self): 75 | """ Test for an error in logging in with the incorrect email """ 76 | 77 | # Log in a fake user using the wrong email 78 | result = self.client.post("/login-form", data=dict( 79 | email="mu@m.com", 80 | password="mu"), follow_redirects=True) 81 | 82 | # Check if the error messages pops up 83 | self.assertIn("Error in logging in", result.data) 84 | 85 | def test_signin_fail_wrong_email_and_password(self): 86 | """ Test for an error in logging in with the incorrect email and password """ 87 | 88 | # Log in as a test client 89 | result = self.client.post("/login-form", data=dict( 90 | email="ashdgu@m.com", 91 | password="maskj"), follow_redirects=True) 92 | 93 | self.assertIn("Error in logging in", result.data) 94 | 95 | def test_signin_success(self): 96 | """ Test for successfully logging in """ 97 | 98 | # Log in as a test client 99 | result = self.client.post("/login-form", data=dict( 100 | email="mu@mu.com", 101 | password="mu"), follow_redirects=True) 102 | 103 | self.assertIn("Spent", result.data) 104 | 105 | def test_login_redirect(self): 106 | """ Test for successfully being redirected to the login form """ 107 | 108 | # Log in as a test client 109 | result = self.client.post("/login-form", data=dict( 110 | email="mu@mu.com", 111 | password="mu"), follow_redirects=True) 112 | 113 | self.assertIn("Spent", result.data) 114 | 115 | def test_add_budget_success(self): 116 | """ Test for successfully adding a budget """ 117 | 118 | # Log in a test client 119 | self.client.post("/login-form", data=dict( 120 | email="mu@mu.com", 121 | password="mu"), follow_redirects=True) 122 | 123 | # Add a budget 124 | result = self.client.post("/add-budget", data=dict( 125 | budget="100", 126 | category=3), follow_redirects=True) 127 | 128 | self.assertIn("100", result.data) 129 | 130 | # Verify that the correct category is in the database 131 | self.assertTrue("3") 132 | 133 | def test_add_expenditure_success(self): 134 | """ Test for successfully adding an expenditure to the database """ 135 | 136 | # Log in a test client 137 | self.client.post("/login-form", data=dict( 138 | email="mu@mu.com", 139 | password="mu"), follow_redirects=True) 140 | 141 | # Add an expenditure to the db 142 | result = self.client.post("/add-expenditure-to-db", data=dict( 143 | category=3, 144 | price=40, 145 | date=datetime.now(), 146 | where_bought="Whole Foods", 147 | description="groceries and stuff" 148 | ), follow_redirects=True) 149 | 150 | self.assertIn("3", result.data) 151 | 152 | # Verify that the correct price was addedt to the database 153 | self.assertTrue("40") 154 | 155 | def test_dashboard(self): 156 | """ Test if the dashboard routes correctly """ 157 | 158 | # Log in a test client 159 | self.client.post("/login-form", data=dict( 160 | email="mu@mu.com", 161 | password="mu"), follow_redirects=True) 162 | 163 | # Test the route 164 | result = self.client.get("/dashboard/1", follow_redirects=True) 165 | 166 | # Test if 'Dashboard' shows 167 | self.assertIn("Spent", result.data) 168 | 169 | self.assertIn("Account", result.data) 170 | 171 | self.assertTrue("Budget") 172 | 173 | def test_profile_edit(self): 174 | """ Test if editing profile info works """ 175 | 176 | # Log in a test client 177 | self.client.post("/login-form", data=dict( 178 | email="mu@mu.com", 179 | password="mu"), follow_redirects=True) 180 | 181 | # Test the route 182 | result = self.client.post("/profile-edit", data={ 183 | "profile-name": "kitty"}, follow_redirects=True) 184 | 185 | # Checking if the redirect works 186 | self.assertIn("kitty", result.data) 187 | 188 | # Query the database for the user 189 | profile_test_user = User.query.filter_by(email="mu@mu.com").first() 190 | 191 | # Verify that the new name is in the database 192 | self.assertTrue(profile_test_user.name == "kitty") 193 | 194 | # Verify that the user's old name is not in the database 195 | self.assertFalse(profile_test_user.name == "mu") 196 | 197 | def test_remove_budget(self): 198 | """ Test if a budget can be removed """ 199 | 200 | # Log in a test client 201 | self.client.post("/login-form", data=dict( 202 | email="mu@mu.com", 203 | password="mu"), follow_redirects=True) 204 | 205 | # Query the database for the budget 206 | budget_test = Budget.query.filter_by(budget=1000).first() 207 | 208 | # Query for the budget id 209 | budget_test_id = budget_test.id 210 | 211 | # Test the route 212 | result = self.client.post("/remove-budget/" + str(budget_test_id), follow_redirects=True) 213 | 214 | # Query for the removed budget 215 | budget_test_after_removal = Budget.query.filter_by(budget=1000).count() 216 | 217 | # See if the budget was removed by checking if count is 0 218 | self.assertTrue(budget_test_after_removal == 0) 219 | 220 | self.assertIn("Spent", result.data) 221 | 222 | def test_remove_expenditure(self): 223 | """ Test if expenditures can be removed """ 224 | 225 | # Log in a test client 226 | self.client.post("/login-form", data=dict( 227 | email="mu@mu.com", 228 | password="mu"), follow_redirects=True) 229 | 230 | # Query the database for the expenditure 231 | expenditure_test = Expenditure.query.filter_by(price=500).first() 232 | 233 | # Query for the expenditure id 234 | expenditure_test_id = expenditure_test.id 235 | 236 | # Test the route 237 | result = self.client.post("/remove-expenditure/" + str(expenditure_test_id), follow_redirects=True) 238 | 239 | # Query for the removed expenditure 240 | expenditure_test_after_removal = Expenditure.query.filter_by(price=500).count() 241 | 242 | # See if the expenditure was removed by checking if count is 0 243 | self.assertTrue(expenditure_test_after_removal == 0) 244 | 245 | self.assertIn("1", result.data) 246 | 247 | 248 | if __name__ == "__main__": 249 | unittest.main() 250 | -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from jinja2 import StrictUndefined 2 | 3 | # from pprint import pprint 4 | 5 | from datetime import datetime, timedelta 6 | 7 | import requests 8 | 9 | from flask import Flask, request, render_template, session, url_for, flash, redirect, jsonify, json, abort 10 | from flask_debugtoolbar import DebugToolbarExtension 11 | 12 | from model import User, connect_to_db, db, Expenditure, Budget 13 | 14 | from tools import expenditure_function, budget_totals, get_dates_for_budget, get_progress, get_budget_per_category 15 | 16 | from sqlalchemy.sql import and_ 17 | 18 | import os 19 | 20 | import hashlib 21 | 22 | import hmac 23 | 24 | import base64 25 | 26 | app = Flask(__name__) 27 | 28 | app.jinja_env.undefined = StrictUndefined 29 | 30 | app = Flask(__name__, instance_relative_config=True) 31 | 32 | app.secret_key = os.getenv('SECRET_KEY') 33 | 34 | spent_database = os.getenv('POSTGRES_DB_URL') 35 | connect_to_db(app, spent_database) 36 | 37 | 38 | @app.route('/') 39 | def index(): 40 | """ Homepage """ 41 | 42 | APP_ID = os.getenv('APP_ID') 43 | 44 | # This is the homepage 45 | return render_template("homepage.html", user_session_info=session, 46 | app_id=APP_ID) 47 | 48 | 49 | @app.route('/profile-edit', methods=["POST"]) 50 | def profile_edit(): 51 | """ Edit profile information """ 52 | 53 | # Set the value of the user id of the user in the session 54 | id = session.get('id') 55 | 56 | # Query the database for the user 57 | user_info = User.query.filter_by(id=id).first() 58 | 59 | # Get information from the forms 60 | name = request.form.get("profile-name") 61 | email = request.form.get("profile-email") 62 | password = request.form.get("new-password") 63 | 64 | # Replace info in the database with new info 65 | if name: 66 | user_info.name = name 67 | db.session.commit() 68 | 69 | if password: 70 | user_info.password = password 71 | db.session.commit() 72 | 73 | if email: 74 | user_info.email = email 75 | db.session.commit() 76 | 77 | name_info = { 78 | 'name': name, 79 | 'email': email 80 | } 81 | 82 | # Return jsonified budget info to submit-new-account-info.js 83 | return jsonify(name_info) 84 | 85 | 86 | @app.route('/webhook', methods=['POST']) 87 | def intercom_webhook(): 88 | 89 | x_signature_header = request.headers['X-Hub-Signature'] 90 | 91 | print 92 | print 93 | print 94 | print x_signature_header 95 | print 96 | print 97 | print 98 | 99 | json_blob = request.data 100 | 101 | print 102 | print 103 | print 104 | print json_blob 105 | print 106 | print 107 | print 108 | 109 | KEY = os.getenv('MARSH_SECRET') 110 | 111 | print 112 | print type(KEY) 113 | print "yo" 114 | print "hi" 115 | print 116 | 117 | hash_result = hmac.new(KEY, json_blob, hashlib.sha1).hexdigest() 118 | 119 | print 120 | print "hi" 121 | print 122 | print type(hash_result) 123 | print hash_result 124 | print "sha1=" + hash_result 125 | print 126 | 127 | if "sha1=" + hash_result == x_signature_header: 128 | 129 | print 130 | print 131 | print "cool" 132 | print 133 | print 134 | 135 | return 'OK' 136 | 137 | else: 138 | 139 | print 140 | print 141 | print "bad" 142 | print 143 | print 144 | 145 | return abort (400) 146 | 147 | 148 | @app.route('/tracking/', methods=["POST"]) 149 | def tracking_with_id(tracking_num): 150 | """ Handle the tracking information and display on the map """ 151 | 152 | # Get the expenditure associated with the tracking number 153 | expenditure_object = Expenditure.query.filter_by(tracking_num=tracking_num).first() 154 | 155 | # Get the carrier associated with the tracking number 156 | carrier = expenditure_object.tracking_num_carrier 157 | 158 | def shippo_url(track_num, carrier_info): 159 | """ Creates API call using tracking information the user input via the form """ 160 | 161 | url = "https://api.goshippo.com/v1/tracks/" + str(carrier_info) + "/" + str(track_num) + "/" 162 | return url 163 | 164 | # Returns the data we need 165 | shippo_tracking = shippo_url(tracking_num, carrier) 166 | result = requests.get(shippo_tracking) 167 | data = json.loads(result.content) 168 | 169 | final_dest = data['tracking_status']['location'] 170 | 171 | city = final_dest['city'] 172 | state = final_dest['state'] 173 | zipcode = final_dest['zip'] 174 | country = final_dest['country'] 175 | 176 | # This is the delivery status of the paackage 177 | tracking_status = data['tracking_status']['status'] 178 | 179 | address_info = { 180 | 'city': city, 181 | 'state': state, 182 | 'zipcode': zipcode, 183 | 'country': country, 184 | 'tracking_status': tracking_status 185 | } 186 | 187 | # Return jsonified budget info to map.js 188 | return jsonify(address_info) 189 | 190 | 191 | @app.route('/total-spent.json') 192 | def budget_types_data(): 193 | """ Bar chart shows totals for last 30 days """ 194 | 195 | id = session.get('id') 196 | 197 | # This is today's date 198 | today = datetime.today().strftime('%Y-%m-%d') 199 | # '2016-05-31' 200 | 201 | # This is the date 30 days in the past from today 202 | thirty_days_past = (datetime.today() + timedelta(-30)).strftime('%Y-%m-%d') 203 | 204 | # If the user id is in the session, this will render the dashboard 205 | # template, which will display their information and expenditure information 206 | if 'id' in session: 207 | 208 | # Unpacking the total price and average spent 209 | total_food_price, avg_food_expenditures = expenditure_function(3, id, thirty_days_past, today) 210 | total_groceries_price, avg_groceries_expenditures = expenditure_function(4, id, thirty_days_past, today) 211 | total_clothing_price, avg_clothing_expenditures = expenditure_function(5, id, thirty_days_past, today) 212 | total_entertainment_price, avg_entertainment_expenditures = expenditure_function(6, id, thirty_days_past, today) 213 | total_travel_price, avg_travel_expenditures = expenditure_function(2, id, thirty_days_past, today) 214 | total_online_purchase_price, avg_online_expenditures = expenditure_function(1, id, thirty_days_past, today) 215 | 216 | data_dict = { 217 | "labels": ["Food", "Groceries", "Clothing", "Entertainment", "Travel", "Online Purchases"], 218 | "datasets": [ 219 | { 220 | "label": "Total Spent", 221 | "fillColor": "#F37257", 222 | "strokeColor": "#F37257", 223 | "pointColor": "#F37257", 224 | "pointStrokeColor": "#fff", 225 | "pointHighlightFill": "#fff", 226 | "pointHighlightStroke": "#F37257", 227 | "data": [total_food_price, total_groceries_price, total_clothing_price, total_entertainment_price, total_travel_price, total_online_purchase_price] 228 | }, 229 | { 230 | "label": "Average", 231 | "fillColor": "#AFC1CC", 232 | "strokeColor": "#AFC1CC", 233 | "pointColor": "#AFC1CC", 234 | "pointStrokeColor": "#fff", 235 | "pointHighlightFill": "#fff", 236 | "pointHighlightStroke": "#AFC1CC", 237 | "data": [avg_food_expenditures, avg_groceries_expenditures, avg_clothing_expenditures, avg_entertainment_expenditures, avg_travel_expenditures, avg_online_expenditures] 238 | } 239 | ] 240 | } 241 | 242 | # This returns the data jsonified 243 | return jsonify(data_dict) 244 | 245 | 246 | @app.route('/expenditure-types.json') 247 | def expenditure_types_data(): 248 | """ Return data about expenditures to the donut chart """ 249 | 250 | # Get the id of the user in the session 251 | id = session.get('id') 252 | 253 | # This is today's date 254 | today = datetime.today().strftime('%Y-%m-%d') 255 | # '2016-05-31' 256 | 257 | # This is the date 30 days in the past from today 258 | thirty_days_past = (datetime.today() + timedelta(-30)).strftime('%Y-%m-%d') 259 | 260 | # Get the total amount spent per category by calling the get_expenditures function 261 | travel_expenditures, avg_travel = expenditure_function(2, id, thirty_days_past, today) 262 | entertainment_expenditures, avg_entertainment = expenditure_function(6, id, thirty_days_past, today) 263 | groceries_expenditures, avg_groceries = expenditure_function(4, id, thirty_days_past, today) 264 | clothing_expenditures, avg_clothing = expenditure_function(5, id, thirty_days_past, today) 265 | food_expenditures, avg_food = expenditure_function(3, id, thirty_days_past, today) 266 | online_purchase_expenditures, avg_online = expenditure_function(1, id, thirty_days_past, today) 267 | 268 | # Jsonified info 269 | data_list_of_dicts = { 270 | 'expenditures': [ 271 | { 272 | "value": travel_expenditures, 273 | "color": "#F4D27A", 274 | "highlight": "#963019", 275 | "label": "Travel" 276 | }, 277 | { 278 | "value": entertainment_expenditures, 279 | "color": "#517281", 280 | "highlight": "#963019", 281 | "label": "Entertainment" 282 | }, 283 | { 284 | "value": groceries_expenditures, 285 | "color": "#7895A2", 286 | "highlight": "#963019", 287 | "label": "Groceries" 288 | }, 289 | { 290 | "value": clothing_expenditures, 291 | "color": "#AFC1CC", 292 | "highlight": "#963019", 293 | "label": "Clothing" 294 | }, 295 | { 296 | "value": food_expenditures, 297 | "color": "#F37257", 298 | "highlight": "#963019", 299 | "label": "Food" 300 | }, 301 | { 302 | "value": online_purchase_expenditures, 303 | "color": "#F68D5C", 304 | "highlight": "#963019", 305 | "label": "Online Purchase" 306 | } 307 | ] 308 | } 309 | 310 | # Return jsonified info 311 | return jsonify(data_list_of_dicts) 312 | 313 | 314 | @app.route('/dashboard/') 315 | def dashboard(id): 316 | """ This is the user dashboard """ 317 | 318 | # If the user id is in the session, this will render the dashboard 319 | # template, which will display their information and expenditure information 320 | if 'id' in session: 321 | 322 | # This is the user object 323 | user = User.query.filter_by(id=id).first() 324 | 325 | ### GENERATE THE USER HASH ### 326 | 327 | APP_ID = os.getenv('APP_ID') 328 | KEY = os.getenv('SECURE_MODE_KEY') 329 | MESSAGE = str(user.id) 330 | hash_result = hmac.new(KEY, MESSAGE, hashlib.sha256).hexdigest() 331 | 332 | ####### GET THE USER'S BUDGETS FOR EACH CATEGORY 333 | 334 | cat_1_budget = get_budget_per_category(1, id) 335 | cat_2_budget = get_budget_per_category(2, id) 336 | cat_3_budget = get_budget_per_category(3, id) 337 | cat_4_budget = get_budget_per_category(4, id) 338 | cat_5_budget = get_budget_per_category(5, id) 339 | cat_6_budget = get_budget_per_category(6, id) 340 | 341 | # This is the expenditure object, which contains information about 342 | # expenditures specific to the user from the expenditure table in the 343 | # database 344 | expenditures = Expenditure.query.filter_by(expenditure_userid=id).all() 345 | 346 | ########### GET BUDGET START AND END DATES ########### 347 | 348 | # Calls the get_dates_for_budget function in tools.py 349 | cat_3_start, cat_3_end = get_dates_for_budget(3, id) 350 | cat_1_start, cat_1_end = get_dates_for_budget(1, id) 351 | cat_2_start, cat_2_end = get_dates_for_budget(2, id) 352 | cat_4_start, cat_4_end = get_dates_for_budget(4, id) 353 | cat_5_start, cat_5_end = get_dates_for_budget(5, id) 354 | cat_6_start, cat_6_end = get_dates_for_budget(6, id) 355 | 356 | # Strips datetime objects to year, month, day 357 | cat_3_start_date = cat_3_start.strftime('%m-%d-%Y') 358 | cat_1_start_date = cat_1_start.strftime('%m-%d-%Y') 359 | cat_2_start_date = cat_2_start.strftime('%m-%d-%Y') 360 | cat_4_start_date = cat_4_start.strftime('%m-%d-%Y') 361 | cat_5_start_date = cat_5_start.strftime('%m-%d-%Y') 362 | cat_6_start_date = cat_6_start.strftime('%m-%d-%Y') 363 | 364 | cat_3_end_date = cat_3_end.strftime('%m-%d-%Y') 365 | cat_1_end_date = cat_1_end.strftime('%m-%d-%Y') 366 | cat_2_end_date = cat_2_end.strftime('%m-%d-%Y') 367 | cat_4_end_date = cat_4_end.strftime('%m-%d-%Y') 368 | cat_5_end_date = cat_5_end.strftime('%m-%d-%Y') 369 | cat_6_end_date = cat_6_end.strftime('%m-%d-%Y') 370 | 371 | ########### TOTAL PRICE AND AVERAGE SPENT ########### 372 | 373 | # Unpacking the total price and average spent 374 | total_food_price, avg_food_expenditures = expenditure_function(3, id, cat_3_start_date, cat_3_end_date) 375 | total_groceries_price, avg_groceries_expenditures = expenditure_function(4, id, cat_4_start_date, cat_4_end_date) 376 | total_clothing_price, avg_clothing_expenditures = expenditure_function(5, id, cat_5_start_date, cat_5_end_date) 377 | total_entertainment_price, avg_entertainment_expenditures = expenditure_function(6, id, cat_6_start_date, cat_6_end_date) 378 | total_travel_price, avg_travel_expenditures = expenditure_function(2, id, cat_2_start_date, cat_2_end_date) 379 | total_online_purchase_price, avg_online_expenditures = expenditure_function(1, id, cat_1_start_date, cat_1_end_date) 380 | 381 | total_price = (total_food_price + total_groceries_price + total_clothing_price + 382 | total_entertainment_price + total_travel_price + 383 | total_online_purchase_price) 384 | 385 | ########### BUDGET ########### 386 | 387 | # Calling the function for each of the expenditure categories 388 | food_budget_minus_expenses = budget_totals(3, id, total_food_price) 389 | online_budget_minus_expenses = budget_totals(1, id, total_online_purchase_price) 390 | groceries_budget_minus_expenses = budget_totals(4, id, total_groceries_price) 391 | clothing_budget_minus_expenses = budget_totals(5, id, total_clothing_price) 392 | travel_budget_minus_expenses = budget_totals(2, id, total_travel_price) 393 | entertainment_budget_minus_expenses = budget_totals(6, id, total_entertainment_price) 394 | 395 | ############# PROGRESS BAR ############## 396 | 397 | # Call get_progress in tools.py to calculate the progress bar totals 398 | clothing_progress = get_progress(clothing_budget_minus_expenses, cat_5_budget) 399 | online_progress = get_progress(online_budget_minus_expenses, cat_1_budget) 400 | food_progress = get_progress(food_budget_minus_expenses, cat_3_budget) 401 | groceries_progress = get_progress(groceries_budget_minus_expenses, cat_4_budget) 402 | entertainment_progress = get_progress(entertainment_budget_minus_expenses, cat_6_budget) 403 | travel_progress = get_progress(travel_budget_minus_expenses, cat_2_budget) 404 | 405 | # Renders the dashboard, which displays the following info 406 | return render_template("dashboard.html", 407 | name=user.name, 408 | password=user.password, 409 | email=user.email, 410 | expenditures=expenditures, 411 | id=id, 412 | total_food_price=total_food_price, 413 | total_travel_price=total_travel_price, 414 | total_clothing_price=total_clothing_price, 415 | total_entertainment_price=total_entertainment_price, 416 | total_online_purchase_price=total_online_purchase_price, 417 | total_groceries_price=total_groceries_price, 418 | avg_online_expenditures=avg_online_expenditures, 419 | avg_entertainment_expenditures=avg_entertainment_expenditures, 420 | avg_clothing_expenditures=avg_clothing_expenditures, 421 | avg_travel_expenditures=avg_travel_expenditures, 422 | avg_groceries_expenditures=avg_groceries_expenditures, 423 | avg_food_expenditures=avg_food_expenditures, 424 | clothing_budget_minus_expenses=clothing_budget_minus_expenses, 425 | travel_budget_minus_expenses=travel_budget_minus_expenses, 426 | groceries_budget_minus_expenses=groceries_budget_minus_expenses, 427 | food_budget_minus_expenses=food_budget_minus_expenses, 428 | online_budget_minus_expenses=online_budget_minus_expenses, 429 | entertainment_budget_minus_expenses=entertainment_budget_minus_expenses, 430 | cat_1_budget=cat_1_budget, 431 | cat_2_budget=cat_2_budget, 432 | cat_3_budget=cat_3_budget, 433 | cat_4_budget=cat_4_budget, 434 | cat_5_budget=cat_5_budget, 435 | cat_6_budget=cat_6_budget, 436 | cat_1_start_date=cat_1_start_date, 437 | cat_2_start_date=cat_2_start_date, 438 | cat_3_start_date=cat_3_start_date, 439 | cat_4_start_date=cat_4_start_date, 440 | cat_5_start_date=cat_5_start_date, 441 | cat_6_start_date=cat_6_start_date, 442 | cat_1_end_date=cat_1_end_date, 443 | cat_2_end_date=cat_2_end_date, 444 | cat_3_end_date=cat_3_end_date, 445 | cat_4_end_date=cat_4_end_date, 446 | cat_5_end_date=cat_5_end_date, 447 | cat_6_end_date=cat_6_end_date, 448 | clothing_progress=clothing_progress, 449 | entertainment_progress=entertainment_progress, 450 | online_progress=online_progress, 451 | food_progress=food_progress, 452 | groceries_progress=groceries_progress, 453 | travel_progress=travel_progress, 454 | total_price=total_price, 455 | user_hash=hash_result, 456 | app_id=APP_ID) 457 | 458 | 459 | @app.route('/remove-budget/', methods=["POST"]) 460 | def remove_budget(id): 461 | """ Remove a budget from the database """ 462 | 463 | # This is the budget object we are working with 464 | budget_at_hand = Budget.query.filter_by(id=id).first() 465 | 466 | # This is the user id of the user in the session 467 | user_id = session.get('id') 468 | 469 | # Check to make sure the budget is associated with the logged in user 470 | if user_id == budget_at_hand.budget_userid: 471 | 472 | # Deletes the budget item from the budget table 473 | db.session.delete(budget_at_hand) 474 | db.session.commit() 475 | 476 | # Redirect the user to their dashboard 477 | return redirect(url_for('dashboard', id=user_id)) 478 | 479 | 480 | @app.route('/add-budget', methods=["POST"]) 481 | def add_budget(): 482 | """ Add a budget """ 483 | 484 | # Set the value of the user id of the user in the session 485 | id = session.get('id') 486 | 487 | # Get values from the form 488 | budget = request.form.get("budget") 489 | category_id = int(request.form.get("category")) 490 | start_date = request.form.get("start-date") 491 | end_date = request.form.get("end-date") 492 | 493 | user_budget_query = Budget.query.filter_by(budget_userid=id).all() 494 | 495 | # Check for budgets in the database under the user ID in particular categories; 496 | # delete budgets that exist to override them 497 | # Check to see if you can modify it instead 498 | for query in user_budget_query: 499 | if query.category_id == category_id: 500 | db.session.delete(query) 501 | db.session.commit() 502 | 503 | # Add the budget to the database. It will be the only budget for that 504 | # category in the database for the user 505 | new_budget = Budget(budget=budget, 506 | category_id=category_id, 507 | budget_userid=id, 508 | budget_start_date=start_date, 509 | budget_end_date=end_date) 510 | 511 | # Insert the new budget into the budget table and commit the insert 512 | db.session.add(new_budget) 513 | db.session.commit() 514 | 515 | # Call functions in tools.py 516 | total_cat_price, avg_cat_expenditures = expenditure_function(category_id, id, start_date, end_date) 517 | cat_budget_minus_expenses = budget_totals(category_id, id, total_cat_price) 518 | 519 | # Call get_progress in tools.py to calculate the progress bar totals 520 | category_progress = get_progress(cat_budget_minus_expenses, budget) 521 | 522 | budget_info = { 523 | 'id': new_budget.id, 524 | 'category': new_budget.category.category, 525 | 'category_id': category_id, 526 | 'budget': budget, 527 | 'cat_budget_minus_expenses': cat_budget_minus_expenses, 528 | 'category_progress': category_progress 529 | } 530 | 531 | # Return jsonified budget info to submit-budget.js 532 | return jsonify(budget_info) 533 | 534 | 535 | @app.route('/add-expenditure-to-db', methods=["POST"]) 536 | def add_expenditure(): 537 | """ Add new expenditure to the database """ 538 | 539 | # Set the value of the user id of the user in the session 540 | id = session.get('id') 541 | 542 | # Get values from the form 543 | category_id = int(request.form.get("category")) 544 | price = request.form.get("price") 545 | date_of_expenditure = request.form.get("date") 546 | where_bought = request.form.get("wherebought") 547 | description = request.form.get("description") 548 | tracking_num = request.form.get("tracking-num") 549 | tracking_num_carrier = request.form.get("tracking-num-carrier") 550 | 551 | start_date, end_date = get_dates_for_budget(category_id, id) 552 | 553 | # Create a new expenditure object to insert into the expenditures table 554 | new_expenditure = Expenditure(category_id=category_id, 555 | price=price, 556 | date_of_expenditure=date_of_expenditure, 557 | where_bought=where_bought, 558 | description=description, 559 | expenditure_userid=id, 560 | tracking_num=tracking_num, 561 | tracking_num_carrier=tracking_num_carrier) 562 | 563 | # Insert the new expenditure into the expenditures table and commit the insert 564 | db.session.add(new_expenditure) 565 | db.session.commit() 566 | 567 | # Unpacking the function call 568 | total_cat_price, avg_cat_expenditures = expenditure_function(category_id, id, start_date, end_date) 569 | 570 | budget_minus_expenses = budget_totals(category_id, id, total_cat_price) 571 | cat_budget = get_budget_per_category(category_id, id) 572 | category_progress = get_progress(budget_minus_expenses, cat_budget) 573 | 574 | expenditure_info = { 575 | 'total_cat_price': total_cat_price, 576 | 'avg_cat_expenditures': avg_cat_expenditures, 577 | 'category_id': category_id, 578 | 'expenditure_id': new_expenditure.id, 579 | 'date_of_expenditure': new_expenditure.date_of_expenditure.strftime('%Y-%m-%d'), 580 | 'where_bought': new_expenditure.where_bought, 581 | 'description': new_expenditure.description, 582 | 'price': str(new_expenditure.price), 583 | 'category': new_expenditure.category.category, 584 | 'tracking_num': new_expenditure.tracking_num, 585 | 'tracking_num_carrier': new_expenditure.tracking_num_carrier, 586 | 'cat_budget_minus_expenses': budget_minus_expenses, 587 | 'category_progress': category_progress 588 | } 589 | 590 | # Return jsonified info to submit-expenditure.js 591 | return jsonify(expenditure_info) 592 | 593 | 594 | @app.route('/remove-expenditure/', methods=["POST"]) 595 | def remove_expenditure(id): 596 | """ Remove an expenditure from the database """ 597 | 598 | # This is the expenditure object we are working with 599 | expenditure_at_hand = Expenditure.query.filter_by(id=id).first() 600 | 601 | # Deletes the expenditure item from the expenditure table 602 | db.session.delete(expenditure_at_hand) 603 | db.session.commit() 604 | 605 | # Return jsonified id to delete-expenditure.js 606 | return jsonify({"expenditure_id": id}) 607 | 608 | 609 | @app.route('/sign-up', methods=["POST"]) 610 | def sign_up(): 611 | """ Sign up form consumption """ 612 | 613 | # Gathering information from the sign up form 614 | name = request.form.get("name") 615 | email = request.form.get("email") 616 | password = request.form.get("password") 617 | 618 | # If the user does not exist, this will return None, and we will add them 619 | # to the database, otherwise we will flash an error message 620 | email_login_query = User.query.filter_by(email=email).first() 621 | 622 | # Check if user already exists 623 | if email_login_query is None: 624 | 625 | # If the user does not exist in the database, we add the user 626 | new_user = User() 627 | 628 | # Set the new user's name, email, and password 629 | new_user.name = name 630 | new_user.email = email 631 | new_user.password = password 632 | 633 | # Add the new user to the session - this is a database insertion 634 | db.session.add(new_user) 635 | db.session.commit() 636 | 637 | # Flash a message confirming the user has successfully signed up 638 | flash('You have successfully signed up') 639 | 640 | return redirect(url_for('index')) 641 | 642 | # Should the user already exist in the database, this will 643 | # redirect them back to the homepage and flash a message that says 644 | # a user with that information already exists 645 | else: 646 | user_existence_check = User.query.filter( 647 | and_( 648 | User.email == email, 649 | User.password == password)).first() 650 | 651 | if user_existence_check: 652 | 653 | # Flash a message saying a user by this name already exists 654 | flash('A user by this name already exists') 655 | 656 | # Take the user back to the homepage 657 | return redirect(url_for("index")) 658 | 659 | 660 | @app.route('/login-form', methods=["POST"]) 661 | def login_form(): 662 | """ Login form """ 663 | 664 | # FIXME: login breaks if incorrect email but correct password 665 | # login works with incorrect password 666 | 667 | # Gather information from the login form 668 | email = request.form.get("email") 669 | password = request.form.get("password") 670 | 671 | # If either of these return None, the user will not be able to 672 | # successfully log in 673 | email_login_query = User.query.filter_by(email=email).first() 674 | password_login_query = User.query.filter_by(password=password).first() 675 | 676 | # Check if email_login_query is empty 677 | if email_login_query is None and password_login_query is None: 678 | 679 | # Flash an error message if the login information provided by the user 680 | # does not match any records 681 | flash('Error in logging in') 682 | 683 | # Take the user back to the homepage so they can try logging in again 684 | # or sign up if they haven't 685 | return redirect(url_for("index")) 686 | 687 | # If the user logs in with the incorrect email an error message will flash 688 | # and they will not be logged in 689 | elif email_login_query is None: 690 | 691 | flash('Error in logging in') 692 | 693 | return redirect(url_for("index")) 694 | 695 | # If the user logs in with the incorrect password an error message will flash 696 | # and they will not be logged in 697 | elif password_login_query is None: 698 | 699 | flash('Error in logging in') 700 | 701 | return redirect(url_for("index")) 702 | 703 | else: 704 | # Put the id into the session 705 | session['id'] = email_login_query.id 706 | 707 | # Take the user to the dashboard page, using their id 708 | return redirect(url_for('dashboard', id=session['id'])) 709 | 710 | 711 | @app.route('/logout', methods=["GET"]) 712 | def logout(): 713 | """ Logs the user out """ 714 | 715 | # Remove the user id from the session if it exists 716 | session.pop('id', None) 717 | 718 | # Bring the user back to the homepage once they have been logged out 719 | return redirect(url_for('index')) 720 | 721 | 722 | if __name__ == "__main__": 723 | 724 | # We have to set debug=True here, since it has to be True at the point 725 | # that we invoke the DebugToolbarExtension 726 | app.debug = True 727 | app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False 728 | 729 | # spent_database = 'postgres:///spending' 730 | # connect_to_db(app, spent_database) 731 | 732 | # Use the DebugToolbar 733 | DebugToolbarExtension(app) 734 | 735 | app.run() 736 | -------------------------------------------------------------------------------- /templates/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block headerstuff %} 4 | 8 | 9 | 20 | 21 | {% endblock %} 22 | 23 | {% block content %} 24 | 25 | 26 | 27 | {% with messages = get_flashed_messages() %} 28 | {% if messages %} 29 |
    30 | {% for message in messages %} 31 |
  • {{ message }}
  • 32 | {% endfor %} 33 |
34 | {% endif %} 35 | {% endwith %} 36 | 37 | 38 | 39 |
40 | 41 | 122 | 123 | 124 | 125 |
126 |
127 | 128 |
129 |
130 |

AVERAGE SPENT OVER THE LAST 30 DAYS

131 |
132 |
133 | 134 | 135 | 136 |
137 | 138 |
139 |
140 | 141 |
142 |
143 | 144 |
145 |
146 |

TOTAL SPENT OVER THE LAST 30 DAYS

147 |
148 |
149 | 150 | 151 | 152 |
153 | 154 |
155 | 156 |
157 |
158 | 159 |
160 | 161 |

TOTAL SPENT

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 |
CategoryTotal SpentStart DateEnd Date
Clothing${{ '%0.2f' % total_clothing_price|float }}{{ cat_5_start_date }}{{ cat_5_end_date }}
Entertainment${{ '%0.2f' % total_entertainment_price|float }}{{ cat_6_start_date }}{{ cat_6_end_date }}
Food${{ '%0.2f' % total_food_price|float }}{{ cat_3_start_date }}{{ cat_3_end_date }}
Groceries${{ '%0.2f' % total_groceries_price|float }}{{ cat_4_start_date }}{{ cat_4_end_date }}
Online Purchases${{ '%0.2f' % total_online_purchase_price|float }}{{ cat_1_start_date }}{{ cat_1_end_date }}
Travel${{ '%0.2f' % total_travel_price|float }}{{ cat_2_start_date }}{{ cat_2_end_date }}
213 | 214 | 215 | 216 |
217 | 218 |
219 | 220 |

AVERAGE SPENT

221 | 222 | 223 | 224 | 225 |
226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 |
CategoryAverage SpentStart DateEnd Date
Clothing${{ '%0.2f' % avg_clothing_expenditures|float }}{{ cat_5_start_date }}{{ cat_5_end_date }}
Entertainment${{ '%0.2f' % avg_entertainment_expenditures|float }}{{ cat_6_start_date }}{{ cat_6_end_date }}
Food${{ '%0.2f' % avg_food_expenditures|float }}{{ cat_3_start_date }}{{ cat_3_end_date }}
Groceries${{ '%0.2f' % avg_groceries_expenditures|float }}{{ cat_4_start_date }}{{ cat_4_end_date }}
Online Purchases${{ '%0.2f' % avg_online_expenditures|float }}{{ cat_1_start_date }}{{ cat_1_end_date }}
Travel${{ '%0.2f' % avg_travel_expenditures|float }}{{ cat_2_start_date }}{{ cat_2_end_date }}
273 | 274 | 275 | 276 |
277 | 278 | 279 |
280 | 281 | 282 |
283 | 284 | 285 |
286 | 287 |
288 | 289 |

BUDGET

290 | 291 | Add Budget 292 | 293 |
294 | 295 | 296 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
CategoryBudgetStart DateEnd Date
Clothing${{ cat_5_budget }}{{ cat_5_start_date }}{{ cat_5_end_date }}
Entertainment${{ cat_6_budget }}{{ cat_6_start_date }}{{ cat_6_end_date }}
Food${{ cat_3_budget }}{{ cat_3_start_date }}{{ cat_3_end_date }}
Groceries${{ cat_4_budget }}{{ cat_4_start_date }}{{ cat_4_end_date }}
Online Purchases${{ cat_1_budget }}{{ cat_1_start_date }}{{ cat_1_end_date }}
Travel${{ cat_2_budget }}{{ cat_2_start_date }}{{ cat_2_end_date }}
392 | 393 | 394 | 395 |
396 | 397 |
398 | 399 |

BUDGET REMAINING

400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 420 | 421 | 422 | 423 | 424 | 433 | 434 | 435 | 436 | 437 | 446 | 447 | 448 | 449 | 450 | 459 | 460 | 461 | 462 | 463 | 472 | 473 | 474 | 475 | 476 | 485 | 486 | 487 | 488 |
CategoryBudget Remaining
Clothing 412 | 413 |
414 |
415 | ${{ '%0.2f' % clothing_budget_minus_expenses|float }} 416 |
417 |
418 | 419 |
Entertainment 425 | 426 |
427 |
428 | ${{ '%0.2f' % entertainment_budget_minus_expenses|float }} 429 |
430 |
431 | 432 |
Food 438 | 439 |
440 |
441 | ${{ '%0.2f' % food_budget_minus_expenses|float }} 442 |
443 |
444 | 445 |
Groceries 451 | 452 |
453 |
454 | ${{ '%0.2f' % groceries_budget_minus_expenses|float }} 455 |
456 |
457 | 458 |
Online Purchases 464 | 465 |
466 |
467 | ${{ '%0.2f' % online_budget_minus_expenses|float }} 468 |
469 |
470 | 471 |
Travel 477 | 478 |
479 |
480 | ${{ '%0.2f' % travel_budget_minus_expenses|float }} 481 |
482 |
483 | 484 |
489 | 490 |
491 | 492 | 493 | 494 |
495 |
496 |

EXPENDITURES

497 | Add Expenditure 498 |
499 | 500 | 501 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | {% for expenditure in expenditures %} 581 |
582 |
583 | 584 | 585 | 586 | 587 | 588 | 592 | 602 | 603 | 604 | {% endfor %} 605 | 606 | 607 |
CategoryPriceDatePlaceDescriptionTrackRemove
{{ expenditure.category.category }}${{ expenditure.price }}{{ expenditure.date_of_expenditure.strftime('%Y-%m-%d') }}{{ expenditure.where_bought }}{{ expenditure.description }}{% if expenditure.tracking_num %} 589 |
{% endif %}
593 |
594 | 595 | 596 | 597 | 600 | 601 |
608 | 609 | 610 | 649 | 650 |
651 |
652 | 653 |
654 | 655 |
656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | {% endblock %} -------------------------------------------------------------------------------- /static/bootstrap-3.3.6/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>2)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.6",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.6",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.6",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.6",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.6",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------