├── .gitignore ├── AUTHORS.txt ├── CHANGELOG.txt ├── LICENSE.txt ├── README.rst ├── budget-sample-templates ├── media │ ├── css │ │ ├── basic.css │ │ ├── ie6.css │ │ ├── print.css │ │ └── ui.datepicker.css │ ├── img │ │ └── calendar.gif │ └── js │ │ ├── basic.js │ │ ├── jquery-1.2.6.js │ │ └── ui.datepicker.js └── templates │ ├── base.html │ └── budget │ ├── budgets │ ├── add.html │ ├── delete.html │ ├── edit.html │ ├── list.html │ └── show.html │ ├── categories │ ├── add.html │ ├── delete.html │ ├── edit.html │ └── list.html │ ├── dashboard.html │ ├── estimates │ ├── add.html │ ├── delete.html │ ├── edit.html │ └── list.html │ ├── pagination.html │ ├── setup.html │ ├── summaries │ ├── summary_list.html │ ├── summary_month.html │ └── summary_year.html │ └── transactions │ ├── add.html │ ├── delete.html │ ├── edit.html │ └── list.html ├── budget ├── __init__.py ├── admin.py ├── categories │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ └── categories_testdata.yaml │ ├── forms.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── forms.py ├── locale │ └── es │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── models.py ├── templatetags │ ├── __init__.py │ └── budget.py ├── tests.py ├── transactions │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── urls.py └── views.py └── docs └── budget.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Primary author: 2 | 3 | * Daniel Lindsley 4 | 5 | Contributors: 6 | 7 | * Brad Pitcher (brad) for a patch involving deleted estimates. 8 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | v1.0.3 2 | ====== 3 | 4 | * Fixed the ``Budget`` model to exclude deleted estimates. 5 | 6 | 7 | v1.0.2 8 | ====== 9 | 10 | * Corrected end_date in the dashboard view. 11 | * Corrected end_date in the summary_month view. 12 | 13 | 14 | v1.0.1 15 | ====== 16 | 17 | * Added much needed default timestamp to "updated" field. 18 | 19 | 20 | v1.0 21 | ==== 22 | 23 | * Initial release. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010, Daniel Lindsley 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | django-budget 3 | ============= 4 | 5 | ``django-budget`` is a simple budgeting application for use with Django. It is 6 | designed to manage personal finances. We used it to replace a complicated Excel 7 | spreadsheet that didn't retain all the details we wanted. 8 | 9 | It was implemented in Django based on familiarity, quick time to implement and 10 | the premise that it could be accessible everywhere. In practice, we run this 11 | locally (NOT on a publicly accessible website). 12 | 13 | 14 | A Note About Security 15 | ===================== 16 | 17 | It is recommended that anyone using this application add further security by 18 | either protecting the whole app with HTTP Auth, wrap the views with the 19 | ``login_required`` decorator, run it on a local machine or implement similar 20 | protections. This application is for your use and makes no assumptions about 21 | how viewable the data is to other people. 22 | 23 | 24 | Requirements 25 | ============ 26 | 27 | ``django-budget`` requires: 28 | 29 | * Python 2.3+ 30 | * Django 1.0+ 31 | 32 | 33 | Installation 34 | ============ 35 | 36 | * Either copy/symlink the budget app into your project or place it somewhere on 37 | your ``PYTHONPATH``. 38 | * Add the ``budget.categories``, ``budget.transactions`` and ``budget`` apps to 39 | your ``INSTALLED_APPS``. 40 | * Run ``./manage.py syncdb``. 41 | * Add ``(r'^budget/', include('budgetproject.budget.urls')),`` to your 42 | ``urls.py``. 43 | 44 | 45 | About The Templates/Media 46 | ========================= 47 | 48 | The templates provided are for reference only and ARE NOT SUPPORTED! Please do 49 | not submit bugs or feature requests for them. You will likely have to create 50 | your own templates or at least heavily modify these templates to adapt them to 51 | your own uses. Everything within the templates is either original or MIT 52 | licensed. -------------------------------------------------------------------------------- /budget-sample-templates/media/css/basic.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background-color: #BBDDFF; 8 | color: #333333; 9 | font-family: Helvetica, Verdana, Arial; 10 | font-size: 14px; 11 | line-height: 18px; 12 | text-align: center; 13 | } 14 | 15 | h1, h2, h3, h4, h5, h6 { 16 | margin-bottom: 10px; 17 | } 18 | 19 | p { 20 | margin-bottom: 10px; 21 | } 22 | 23 | a, a:visited, a:hover { 24 | color: #0000FF; 25 | } 26 | 27 | img { 28 | border-width: 0px; 29 | } 30 | 31 | ul li, ol li, dl dd { 32 | margin-left: 20px; 33 | } 34 | 35 | table { 36 | border-collapse: collapse; 37 | margin-bottom: 10px; 38 | } 39 | 40 | table tr th, 41 | table tr td { 42 | text-align: left; 43 | vertical-align: top; 44 | } 45 | 46 | label { 47 | margin-right: 10px; 48 | } 49 | 50 | input, select, textarea { 51 | margin-bottom: 5px; 52 | } 53 | 54 | 55 | 56 | #page_wrapper { 57 | background-color: #FFFFFF; 58 | margin-bottom: 10px; 59 | margin-left: auto; 60 | margin-right: auto; 61 | margin-top: 10px; 62 | text-align: left; 63 | width: 760px; 64 | } 65 | 66 | #page_header { 67 | background-color: #BBDDFF; 68 | padding: 10px; 69 | text-align: center; 70 | } 71 | 72 | #page_navigation { 73 | background-color: #BBDDFF; 74 | height: 26px; 75 | } 76 | 77 | #page_navigation ul { 78 | list-style: none; 79 | } 80 | 81 | #page_navigation ul li { 82 | background-color: #E0E0E0; 83 | float: left; 84 | margin-left: 0px; 85 | margin-right: 5px; 86 | } 87 | 88 | #page_navigation ul li a, 89 | #page_navigation ul li a:visited, 90 | #page_navigation ul li a:hover { 91 | color: #333333; 92 | display: block; 93 | font-weight: bold; 94 | padding: 4px; 95 | text-decoration: none; 96 | } 97 | 98 | #page_navigation ul li a.active { 99 | background-color: #FFFFFF; 100 | } 101 | 102 | #page_content { 103 | clear: both; 104 | padding: 10px; 105 | } 106 | 107 | #content { 108 | margin: 10px; 109 | } 110 | 111 | #page_footer { 112 | clear: both; 113 | text-align: center; 114 | } 115 | 116 | #latest_expenses { 117 | float: left; 118 | width: 45%; 119 | margin-right: 10px; 120 | margin-top: 10px; 121 | } 122 | 123 | #latest_incomes { 124 | float: left; 125 | width: 45%; 126 | margin-top: 10px; 127 | } 128 | 129 | #budget_progress_bar { 130 | clear: both; 131 | padding-top: 10px; 132 | text-align: center; 133 | } 134 | 135 | #progress_wrapper { 136 | border: solid 1px #333333; 137 | height: 20px; 138 | overflow: hidden; 139 | text-align: left; 140 | } 141 | 142 | #progress_bar { 143 | height: 100%; 144 | } 145 | 146 | #progress_bar.green { 147 | background-color: #88FF88; 148 | } 149 | 150 | #progress_bar.yellow { 151 | background-color: #FFFF88; 152 | } 153 | 154 | #progress_bar.red { 155 | background-color: #FF8888; 156 | } 157 | 158 | .report_table { 159 | width: 100%; 160 | } 161 | 162 | .report_table thead { 163 | border-bottom: solid 1px #333333; 164 | } 165 | 166 | .report_table .numeric { 167 | text-align: right; 168 | width: 25%; 169 | } 170 | 171 | .report_table .even { 172 | background-color: #EEEEEE; 173 | border-bottom: solid 1px #E0E0E0; 174 | border-top: solid 1px #E0E0E0; 175 | } 176 | 177 | .report_table td span.green { 178 | color: inherit; 179 | } 180 | 181 | .report_table td span.yellow { 182 | color: #AAAA00; 183 | } 184 | 185 | .report_table td span.red { 186 | color: #CC3333; 187 | } 188 | 189 | .report_table tfoot { 190 | border-top: solid 1px #333333; 191 | } 192 | 193 | .form_table { 194 | margin-top: 10px; 195 | } 196 | 197 | .deletion { 198 | margin-top: 15px; 199 | } 200 | 201 | .previous_next_wrapper { 202 | height: 20px; 203 | padding-top: 10px; 204 | } 205 | 206 | .previous, 207 | .next { 208 | float: left; 209 | margin-right: 5px; 210 | } 211 | 212 | .previous a, 213 | .previous span, 214 | .next a, 215 | .next span { 216 | background-color: #EEEEEE; 217 | border: solid 1px #E0E0E0; 218 | display: block; 219 | padding: 5px; 220 | text-decoration: none; 221 | } 222 | 223 | .previous a:hover, 224 | .next a:hover { 225 | background-color: #DDDDFF; 226 | border: solid 1px #0000FF; 227 | } 228 | 229 | #id_start_date_1 { 230 | /* Cheap trick to make the DatePicker look correct. */ 231 | display: block; 232 | } 233 | 234 | .hide_show_button, 235 | .hide_show_button:visited { 236 | color: #888888; 237 | text-decoration: none; 238 | } 239 | 240 | .transaction_table { 241 | display: none; 242 | margin: 0px 0px 0px 20px; 243 | width: 100%; 244 | } 245 | 246 | .transaction_table tbody tr td { 247 | color: #888888; 248 | width: 20%; 249 | } 250 | 251 | .transaction_table tbody tr td.wide { 252 | width: 60%; 253 | } -------------------------------------------------------------------------------- /budget-sample-templates/media/css/ie6.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget-sample-templates/media/css/ie6.css -------------------------------------------------------------------------------- /budget-sample-templates/media/css/print.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background-color: #FFFFFF; 8 | font-family: Helvetica, Verdana, Arial; 9 | font-size: 14px; 10 | line-height: 18px; 11 | } 12 | 13 | h1, h2, h3, h4, h5, h6 { 14 | margin-bottom: 10px; 15 | } 16 | 17 | p { 18 | margin-bottom: 10px; 19 | } 20 | 21 | a, a:visited, a:hover { 22 | color: #0000FF; 23 | } 24 | 25 | img { 26 | border-width: 0px; 27 | } 28 | 29 | ul li, ol li, dl dd { 30 | margin-left: 20px; 31 | } 32 | 33 | table { 34 | border-collapse: collapse; 35 | margin-bottom: 10px; 36 | } 37 | 38 | table tr th, 39 | table tr td { 40 | text-align: left; 41 | vertical-align: top; 42 | } 43 | 44 | label { 45 | margin-right: 10px; 46 | } 47 | 48 | input, select, textarea { 49 | margin-bottom: 5px; 50 | } 51 | 52 | 53 | 54 | #page_wrapper { 55 | width: 760px; 56 | } 57 | 58 | #page_header { 59 | text-align: center; 60 | } 61 | 62 | #page_navigation { 63 | display: none; 64 | } 65 | 66 | #page_content { 67 | padding: 10px; 68 | } 69 | 70 | #content { 71 | margin: 10px; 72 | } 73 | 74 | #page_footer { 75 | clear: both; 76 | text-align: center; 77 | } 78 | 79 | .report_table { 80 | width: 100%; 81 | } 82 | 83 | .report_table thead { 84 | border-bottom: solid 1px #333333; 85 | } 86 | 87 | .report_table .numeric { 88 | text-align: right; 89 | width: 25%; 90 | } 91 | 92 | .report_table .even { 93 | background-color: #EEEEEE; 94 | border-bottom: solid 1px #E0E0E0; 95 | border-top: solid 1px #E0E0E0; 96 | } 97 | 98 | .report_table tfoot { 99 | border-top: solid 1px #333333; 100 | } 101 | 102 | .form_table { 103 | margin-top: 10px; 104 | } 105 | 106 | .previous_next_wrapper { 107 | display: none; 108 | } -------------------------------------------------------------------------------- /budget-sample-templates/media/css/ui.datepicker.css: -------------------------------------------------------------------------------- 1 | /* Main Style Sheet for jQuery UI date picker */ 2 | #ui-datepicker-div, .ui-datepicker-inline { 3 | font-family: Arial, Helvetica, sans-serif; 4 | font-size: 14px; 5 | padding: 0; 6 | margin: 0; 7 | background: #ddd; 8 | width: 185px; 9 | } 10 | #ui-datepicker-div { 11 | display: none; 12 | border: 1px solid #777; 13 | z-index: 100; /*must have*/ 14 | } 15 | .ui-datepicker-inline { 16 | float: left; 17 | display: block; 18 | border: 0; 19 | } 20 | .ui-datepicker-rtl { 21 | direction: rtl; 22 | } 23 | .ui-datepicker-dialog { 24 | padding: 5px !important; 25 | border: 4px ridge #ddd !important; 26 | } 27 | .ui-datepicker-disabled { 28 | position: absolute; 29 | z-index: 100; 30 | background-color: white; 31 | opacity: 0.5; 32 | } 33 | button.ui-datepicker-trigger { 34 | width: 25px; 35 | } 36 | img.ui-datepicker-trigger { 37 | margin: 2px; 38 | vertical-align: middle; 39 | } 40 | .ui-datepicker-prompt { 41 | float: left; 42 | padding: 2px; 43 | background: #ddd; 44 | color: #000; 45 | } 46 | * html .ui-datepicker-prompt { 47 | width: 185px; 48 | } 49 | .ui-datepicker-control, .ui-datepicker-links, .ui-datepicker-header, .ui-datepicker { 50 | clear: both; 51 | float: left; 52 | width: 100%; 53 | color: #fff; 54 | } 55 | .ui-datepicker-control { 56 | background: #400; 57 | padding: 2px 0px; 58 | } 59 | .ui-datepicker-links { 60 | background: #000; 61 | padding: 2px 0px; 62 | } 63 | .ui-datepicker-control, .ui-datepicker-links { 64 | font-weight: bold; 65 | font-size: 80%; 66 | } 67 | .ui-datepicker-links label { /* disabled links */ 68 | padding: 2px 5px; 69 | color: #888; 70 | } 71 | .ui-datepicker-clear, .ui-datepicker-prev { 72 | float: left; 73 | width: 34%; 74 | } 75 | .ui-datepicker-rtl .ui-datepicker-clear, .ui-datepicker-rtl .ui-datepicker-prev { 76 | float: right; 77 | text-align: right; 78 | } 79 | .ui-datepicker-current { 80 | float: left; 81 | width: 30%; 82 | text-align: center; 83 | } 84 | .ui-datepicker-close, .ui-datepicker-next { 85 | float: right; 86 | width: 34%; 87 | text-align: right; 88 | } 89 | .ui-datepicker-rtl .ui-datepicker-close, .ui-datepicker-rtl .ui-datepicker-next { 90 | float: left; 91 | text-align: left; 92 | } 93 | .ui-datepicker-header { 94 | padding: 1px 0 3px; 95 | background: #333; 96 | text-align: center; 97 | font-weight: bold; 98 | height: 1.3em; 99 | } 100 | .ui-datepicker-header select { 101 | background: #333; 102 | color: #fff; 103 | border: 0px; 104 | font-weight: bold; 105 | } 106 | .ui-datepicker { 107 | background: #ccc; 108 | text-align: center; 109 | font-size: 100%; 110 | } 111 | .ui-datepicker a { 112 | display: block; 113 | width: 100%; 114 | } 115 | .ui-datepicker-title-row { 116 | background: #777; 117 | } 118 | .ui-datepicker-days-row { 119 | background: #eee; 120 | color: #666; 121 | } 122 | .ui-datepicker-week-col { 123 | background: #777; 124 | color: #fff; 125 | } 126 | .ui-datepicker-days-cell { 127 | color: #000; 128 | border: 1px solid #ddd; 129 | } 130 | .ui-datepicker-days-cell a{ 131 | display: block; 132 | } 133 | .ui-datepicker-week-end-cell { 134 | background: #ddd; 135 | } 136 | .ui-datepicker-title-row .ui-datepicker-week-end-cell { 137 | background: #777; 138 | } 139 | .ui-datepicker-days-cell-over { 140 | background: #fff; 141 | border: 1px solid #777; 142 | } 143 | .ui-datepicker-unselectable { 144 | color: #888; 145 | } 146 | .ui-datepicker-today { 147 | background: #fcc !important; 148 | } 149 | .ui-datepicker-current-day { 150 | background: #999 !important; 151 | } 152 | .ui-datepicker-status { 153 | background: #ddd; 154 | width: 100%; 155 | font-size: 80%; 156 | text-align: center; 157 | } 158 | 159 | /* ________ Datepicker Links _______ 160 | 161 | ** Reset link properties and then override them with !important */ 162 | #ui-datepicker-div a, .ui-datepicker-inline a { 163 | cursor: pointer; 164 | margin: 0; 165 | padding: 0; 166 | background: none; 167 | color: #000; 168 | } 169 | .ui-datepicker-inline .ui-datepicker-links a { 170 | padding: 0 5px !important; 171 | } 172 | .ui-datepicker-control a, .ui-datepicker-links a { 173 | padding: 2px 5px !important; 174 | color: #eee !important; 175 | } 176 | .ui-datepicker-title-row a { 177 | color: #eee !important; 178 | } 179 | .ui-datepicker-control a:hover { 180 | background: #fdd !important; 181 | color: #333 !important; 182 | } 183 | .ui-datepicker-links a:hover, .ui-datepicker-title-row a:hover { 184 | background: #ddd !important; 185 | color: #333 !important; 186 | } 187 | 188 | /* ___________ MULTIPLE MONTHS _________*/ 189 | 190 | .ui-datepicker-multi .ui-datepicker { 191 | border: 1px solid #777; 192 | } 193 | .ui-datepicker-one-month { 194 | float: left; 195 | width: 185px; 196 | } 197 | .ui-datepicker-new-row { 198 | clear: left; 199 | } 200 | 201 | /* ___________ IE6 IFRAME FIX ________ */ 202 | 203 | .ui-datepicker-cover { 204 | display: none; /*sorry for IE5*/ 205 | display/**/: block; /*sorry for IE5*/ 206 | position: absolute; /*must have*/ 207 | z-index: -1; /*must have*/ 208 | filter: mask(); /*must have*/ 209 | top: -4px; /*must have*/ 210 | left: -4px; /*must have*/ 211 | width: 200px; /*must have*/ 212 | height: 200px; /*must have*/ 213 | } 214 | -------------------------------------------------------------------------------- /budget-sample-templates/media/img/calendar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget-sample-templates/media/img/calendar.gif -------------------------------------------------------------------------------- /budget-sample-templates/media/js/basic.js: -------------------------------------------------------------------------------- 1 | var Basic = { 2 | setup: function() { 3 | Basic.highlight_current_tab(); 4 | Basic.setup_datepickers(); 5 | Basic.setup_hide_show_buttons(); 6 | }, 7 | 8 | highlight_current_tab: function() { 9 | var current_location = window.location.pathname; 10 | 11 | if(current_location.match(/summary/)) { 12 | $('#page_navigation ul li a[href*=summary]').addClass('active'); 13 | } 14 | else if(current_location.match(/category/) || current_location.match(/budget\/budget/) || current_location.match(/setup/)) { 15 | $('#page_navigation ul li a[href*=setup]').addClass('active'); 16 | } 17 | else if(current_location.match(/transaction/)) { 18 | $('#page_navigation ul li a[href*=transaction]').addClass('active'); 19 | } 20 | else { 21 | // Dashboard special case. 22 | $('#page_navigation ul li:first a').addClass('active'); 23 | } 24 | }, 25 | 26 | setup_datepickers: function() { 27 | $("input[type=text]#id_date").datepicker({ 28 | dateFormat: $.datepicker.ISO_8601, 29 | showOn: "both", 30 | buttonImage: "/img/calendar.gif", 31 | buttonImageOnly: true 32 | }); 33 | 34 | $("input[type=text]#id_start_date_0").datepicker({ 35 | dateFormat: $.datepicker.ISO_8601, 36 | showOn: "both", 37 | buttonImage: "/img/calendar.gif", 38 | buttonImageOnly: true 39 | }); 40 | }, 41 | 42 | setup_hide_show_buttons: function() { 43 | $('.hide_show_button').click(function() { 44 | if($(this).text() == "[+]") { 45 | $(this).text("[-]"); 46 | } 47 | else { 48 | $(this).text("[+]"); 49 | } 50 | 51 | var hide_show_id = $(this).attr('id') 52 | var relevant_transactions_id = hide_show_id.replace('id_hide_show', 'id_hidden_transaction_list'); 53 | $('#' + relevant_transactions_id).toggle(); 54 | return false; 55 | }) 56 | } 57 | } 58 | 59 | $(document).ready(Basic.setup); -------------------------------------------------------------------------------- /budget-sample-templates/media/js/jquery-1.2.6.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery 1.2.6 - New Wave Javascript 3 | * 4 | * Copyright (c) 2008 John Resig (jquery.com) 5 | * Dual licensed under the MIT (MIT-LICENSE.txt) 6 | * and GPL (GPL-LICENSE.txt) licenses. 7 | * 8 | * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ 9 | * $Rev: 5685 $ 10 | */ 11 | (function(){var _jQuery=window.jQuery,_$=window.$;var jQuery=window.jQuery=window.$=function(selector,context){return new jQuery.fn.init(selector,context);};var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,isSimple=/^.[^:#\[\.]*$/,undefined;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem){if(elem.id!=match[3])return jQuery().find(selector);return jQuery(elem);}selector=[];}}else 12 | return jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(jQuery.makeArray(selector));},jquery:"1.2.6",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;return jQuery.inArray(elem&&elem.jquery?elem[0]:elem,this);},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value===undefined)return this[0]&&jQuery[type||"attr"](this[0],name);else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else 13 | return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else 14 | selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),typeof selector=='string'?jQuery(selector):jQuery.makeArray(selector))));},is:function(selector){return!!selector&&jQuery.multiFilter(selector,this).length>0;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=jQuery.makeArray(value);jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else 16 | this.value=value;});},html:function(value){return value==undefined?(this[0]?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value===undefined){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data===undefined&&this.length)data=jQuery.data(this[0],key);return data===undefined&&parts[1]?this.data(parts[0]):data;}else 17 | return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script"))scripts=scripts.add(elem);else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.fn.init.prototype=jQuery.fn;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else 18 | jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}function now(){return+new Date;}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==i){target=this;--i;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else 22 | jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret,style=elem.style;function color(elem){if(!jQuery.browser.safari)return false;var ret=defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=style.outline;style.outline="0 solid black";style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&style&&style[name])ret=style[name];else if(defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var computedStyle=defaultView.getComputedStyle(elem,null);if(computedStyle&&!color(elem))ret=computedStyle.getPropertyValue(name);else{var swap=[],stack=[],a=elem,i=0;for(;a&&color(a);a=a.parentNode)stack.unshift(a);for(;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!tags.indexOf("",""]||(!tags.indexOf("",""]||!tags.indexOf("",""]||jQuery.browser.msie&&[1,"div
","
"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf(""&&tags.indexOf("=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else 23 | ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var notxml=!jQuery.isXMLDoc(elem),set=value!==undefined,msie=jQuery.browser.msie;name=notxml&&jQuery.props[name]||name;if(elem.tagName){var special=/href|src|style/.test(name);if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(name in elem&¬xml&&!special){if(set){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem[name]=value;}if(jQuery.nodeName(elem,"form")&&elem.getAttributeNode(name))return elem.getAttributeNode(name).nodeValue;return elem[name];}if(msie&¬xml&&name=="style")return jQuery.attr(elem.style,"cssText",value);if(set)elem.setAttribute(name,""+value);var attr=msie&¬xml&&special?elem.getAttribute(name,2):elem.getAttribute(name);return attr===null?undefined:attr;}if(msie&&name=="opacity"){if(set){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(value)+''=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100)+'':"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(set)elem[name]=value;return elem[name];},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(array!=null){var i=array.length;if(i==null||array.split||array.setInterval||array.call)ret[0]=array;else 24 | while(i)ret[--i]=array[i];}return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return im[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false,re=quickChild,m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[],cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&n!=elem)r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=this.proxy(fn,function(){return fn.apply(this,arguments);});handler.data=data;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){if(typeof jQuery!="undefined"&&!jQuery.event.triggered)return jQuery.event.handle.apply(arguments.callee.elem,arguments);});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else 26 | for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event){data.unshift({type:type,target:elem,preventDefault:function(){},stopPropagation:function(){},timeStamp:now()});data[0][expando]=true;}data[0].type=type;if(exclusive)data[0].exclusive=true;var handle=jQuery.data(elem,"handle");if(handle)val=handle.apply(elem,data);if((!fn||(jQuery.nodeName(elem,'a')&&type=="click"))&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val,ret,namespace,all,handlers;event=arguments[0]=jQuery.event.fix(event||window.event);namespace=event.type.split(".");event.type=namespace[0];namespace=namespace[1];all=!namespace&&!event.exclusive;handlers=(jQuery.data(this,"events")||{})[event.type];for(var j in handlers){var handler=handlers[j];if(all||handler.type==namespace){event.handler=handler;event.data=handler.data;ret=handler.apply(this,arguments);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}return val;},fix:function(event){if(event[expando]==true)return event;var originalEvent=event;event={originalEvent:originalEvent};var props="altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");for(var i=props.length;i;i--)event[props[i]]=originalEvent[props[i]];event[expando]=true;event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};event.timeStamp=event.timeStamp||now();if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=event.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},proxy:function(fn,proxy){proxy.guid=fn.guid=fn.guid||proxy.guid||this.guid++;return proxy;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){var one=jQuery.event.proxy(fn||data,function(event){jQuery(this).unbind(event,one);return(fn||data).apply(this,arguments);});return this.each(function(){jQuery.event.add(this,type,one,fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){return this[0]&&jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(fn){var args=arguments,i=1;while(i=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("
").append(res.responseText.replace(//g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=now();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{url:location.href,global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));var jsonp,jsre=/=\?(&|$)/g,status,data,type=s.type.toUpperCase();if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(type=="GET"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&type=="GET"){var ts=now();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&type=="GET"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");var remote=/^(?:\w+:)?\/\/([^\/?#]+)/;if(s.dataType=="script"&&type=="GET"&&remote.test(s.url)&&remote.exec(s.url)[1]!=location.host){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xhr=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();if(s.username)xhr.open(type,s.url,s.async,s.username,s.password);else 28 | xhr.open(type,s.url,s.async);try{if(s.data)xhr.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xhr.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");xhr.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend&&s.beforeSend(xhr,s)===false){s.global&&jQuery.active--;xhr.abort();return false;}if(s.global)jQuery.event.trigger("ajaxSend",[xhr,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xhr&&(xhr.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xhr)&&"error"||s.ifModified&&jQuery.httpNotModified(xhr,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xhr,s.dataType,s.dataFilter);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xhr.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else 29 | jQuery.handleError(s,xhr,status);complete();if(s.async)xhr=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xhr){xhr.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xhr.send(s.data);}catch(e){jQuery.handleError(s,xhr,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xhr,s]);}function complete(){if(s.complete)s.complete(xhr,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xhr,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xhr;},handleError:function(s,xhr,status,e){if(s.error)s.error(xhr,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xhr,s,e]);},active:0,httpSuccess:function(xhr){try{return!xhr.status&&location.protocol=="file:"||(xhr.status>=200&&xhr.status<300)||xhr.status==304||xhr.status==1223||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpNotModified:function(xhr,url){try{var xhrRes=xhr.getResponseHeader("Last-Modified");return xhr.status==304||xhrRes==jQuery.lastModified[url]||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpData:function(xhr,type,filter){var ct=xhr.getResponseHeader("content-type"),xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0,data=xml?xhr.responseXML:xhr.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(filter)data=filter(data,type);if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else 30 | for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else 31 | s.push(encodeURIComponent(j)+"="+encodeURIComponent(jQuery.isFunction(a[j])?a[j]():a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle.apply(this,arguments):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall),p,hidden=jQuery(this).is(":hidden"),self=this;for(p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return opt.complete.call(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else 32 | e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.call(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(elem){type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",jQuery.makeArray(array));}return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].call(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:jQuery.fx.speeds[opt.duration])||jQuery.fx.speeds.def;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.call(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.call(this.elem,this.now,this);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=now();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;ithis.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done)this.options.complete.call(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.extend(jQuery.fx,{speeds:{slow:600,fast:200,def:400},step:{scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}}});jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),css=jQuery.curCSS,fixed=css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||css(offsetChild,"position")=="absolute"))||(mozilla&&css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l,10)||0;top+=parseInt(t,10)||0;}return results;};jQuery.fn.extend({position:function(){var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,'marginTop');offset.left-=num(this,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&jQuery.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return jQuery(offsetParent);}});jQuery.each(['Left','Top'],function(i,name){var method='scroll'+name;jQuery.fn[method]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(!i?val:jQuery(window).scrollLeft(),i?val:jQuery(window).scrollTop()):this[method]=val;}):this[0]==window||this[0]==document?self[i?'pageYOffset':'pageXOffset']||jQuery.boxModel&&document.documentElement[method]||document.body[method]:this[0][method];};});jQuery.each(["Height","Width"],function(i,name){var tl=i?"Left":"Top",br=i?"Right":"Bottom";jQuery.fn["inner"+name]=function(){return this[name.toLowerCase()]()+num(this,"padding"+tl)+num(this,"padding"+br);};jQuery.fn["outer"+name]=function(margin){return this["inner"+name]()+num(this,"border"+tl+"Width")+num(this,"border"+br+"Width")+(margin?num(this,"margin"+tl)+num(this,"margin"+br):0);};});})(); -------------------------------------------------------------------------------- /budget-sample-templates/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block page_title %}{% endblock %} 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 22 | 23 | 31 | 32 |
33 |
34 | {% block content %} 35 |

You really shouldn't see this.

36 | {% endblock %} 37 |
38 |
39 | 40 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/budgets/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Add A Budget{% endblock %} 4 | 5 | {% block content %} 6 |

Add A Budget

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/budgets/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Delete Budget{% endblock %} 4 | 5 | {% block content %} 6 |

Delete Budget

7 | 8 |

Are you sure you want to delete "{{ budget }}"?

9 | 10 |
11 | 12 | 13 | 18 | 19 |
14 | 15 | or 16 | 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/budgets/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Edit Budget{% endblock %} 4 | 5 | {% block content %} 6 |

Edit Budget

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | 22 |

23 | You can also 24 | delete 25 | this budget. 26 |

27 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/budgets/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Budget List{% endblock %} 4 | 5 | {% block content %} 6 |

Budget List

7 | 8 |

9 | Add A Budget 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% if budgets %} 23 | {% for budget in budgets %} 24 | 25 | 26 | 27 | 28 | 29 | {% endfor %} 30 | {% else %} 31 | 32 | 33 | 34 | {% endif %} 35 | 36 |
Budget  
{{ budget.name }}View/Add EstimatesEdit Budget
No budgets found.
37 | 38 | {% include 'budget/pagination.html' %} 39 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/budgets/show.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget-sample-templates/templates/budget/budgets/show.html -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/categories/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Add A Category{% endblock %} 4 | 5 | {% block content %} 6 |

Add A Category

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/categories/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Delete Category{% endblock %} 4 | 5 | {% block content %} 6 |

Delete Category

7 | 8 |

Are you sure you want to delete "{{ category }}"?

9 | 10 |
11 | 12 | 13 | 18 | 19 |
14 | 15 | or 16 | 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/categories/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Edit Category{% endblock %} 4 | 5 | {% block content %} 6 |

Edit Category

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | 22 |

23 | You can also 24 | delete 25 | this category. 26 |

27 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/categories/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Category List{% endblock %} 4 | 5 | {% block content %} 6 |

Category List

7 | 8 |

9 | Add A Category 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% if categories %} 20 | {% for category in categories %} 21 | 22 | 23 | 24 | {% endfor %} 25 | {% else %} 26 | 27 | 28 | 29 | {% endif %} 30 | 31 |
Name
{{ category.name }}
No categories found.
32 | 33 | {% include 'budget/pagination.html' %} 34 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load budget %} 3 | 4 | {% block page_title %}Dashboard{% endblock %} 5 | 6 | {% block content %} 7 |

Dashboard

8 | 9 |
10 |

Latest Expenses

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% if latest_expenses %} 22 | {% for expense in latest_expenses %} 23 | 24 | 27 | 28 | 29 | 30 | {% endfor %} 31 | {% else %} 32 | 33 | 34 | 35 | {% endif %} 36 | 37 |
NotesDateAmount
25 | {{ expense.notes }} 26 | {{ expense.date|date:"m/d/Y" }}${{ expense.amount|stringformat:".02f" }}
No recent expenses found.
38 | 39 |

40 | Add A New Transaction 41 |

42 |
43 | 44 |
45 |

Latest Incomes

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {% if latest_incomes %} 58 | {% for income in latest_incomes %} 59 | 60 | 63 | 64 | 65 | 66 | {% endfor %} 67 | {% else %} 68 | 69 | 70 | 71 | {% endif %} 72 | 73 |
NotesDateAmount
61 | {{ income.notes }} 62 | {{ income.date|date:"m/d/Y" }}${{ income.amount|stringformat:".02f" }}
No recent incomes found.
74 | 75 |

76 | Add A New Transaction 77 |

78 |
79 | 80 |
81 |

This Month's Usage

82 | 83 |
84 |
 
85 |
86 | 87 |

88 | ${{ amount_used|stringformat:".02f" }} out of ${{ estimated_amount|stringformat:".02f" }}. 89 |

90 |
91 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/estimates/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Add An Estimate{% endblock %} 4 | 5 | {% block content %} 6 |

Add An Estimate

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/estimates/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Delete Estimate{% endblock %} 4 | 5 | {% block content %} 6 |

Delete Estimate

7 | 8 |

Are you sure you want to delete "{{ estimate }}"?

9 | 10 |
11 | 12 | 13 | 18 | 19 |
14 | 15 | or 16 | 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/estimates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Edit Estimate{% endblock %} 4 | 5 | {% block content %} 6 |

Edit Estimate

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | 22 |

23 | You can also 24 | delete 25 | this estimate. 26 |

27 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/estimates/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Estimate List For {{ budget.name }}{% endblock %} 4 | 5 | {% block content %} 6 |

7 | Estimate List For {{ budget.name }} 8 |

9 | 10 |

11 | Add An Estimate 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% if estimates %} 23 | {% for estimate in estimates %} 24 | 25 | 28 | 31 | 32 | {% endfor %} 33 | {% else %} 34 | 35 | 36 | 37 | {% endif %} 38 | 39 |
CategoryAmount
26 | {{ estimate.category.name }} 27 | 29 | ${{ estimate.amount|stringformat:".02f" }} 30 |
No estimates found.
40 | 41 | {% include 'budget/pagination.html' %} 42 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/pagination.html: -------------------------------------------------------------------------------- 1 | {% if paginator.count %} 2 |
3 | 10 | 11 | 18 |
19 | {% endif %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/setup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Setup{% endblock %} 4 | 5 | {% block content %} 6 |

Setup

7 | 8 |

9 | Before you can start using this application, we need to put together 10 | a budget. Three quick steps and you'll be ready to start using 11 | everything. You can come back to this screen at any time to make 12 | changes or add new items. 13 |

14 | 15 |

Step 1. Create Categories

16 | 17 |

18 | Before we can put together the budget, we need categories to tie 19 | the incomes/expenses you enter to the line items in your budget. 20 | Nothing more than a descriptive name is required for each category, 21 | though you may want to err on the side of being a little broad 22 | when naming the category. 23 |

24 | 25 |

26 | View/Add Categories 27 |

28 | 29 |

Step 2. Create A Budget

30 | 31 |

32 | Once you have a couple categories in place, we can create a budget. 33 | Again, not a lot of information is needed here, just a name and when 34 | you'd like the budget to start being used. This application always 35 | tries to use the most recent budget for whatever date is chosen. 36 |

37 | 38 |

39 | View/Add Budgets 40 |

41 | 42 |

Step 3. Create Budget Estimates

43 | 44 |

45 | The last step is to create the line items that make up a budget. To do 46 | this, we'll tie together the budget the estimate applies to, the 47 | category it covers and how much you'd like to allocate to that type of 48 | expense per month. 49 |

50 | 51 |

52 | To view/add estimates, go into the Budget list and click the 53 | "View/Add Estimates" link on the budget you'd like to work with. 54 |

55 | 56 |

57 | View/Add Estimates Within A Budget 58 |

59 | 60 |

Step 4. All Set!

61 | 62 |

63 | You can now use the rest of the application. You'll probably want to 64 | start with the "Transactions" area to add some incomes/expenses, then 65 | go to the "Dashboard" for an overview or "Summaries" for more detail. 66 |

67 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/summaries/summary_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Summaries{% endblock %} 4 | 5 | {% block content %} 6 |

Summaries

7 | 8 | {% if dates %} 9 |
10 | {% for date in dates %} 11 | {% ifchanged date.year %}
{{ date.year }}
{% endifchanged %} 12 |
13 | {{ date|date:"F" }} 14 |
15 | {% endfor %} 16 |
17 | {% else %} 18 |

19 | It looks like there haven't been any transactions, so there's 20 | nothing to show here. 21 |

22 | {% endif %} 23 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/summaries/summary_month.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load budget %} 3 | 4 | {% block page_title %}Month Summary For {{ start_date|date:"F Y" }}{% endblock %} 5 | 6 | {% block content %} 7 |

Month Summary For {{ start_date|date:"F Y" }}

8 | 9 |

{{ budget.name }}

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% if estimates_and_transactions %} 21 | {% for eat_group in estimates_and_transactions %} 22 | 23 | 44 | 45 | 48 | 49 | {% endfor %} 50 | {% else %} 51 | 52 | 53 | 54 | {% endif %} 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 70 | 71 | 72 |
CategoryEstimated TotalActual Total
24 | {{ eat_group.estimate.category.name }} 25 | 26 | {% if eat_group.transactions %} 27 | [+] 28 | 29 | 30 | 31 | {% for trans in eat_group.transactions %} 32 | 33 | 34 | 35 | 38 | 39 | {% endfor %} 40 | 41 |
{{ trans.notes }}{{ trans.date|date:"m/d/Y" }} 36 | ${{ trans.amount|stringformat:".02f" }} 37 |
42 | {% endif %} 43 |
${{ eat_group.estimate.amount|stringformat:".02f" }} 46 | ${{ eat_group.actual_amount|stringformat:".02f" }} 47 |
No data to show.
   
64 | Total: 65 | ${{ budget.monthly_estimated_total|stringformat:".02f" }} 68 | ${{ actual_total|stringformat:".02f" }} 69 |
73 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/summaries/summary_year.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load budget %} 3 | 4 | {% block page_title %}Year Summary For {{ start_date|date:"Y" }}{% endblock %} 5 | 6 | {% block content %} 7 |

Year Summary For {{ start_date|date:"Y" }}

8 | 9 |

{{ budget.name }}

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% if estimates_and_transactions %} 21 | {% for eat_group in estimates_and_transactions %} 22 | 23 | 44 | 45 | 48 | 49 | {% endfor %} 50 | {% else %} 51 | 52 | 53 | 54 | {% endif %} 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 70 | 71 | 72 |
CategoryEstimated TotalActual Total
24 | {{ eat_group.estimate.category.name }} 25 | 26 | {% if eat_group.transactions %} 27 | [+] 28 | 29 | 30 | 31 | {% for trans in eat_group.transactions %} 32 | 33 | 34 | 35 | 38 | 39 | {% endfor %} 40 | 41 |
{{ trans.notes }}{{ trans.date|date:"m/d/Y" }} 36 | ${{ trans.amount|stringformat:".02f" }} 37 |
42 | {% endif %} 43 |
${{ eat_group.estimate.yearly_estimated_amount|stringformat:".02f" }} 46 | ${{ eat_group.actual_amount|stringformat:".02f" }} 47 |
No data to show.
   
64 | Total: 65 | ${{ budget.yearly_estimated_total|stringformat:".02f" }} 68 | ${{ actual_total|stringformat:".02f" }} 69 |
73 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/transactions/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Add A Transaction{% endblock %} 4 | 5 | {% block content %} 6 |

Add A Transaction

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/transactions/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Delete Transaction{% endblock %} 4 | 5 | {% block content %} 6 |

Delete Transaction

7 | 8 |

Are you sure you want to delete "{{ transaction }}"?

9 | 10 |
11 | 12 | 13 | 18 | 19 |
14 | 15 | or 16 | 17 |
20 |
21 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/transactions/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Edit Transaction{% endblock %} 4 | 5 | {% block content %} 6 |

Edit Transaction

7 | 8 |
9 | 10 | {{ form.as_table }} 11 | 12 | 13 | 18 | 19 |
  14 | 15 | or 16 | Cancel 17 |
20 |
21 | 22 |

23 | You can also 24 | delete 25 | this transaction. 26 |

27 | {% endblock %} -------------------------------------------------------------------------------- /budget-sample-templates/templates/budget/transactions/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block page_title %}Transaction List{% endblock %} 4 | 5 | {% block content %} 6 |

Transaction List

7 | 8 |

9 | Add A Transaction 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% if transactions %} 24 | {% for transaction in transactions %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endfor %} 32 | {% else %} 33 | 34 | 35 | 36 | {% endif %} 37 | 38 |
NotesTypeDateAmount
{{ transaction.notes }}{{ transaction.get_transaction_type_display }}{{ transaction.date|date:"m/d/Y" }}${{ transaction.amount|stringformat:".02f" }}
No transactions found.
39 | 40 | {% include 'budget/pagination.html' %} 41 | {% endblock %} -------------------------------------------------------------------------------- /budget/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Daniel Lindsley' 2 | __version__ = '1.0.3' 3 | -------------------------------------------------------------------------------- /budget/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from budget.models import Budget, BudgetEstimate 3 | 4 | 5 | class BudgetAdmin(admin.ModelAdmin): 6 | date_hierarchy = 'start_date' 7 | fieldsets = ( 8 | (None, { 9 | 'fields': ('name', 'slug', 'start_date'), 10 | }), 11 | ('Metadata', { 12 | 'classes': ('collapse',), 13 | 'fields': ('created', 'updated', 'is_deleted') 14 | }) 15 | ) 16 | list_display = ('name', 'start_date', 'is_deleted') 17 | list_filter = ('is_deleted',) 18 | prepopulated_fields = { 19 | 'slug': ('name',), 20 | } 21 | search_fields = ('name',) 22 | 23 | 24 | class BudgetEstimateAdmin(admin.ModelAdmin): 25 | fieldsets = ( 26 | (None, { 27 | 'fields': ('budget', 'category', 'amount'), 28 | }), 29 | ('Metadata', { 30 | 'classes': ('collapse',), 31 | 'fields': ('created', 'updated', 'is_deleted') 32 | }) 33 | ) 34 | list_display = ('category', 'budget', 'amount', 'is_deleted') 35 | list_filter = ('is_deleted',) 36 | 37 | 38 | admin.site.register(Budget, BudgetAdmin) 39 | admin.site.register(BudgetEstimate, BudgetEstimateAdmin) 40 | -------------------------------------------------------------------------------- /budget/categories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget/categories/__init__.py -------------------------------------------------------------------------------- /budget/categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from budget.categories.models import Category 3 | 4 | 5 | class CategoryAdmin(admin.ModelAdmin): 6 | fieldsets = ( 7 | (None, { 8 | 'fields': ('name', 'slug'), 9 | }), 10 | ('Metadata', { 11 | 'classes': ('collapse',), 12 | 'fields': ('created', 'updated', 'is_deleted') 13 | }) 14 | ) 15 | list_display = ('name', 'is_deleted') 16 | list_filter = ('is_deleted',) 17 | prepopulated_fields = { 18 | 'slug': ('name',), 19 | } 20 | search_fields = ('name',) 21 | 22 | 23 | admin.site.register(Category, CategoryAdmin) 24 | -------------------------------------------------------------------------------- /budget/categories/fixtures/categories_testdata.yaml: -------------------------------------------------------------------------------- 1 | - model: categories.category 2 | pk: 1 3 | fields: 4 | name: Misc 5 | slug: misc 6 | created: '2008-10-15 01:00:00' 7 | updated: '2008-10-15 01:00:00' -------------------------------------------------------------------------------- /budget/categories/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.template.defaultfilters import slugify 3 | from budget.categories.models import Category 4 | 5 | 6 | class CategoryForm(forms.ModelForm): 7 | class Meta: 8 | model = Category 9 | fields = ('name',) 10 | 11 | def save(self): 12 | if not self.instance.slug: 13 | self.instance.slug = slugify(self.cleaned_data['name']) 14 | super(CategoryForm, self).save() 15 | -------------------------------------------------------------------------------- /budget/categories/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | from django.db import models 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class StandardMetadata(models.Model): 8 | """ 9 | A basic (abstract) model for metadata. 10 | """ 11 | created = models.DateTimeField(_('Created'), default=datetime.datetime.now) 12 | updated = models.DateTimeField(_('Updated'), default=datetime.datetime.now) 13 | is_deleted = models.BooleanField(_('Is deleted'), default=False, db_index=True) 14 | 15 | class Meta: 16 | abstract = True 17 | 18 | def save(self, *args, **kwargs): 19 | self.updated = datetime.datetime.now() 20 | super(StandardMetadata, self).save(*args, **kwargs) 21 | 22 | def delete(self): 23 | self.is_deleted = True 24 | self.save() 25 | 26 | 27 | class ActiveManager(models.Manager): 28 | def get_query_set(self): 29 | return super(ActiveManager, self).get_query_set().filter(is_deleted=False) 30 | 31 | 32 | class Category(StandardMetadata): 33 | """ 34 | Categories are the means to loosely tie together the transactions and 35 | estimates. 36 | 37 | They are used to aggregate transactions together and compare them to the 38 | appropriate budget estimate. For the reasoning behind this, the docstring 39 | on the Transaction object explains this. 40 | """ 41 | name = models.CharField(_('Name'), max_length=128) 42 | slug = models.SlugField(_('Slug'), unique=True) 43 | 44 | objects = models.Manager() 45 | active = ActiveManager() 46 | 47 | class Meta: 48 | verbose_name = _('Category') 49 | verbose_name_plural = _('Categories') 50 | 51 | def __unicode__(self): 52 | return self.name 53 | 54 | -------------------------------------------------------------------------------- /budget/categories/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> from django.test import Client 3 | >>> c = Client() 4 | 5 | >>> r = c.get('/budget/category/') 6 | >>> r.status_code # /budget/category/ 7 | 200 8 | >>> r.context[-1]['categories'] 9 | [] 10 | 11 | >>> r = c.get('/budget/category/add/') 12 | >>> r.status_code # /budget/category/add/ 13 | 200 14 | >>> type(r.context[-1]['form']) 15 | 16 | 17 | >>> r = c.post('/budget/category/add/', {'name': 'Mortgage'}) 18 | >>> r.status_code # /budget/category/add/ 19 | 302 20 | >>> r['Location'] 21 | 'http://testserver/budget/category/' 22 | 23 | >>> r = c.get('/budget/category/') 24 | >>> r.status_code # /budget/category/ 25 | 200 26 | >>> r.context[-1]['categories'] 27 | [] 28 | 29 | >>> r = c.get('/budget/category/edit/mortgage/') 30 | >>> r.status_code # /budget/category/edit/mortgage/ 31 | 200 32 | >>> type(r.context[-1]['form']) 33 | 34 | >>> r.context[-1]['category'] 35 | 36 | 37 | >>> r = c.post('/budget/category/edit/mortgage/', {'name': 'First Mortgage'}) 38 | >>> r.status_code # /budget/category/edit/mortgage/ 39 | 302 40 | >>> r['Location'] 41 | 'http://testserver/budget/category/' 42 | 43 | >>> r = c.get('/budget/category/') 44 | >>> r.status_code # /budget/category/ 45 | 200 46 | >>> r.context[-1]['categories'] 47 | [] 48 | 49 | >>> r = c.get('/budget/category/delete/mortgage/') 50 | >>> r.status_code # /budget/category/delete/mortgage/ 51 | 200 52 | >>> r.context[-1]['category'] 53 | 54 | 55 | >>> r = c.post('/budget/category/delete/mortgage/', {'confirmed': 'Yes'}) 56 | >>> r.status_code # /budget/category/delete/mortgage/ 57 | 302 58 | >>> r['Location'] 59 | 'http://testserver/budget/category/' 60 | 61 | >>> r = c.get('/budget/category/') 62 | >>> r.status_code # /budget/category/ 63 | 200 64 | >>> r.context[-1]['categories'] 65 | [] 66 | """ 67 | -------------------------------------------------------------------------------- /budget/categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('budget.categories.views', 4 | url(r'^$', 'category_list', name='budget_category_list'), 5 | url(r'^add/$', 'category_add', name='budget_category_add'), 6 | url(r'^edit/(?P[\w_-]+)/$', 'category_edit', name='budget_category_edit'), 7 | url(r'^delete/(?P[\w_-]+)/$', 'category_delete', name='budget_category_delete'), 8 | ) 9 | -------------------------------------------------------------------------------- /budget/categories/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.urlresolvers import reverse 3 | from django.core.paginator import Paginator, InvalidPage 4 | from django.http import HttpResponseRedirect 5 | from django.shortcuts import render_to_response, get_object_or_404 6 | from django.template import RequestContext 7 | from budget.categories.models import Category 8 | from budget.categories.forms import CategoryForm 9 | 10 | 11 | def category_list(request, model_class=Category, template_name='budget/categories/list.html'): 12 | """ 13 | A list of category objects. 14 | 15 | Templates: ``budget/categories/list.html`` 16 | Context: 17 | categories 18 | paginated list of category objects 19 | paginator 20 | A Django Paginator instance 21 | page 22 | current page of category objects 23 | """ 24 | categories_list = model_class.active.all() 25 | try: 26 | paginator = Paginator(categories_list, getattr(settings, 'BUDGET_LIST_PER_PAGE', 50)) 27 | page = paginator.page(request.GET.get('page', 1)) 28 | categories = page.object_list 29 | except InvalidPage: 30 | raise Http404('Invalid page requested.') 31 | return render_to_response(template_name, { 32 | 'categories': categories, 33 | 'paginator': paginator, 34 | 'page': page, 35 | }, context_instance=RequestContext(request)) 36 | 37 | 38 | def category_add(request, form_class=CategoryForm, template_name='budget/categories/add.html'): 39 | """ 40 | Create a new category object. 41 | 42 | Templates: ``budget/categories/add.html`` 43 | Context: 44 | form 45 | a category form 46 | """ 47 | if request.POST: 48 | form = form_class(request.POST) 49 | 50 | if form.is_valid(): 51 | category = form.save() 52 | return HttpResponseRedirect(reverse('budget_category_list')) 53 | else: 54 | form = form_class() 55 | return render_to_response(template_name, { 56 | 'form': form, 57 | }, context_instance=RequestContext(request)) 58 | 59 | 60 | def category_edit(request, slug, model_class=Category, form_class=CategoryForm, template_name='budget/categories/edit.html'): 61 | """ 62 | Edit a category object. 63 | 64 | Templates: ``budget/categories/edit.html`` 65 | Context: 66 | category 67 | the existing category object 68 | form 69 | a category form 70 | """ 71 | category = get_object_or_404(model_class.active.all(), slug=slug) 72 | if request.POST: 73 | form = form_class(request.POST, instance=category) 74 | 75 | if form.is_valid(): 76 | category = form.save() 77 | return HttpResponseRedirect(reverse('budget_category_list')) 78 | else: 79 | form = form_class(instance=category) 80 | return render_to_response(template_name, { 81 | 'category': category, 82 | 'form': form, 83 | }, context_instance=RequestContext(request)) 84 | 85 | 86 | def category_delete(request, slug, model_class=Category, template_name='budget/categories/delete.html'): 87 | """ 88 | Delete a category object. 89 | 90 | Templates: ``budget/categories/delete.html`` 91 | Context: 92 | category 93 | the existing category object 94 | """ 95 | category = get_object_or_404(model_class.active.all(), slug=slug) 96 | if request.POST: 97 | if request.POST.get('confirmed') and request.POST['confirmed'] == 'Yes': 98 | category.delete() 99 | return HttpResponseRedirect(reverse('budget_category_list')) 100 | return render_to_response(template_name, { 101 | 'category': category, 102 | }, context_instance=RequestContext(request)) 103 | -------------------------------------------------------------------------------- /budget/forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django import forms 3 | from django.template.defaultfilters import slugify 4 | from budget.models import Budget, BudgetEstimate 5 | 6 | 7 | class BudgetForm(forms.ModelForm): 8 | start_date = forms.DateTimeField(initial=datetime.datetime.now, required=False, widget=forms.SplitDateTimeWidget) 9 | 10 | class Meta: 11 | model = Budget 12 | fields = ('name', 'start_date') 13 | 14 | def save(self): 15 | if not self.instance.slug: 16 | self.instance.slug = slugify(self.cleaned_data['name']) 17 | super(BudgetForm, self).save() 18 | 19 | 20 | class BudgetEstimateForm(forms.ModelForm): 21 | class Meta: 22 | model = BudgetEstimate 23 | fields = ('category', 'amount') 24 | 25 | def save(self, budget): 26 | self.instance.budget = budget 27 | super(BudgetEstimateForm, self).save() 28 | -------------------------------------------------------------------------------- /budget/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /budget/locale/es/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2012-11-03 19:47-0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 20 | 21 | #: models.py:21 categories/models.py:41 22 | msgid "Name" 23 | msgstr "Nombre" 24 | 25 | #: models.py:22 categories/models.py:42 26 | msgid "Slug" 27 | msgstr "Slug" 28 | 29 | #: models.py:23 30 | msgid "Start Date" 31 | msgstr "Fecha de Inicio" 32 | 33 | #: models.py:66 34 | msgid "Budget" 35 | msgstr "Presupuesto" 36 | 37 | #: models.py:67 38 | msgid "Budgets" 39 | msgstr "Presupuestos" 40 | 41 | #: models.py:78 transactions/models.py:43 42 | msgid "Amount" 43 | msgstr "Monto" 44 | 45 | #: models.py:101 46 | msgid "Budget estimate" 47 | msgstr "Presupuesto estimado" 48 | 49 | #: models.py:102 50 | msgid "Budget estimates" 51 | msgstr "Estimaciones presupuestarias" 52 | 53 | #: categories/models.py:11 54 | msgid "Created" 55 | msgstr "Creado" 56 | 57 | #: categories/models.py:12 58 | msgid "Updated" 59 | msgstr "Actualizado" 60 | 61 | #: categories/models.py:13 62 | msgid "Is deleted" 63 | msgstr "Esta borrado" 64 | 65 | #: categories/models.py:48 66 | msgid "Category" 67 | msgstr "Categoría" 68 | 69 | #: categories/models.py:49 70 | msgid "Categories" 71 | msgstr "Categorías" 72 | 73 | #: transactions/models.py:11 74 | msgid "Expense" 75 | msgstr "Gasto" 76 | 77 | #: transactions/models.py:12 78 | msgid "Income" 79 | msgstr "Ingreso" 80 | 81 | #: transactions/models.py:40 82 | msgid "Transaction type" 83 | msgstr "Tipo de transacción" 84 | 85 | #: transactions/models.py:41 86 | msgid "Notes" 87 | msgstr "Notas" 88 | 89 | #: transactions/models.py:44 90 | msgid "Date" 91 | msgstr "Fecha" 92 | 93 | #: transactions/models.py:55 94 | msgid "Transaction" 95 | msgstr "Transacción" 96 | 97 | #: transactions/models.py:56 98 | msgid "Transactions" 99 | msgstr "Transacciones" 100 | -------------------------------------------------------------------------------- /budget/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | from django.db import models 4 | from budget.categories.models import Category, StandardMetadata, ActiveManager 5 | from budget.transactions.models import Transaction 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | class BudgetManager(ActiveManager): 10 | def most_current_for_date(self, date): 11 | return super(BudgetManager, self).get_query_set().filter(start_date__lte=date).latest('start_date') 12 | 13 | 14 | class Budget(StandardMetadata): 15 | """ 16 | An object representing a budget. 17 | 18 | Only estimates are tied to a budget object, which allows different budgets 19 | to be applied to the same set of transactions for comparision. 20 | """ 21 | name = models.CharField(_('Name'), max_length=255) 22 | slug = models.SlugField(_('Slug'), unique=True) 23 | start_date = models.DateTimeField(_('Start Date'), 24 | default=datetime.datetime.now, db_index=True) 25 | 26 | objects = models.Manager() 27 | active = BudgetManager() 28 | 29 | def __unicode__(self): 30 | return self.name 31 | 32 | def monthly_estimated_total(self): 33 | total = Decimal('0.0') 34 | for estimate in self.estimates.exclude(is_deleted=True): 35 | total += estimate.amount 36 | return total 37 | 38 | def yearly_estimated_total(self): 39 | return self.monthly_estimated_total() * 12 40 | 41 | def estimates_and_transactions(self, start_date, end_date): 42 | estimates_and_transactions = [] 43 | actual_total = Decimal('0.0') 44 | 45 | for estimate in self.estimates.exclude(is_deleted=True): 46 | actual_amount = estimate.actual_amount(start_date, end_date) 47 | actual_total += actual_amount 48 | estimates_and_transactions.append({ 49 | 'estimate': estimate, 50 | 'transactions': estimate.actual_transactions(start_date, end_date), 51 | 'actual_amount': actual_amount, 52 | }) 53 | 54 | return (estimates_and_transactions, actual_total) 55 | 56 | def actual_total(self, start_date, end_date): 57 | actual_total = Decimal('0.0') 58 | 59 | for estimate in self.estimates.exclude(is_deleted=True): 60 | actual_amount = estimate.actual_amount(start_date, end_date) 61 | actual_total += actual_amount 62 | 63 | return actual_total 64 | 65 | class Meta: 66 | verbose_name = _('Budget') 67 | verbose_name_plural = _('Budgets') 68 | 69 | class BudgetEstimate(StandardMetadata): 70 | """ 71 | The individual line items that make up a budget. 72 | 73 | Some examples include possible items like "Mortgage", "Rent", "Food", "Misc" 74 | and "Car Payment". 75 | """ 76 | budget = models.ForeignKey(Budget, related_name='estimates', verbose_name=_('Budget')) 77 | category = models.ForeignKey(Category, related_name='estimates', verbose_name=_('Category')) 78 | amount = models.DecimalField(_('Amount'), max_digits=11, decimal_places=2) 79 | 80 | objects = models.Manager() 81 | active = ActiveManager() 82 | 83 | def __unicode__(self): 84 | return u"%s - %s" % (self.category.name, self.amount) 85 | 86 | def yearly_estimated_amount(self): 87 | return self.amount * 12 88 | 89 | def actual_transactions(self, start_date, end_date): 90 | # Estimates should only report on expenses to prevent incomes from 91 | # (incorrectly) artificially inflating totals. 92 | return Transaction.expenses.filter(category=self.category, date__range=(start_date, end_date)).order_by('date') 93 | 94 | def actual_amount(self, start_date, end_date): 95 | total = Decimal('0.0') 96 | for transaction in self.actual_transactions(start_date, end_date): 97 | total += transaction.amount 98 | return total 99 | 100 | class Meta: 101 | verbose_name = _('Budget estimate') 102 | verbose_name_plural = _('Budget estimates') 103 | -------------------------------------------------------------------------------- /budget/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget/templatetags/__init__.py -------------------------------------------------------------------------------- /budget/templatetags/budget.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from django import template 3 | from django.conf import settings 4 | 5 | 6 | register = template.Library() 7 | 8 | # To override, copy to your settings file. Make sure to keep the tuples in 9 | # descending order by percentage. 10 | BUDGET_DEFAULT_COLORS = ( 11 | # (percentage, CSS color class) 12 | (1.001, 'red'), 13 | (0.75, 'yellow'), 14 | (0.0, 'green'), 15 | ) 16 | 17 | 18 | class ColorizeAmountNode(template.Node): 19 | def __init__(self, estimated_amount, actual_amount): 20 | self.estimated_amount = template.Variable(estimated_amount) 21 | self.actual_amount = template.Variable(actual_amount) 22 | 23 | def render(self, context): 24 | if hasattr(settings, 'BUDGET_DEFAULT_COLORS'): 25 | colors = settings.BUDGET_DEFAULT_COLORS 26 | else: 27 | colors = BUDGET_DEFAULT_COLORS 28 | 29 | try: 30 | estimate = self.estimated_amount.resolve(context) 31 | actual = self.actual_amount.resolve(context) 32 | estimate = make_decimal(estimate) 33 | 34 | if estimate == 0: 35 | return '' 36 | 37 | actual = make_decimal(actual) 38 | percentage = actual / estimate 39 | 40 | for color in colors: 41 | color_percentage = make_decimal(color[0]) 42 | 43 | if percentage >= color_percentage: 44 | return color[1] 45 | except template.VariableDoesNotExist: 46 | return '' 47 | 48 | 49 | def make_decimal(amount): 50 | """ 51 | If it's not a Decimal, it should be... 52 | """ 53 | if not isinstance(amount, Decimal): 54 | amount = Decimal(str(amount)) 55 | 56 | return amount 57 | 58 | 59 | def colorize_amount(parser, token): 60 | """ 61 | Compares an estimate with an actual amount and returns an appropriate 62 | color as a visual indicator. 63 | 64 | Example: 65 | 66 | {% colorize_amount estimated_amount actual_amount %} 67 | """ 68 | try: 69 | tag_name, estimated_amount, actual_amount = token.split_contents() 70 | except ValueError: 71 | raise template.TemplateSyntaxError("%r tag requires exactly two arguments" % token.contents.split()[0]) 72 | return ColorizeAmountNode(estimated_amount, actual_amount) 73 | 74 | 75 | register.tag('colorize_amount', colorize_amount) 76 | -------------------------------------------------------------------------------- /budget/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> from django.test import Client 3 | >>> c = Client() 4 | 5 | 6 | # Budgets 7 | 8 | >>> r = c.get('/budget/budget/') 9 | >>> r.status_code # /budget/budget/ 10 | 200 11 | >>> r.context[-1]['budgets'] 12 | [] 13 | 14 | >>> r = c.get('/budget/budget/add/') 15 | >>> r.status_code # /budget/budget/add/ 16 | 200 17 | >>> type(r.context[-1]['form']) 18 | 19 | 20 | >>> r = c.post('/budget/budget/add/', {'name': 'Our Budget', 'start_date_0': '2008-10-14', 'start_date_1': '08:00:00'}) 21 | >>> r.status_code # /budget/budget/add/ 22 | 302 23 | >>> r['Location'] 24 | 'http://testserver/budget/budget/' 25 | 26 | >>> r = c.get('/budget/budget/') 27 | >>> r.status_code # /budget/budget/ 28 | 200 29 | >>> r.context[-1]['budgets'] 30 | [] 31 | 32 | >>> r = c.get('/budget/budget/edit/our-budget/') 33 | >>> r.status_code # /budget/budget/edit/our-budget/ 34 | 200 35 | >>> type(r.context[-1]['form']) 36 | 37 | >>> r.context[-1]['budget'] 38 | 39 | 40 | >>> r = c.post('/budget/budget/edit/our-budget/', {'name': 'Our Family Budget', 'start_date_0': '2008-10-14', 'start_date_1': '08:00:00'}) 41 | >>> r.status_code # /budget/budget/edit/our-budget/ 42 | 302 43 | >>> r['Location'] 44 | 'http://testserver/budget/budget/' 45 | 46 | >>> r = c.get('/budget/budget/') 47 | >>> r.status_code # /budget/budget/ 48 | 200 49 | >>> r.context[-1]['budgets'] 50 | [] 51 | 52 | >>> r = c.get('/budget/budget/delete/our-budget/') 53 | >>> r.status_code # /budget/budget/delete/our-budget/ 54 | 200 55 | >>> r.context[-1]['budget'] 56 | 57 | 58 | >>> r = c.post('/budget/budget/delete/our-budget/', {'confirmed': 'Yes'}) 59 | >>> r.status_code # /budget/budget/delete/our-budget/ 60 | 302 61 | >>> r['Location'] 62 | 'http://testserver/budget/budget/' 63 | 64 | >>> r = c.get('/budget/budget/') 65 | >>> r.status_code # /budget/budget/ 66 | 200 67 | >>> r.context[-1]['budgets'] 68 | [] 69 | 70 | 71 | # Budget Estimates 72 | 73 | # Setup 74 | >>> from budget.models import Budget 75 | >>> budget = Budget.objects.create(name='Test Budget', slug='test-budget', start_date='2008-10-14') 76 | 77 | >>> from django.core.management import call_command 78 | >>> call_command('loaddata', 'categories_testdata.yaml') #doctest: +ELLIPSIS 79 | Installing yaml fixture 'categories_testdata' from ... 80 | Installed 1 object(s) from 1 fixture(s) 81 | 82 | >>> from budget.categories.models import Category 83 | >>> cat = Category.objects.get(slug='misc') 84 | 85 | >>> r = c.get('/budget/budget/test-budget/estimate/') 86 | >>> r.status_code # /budget/budget/test-budget/estimate/ 87 | 200 88 | >>> r.context[-1]['estimates'] 89 | [] 90 | 91 | >>> r = c.get('/budget/budget/test-budget/estimate/add/') 92 | >>> r.status_code # /budget/budget/test-budget/estimate/add/ 93 | 200 94 | >>> type(r.context[-1]['form']) 95 | 96 | 97 | >>> r = c.post('/budget/budget/test-budget/estimate/add/', {'budget': budget.id, 'category': cat.id, 'amount': '200.00'}) 98 | >>> r.status_code # /budget/budget/test-budget/estimate/add/ 99 | 302 100 | >>> r['Location'] 101 | 'http://testserver/budget/budget/test-budget/estimate/' 102 | 103 | >>> r = c.get('/budget/budget/test-budget/estimate/') 104 | >>> r.status_code # /budget/budget/test-budget/estimate/ 105 | 200 106 | >>> r.context[-1]['estimates'] 107 | [] 108 | 109 | >>> r = c.get('/budget/budget/test-budget/estimate/edit/1/') 110 | >>> r.status_code # /budget/budget/test-budget/estimate/edit/1/ 111 | 200 112 | >>> type(r.context[-1]['form']) 113 | 114 | >>> r.context[-1]['estimate'] 115 | 116 | 117 | >>> r = c.post('/budget/budget/test-budget/estimate/edit/1/', {'budget': budget.id, 'category': cat.id, 'amount': '250.00'}) 118 | >>> r.status_code # /budget/budget/test-budget/estimate/edit/1/ 119 | 302 120 | >>> r['Location'] 121 | 'http://testserver/budget/budget/test-budget/estimate/' 122 | 123 | >>> r = c.get('/budget/budget/test-budget/estimate/') 124 | >>> r.status_code # /budget/budget/test-budget/estimate/ 125 | 200 126 | >>> r.context[-1]['estimates'] 127 | [] 128 | 129 | >>> r = c.get('/budget/budget/test-budget/estimate/delete/1/') 130 | >>> r.status_code # /budget/budget/test-budget/estimate/delete/1/ 131 | 200 132 | >>> r.context[-1]['estimate'] 133 | 134 | 135 | >>> r = c.post('/budget/budget/test-budget/estimate/delete/1/', {'confirmed': 'Yes'}) 136 | >>> r.status_code # /budget/budget/test-budget/estimate/delete/1/ 137 | 302 138 | >>> r['Location'] 139 | 'http://testserver/budget/budget/test-budget/estimate/' 140 | 141 | >>> r = c.get('/budget/budget/test-budget/estimate/') 142 | >>> r.status_code # /budget/budget/test-budget/estimate/ 143 | 200 144 | >>> r.context[-1]['estimates'] 145 | [] 146 | 147 | 148 | # Summaries 149 | 150 | >>> r = c.get('/budget/summary/2008/') 151 | >>> r.status_code # /budget/summary/2008/ 152 | 200 153 | >>> r.context[-1]['budget'] 154 | 155 | 156 | >>> r = c.get('/budget/summary/2008/10/') 157 | >>> r.status_code # /budget/summary/2008/10/ 158 | 200 159 | >>> r.context[-1]['budget'] 160 | 161 | 162 | 163 | # Dashboard 164 | 165 | >>> r = c.get('/budget/') 166 | >>> r.status_code # /budget/ 167 | 200 168 | >>> r.context[-1]['budget'] 169 | 170 | >>> r.context[-1]['latest_expenses'] 171 | [] 172 | >>> r.context[-1]['latest_incomes'] 173 | [] 174 | >>> r.context[-1]['estimated_amount'] 175 | Decimal("250.0") 176 | >>> r.context[-1]['amount_used'] 177 | Decimal("0.0") 178 | >>> r.context[-1]['progress_bar_percent'] 179 | 0 180 | """ 181 | -------------------------------------------------------------------------------- /budget/transactions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/django-budget/2f0547ae2d7786b5b832083fa7ecd5f6db3d74c1/budget/transactions/__init__.py -------------------------------------------------------------------------------- /budget/transactions/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from budget.transactions.models import Transaction 3 | 4 | 5 | class TransactionAdmin(admin.ModelAdmin): 6 | date_hierarchy = 'date' 7 | fieldsets = ( 8 | (None, { 9 | 'fields': ('transaction_type', 'notes', 'category', 'amount', 'date'), 10 | }), 11 | ('Metadata', { 12 | 'classes': ('collapse',), 13 | 'fields': ('created', 'updated', 'is_deleted') 14 | }) 15 | ) 16 | list_display = ('notes', 'transaction_type', 'amount', 'date', 'is_deleted') 17 | list_filter = ('is_deleted',) 18 | search_fields = ('notes',) 19 | 20 | 21 | admin.site.register(Transaction, TransactionAdmin) 22 | -------------------------------------------------------------------------------- /budget/transactions/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from budget.transactions.models import Transaction 3 | 4 | 5 | class TransactionForm(forms.ModelForm): 6 | class Meta: 7 | model = Transaction 8 | fields = ('transaction_type', 'notes', 'category', 'amount', 'date') 9 | -------------------------------------------------------------------------------- /budget/transactions/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | 4 | from django.db import models 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from budget.categories.models import Category, StandardMetadata, ActiveManager 8 | 9 | 10 | TRANSACTION_TYPES = ( 11 | ('expense', _('Expense')), 12 | ('income', _('Income')), 13 | ) 14 | 15 | 16 | class TransactionManager(ActiveManager): 17 | def get_latest(self, limit=10): 18 | return self.get_query_set().order_by('-date', '-created')[0:limit] 19 | 20 | 21 | class TransactionExpenseManager(TransactionManager): 22 | def get_query_set(self): 23 | return super(TransactionExpenseManager, self).get_query_set().filter(transaction_type='expense') 24 | 25 | 26 | class TransactionIncomeManager(TransactionManager): 27 | def get_query_set(self): 28 | return super(TransactionIncomeManager, self).get_query_set().filter(transaction_type='income') 29 | 30 | 31 | class Transaction(StandardMetadata): 32 | """ 33 | Represents incomes/expenses for the party doing the budgeting. 34 | 35 | Transactions are not tied to individual budgets because this allows 36 | different budgets to applied (like a filter) to a set of transactions. 37 | It also allows for budgets to change through time without altering the 38 | actual incoming/outgoing funds. 39 | """ 40 | transaction_type = models.CharField(_('Transaction type'), max_length=32, choices=TRANSACTION_TYPES, default='expense', db_index=True) 41 | notes = models.CharField(_('Notes'), max_length=255, blank=True) 42 | category = models.ForeignKey(Category, verbose_name=_('Category')) 43 | amount = models.DecimalField(_('Amount'), max_digits=11, decimal_places=2) 44 | date = models.DateField(_('Date'), default=datetime.date.today, db_index=True) 45 | 46 | objects = models.Manager() 47 | active = ActiveManager() 48 | expenses = TransactionExpenseManager() 49 | incomes = TransactionIncomeManager() 50 | 51 | def __unicode__(self): 52 | return u"%s (%s) - %s" % (self.notes, self.get_transaction_type_display(), self.amount) 53 | 54 | class Meta: 55 | verbose_name = _('Transaction') 56 | verbose_name_plural = _('Transactions') 57 | -------------------------------------------------------------------------------- /budget/transactions/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | >>> from django.test import Client 3 | >>> c = Client() 4 | 5 | >>> from django.core.management import call_command 6 | >>> call_command('loaddata', 'categories_testdata.yaml') #doctest: +ELLIPSIS 7 | Installing yaml fixture 'categories_testdata' from ... 8 | Installed 1 object(s) from 1 fixture(s) 9 | 10 | >>> from budget.categories.models import Category 11 | >>> cat = Category.objects.get(slug='misc') 12 | 13 | >>> r = c.get('/budget/transaction/') 14 | >>> r.status_code # /budget/transaction/ 15 | 200 16 | >>> r.context[-1]['transactions'] 17 | [] 18 | 19 | >>> r = c.get('/budget/transaction/add/') 20 | >>> r.status_code # /budget/transaction/add/ 21 | 200 22 | >>> type(r.context[-1]['form']) 23 | 24 | 25 | >>> r = c.post('/budget/transaction/add/', {'transaction_type': 'income', 'category': cat.id, 'notes': 'Paycheck', 'amount': '300.00', 'date': '2008-10-14'}) 26 | >>> r.status_code # /budget/transaction/add/ 27 | 302 28 | >>> r['Location'] 29 | 'http://testserver/budget/transaction/' 30 | 31 | >>> r = c.get('/budget/transaction/') 32 | >>> r.status_code # /budget/transaction/ 33 | 200 34 | >>> r.context[-1]['transactions'] 35 | [] 36 | 37 | >>> r = c.get('/budget/transaction/edit/1/') 38 | >>> r.status_code # /budget/transaction/edit/1/ 39 | 200 40 | >>> type(r.context[-1]['form']) 41 | 42 | >>> r.context[-1]['transaction'] 43 | 44 | 45 | >>> r = c.post('/budget/transaction/edit/1/', {'transaction_type': 'income', 'category': cat.id, 'notes': 'My Paycheck', 'amount': '300.00', 'date': '2008-10-14'}) 46 | >>> r.status_code # /budget/transaction/edit/1/ 47 | 302 48 | >>> r['Location'] 49 | 'http://testserver/budget/transaction/' 50 | 51 | >>> r = c.get('/budget/transaction/') 52 | >>> r.status_code # /budget/transaction/ 53 | 200 54 | >>> r.context[-1]['transactions'] 55 | [] 56 | 57 | >>> r = c.get('/budget/transaction/delete/1/') 58 | >>> r.status_code # /budget/transaction/delete/1/ 59 | 200 60 | >>> r.context[-1]['transaction'] 61 | 62 | 63 | >>> r = c.post('/budget/transaction/delete/1/', {'confirmed': 'Yes'}) 64 | >>> r.status_code # /budget/transaction/delete/1/ 65 | 302 66 | >>> r['Location'] 67 | 'http://testserver/budget/transaction/' 68 | 69 | >>> r = c.get('/budget/transaction/') 70 | >>> r.status_code # /budget/transaction/ 71 | 200 72 | >>> r.context[-1]['transactions'] 73 | [] 74 | """ 75 | -------------------------------------------------------------------------------- /budget/transactions/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | urlpatterns = patterns('budget.transactions.views', 4 | url(r'^$', 'transaction_list', name='budget_transaction_list'), 5 | url(r'^add/$', 'transaction_add', name='budget_transaction_add'), 6 | url(r'^edit/(?P\d+)/$', 'transaction_edit', name='budget_transaction_edit'), 7 | url(r'^delete/(?P\d+)/$', 'transaction_delete', name='budget_transaction_delete'), 8 | ) 9 | -------------------------------------------------------------------------------- /budget/transactions/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.urlresolvers import reverse 3 | from django.core.paginator import Paginator, InvalidPage 4 | from django.http import Http404, HttpResponseRedirect 5 | from django.shortcuts import render_to_response, get_object_or_404 6 | from django.template import RequestContext 7 | from budget.transactions.models import Transaction 8 | from budget.transactions.forms import TransactionForm 9 | 10 | 11 | def transaction_list(request, model_class=Transaction, template_name='budget/transactions/list.html'): 12 | """ 13 | A list of transaction objects. 14 | 15 | Templates: ``budget/transactions/list.html`` 16 | Context: 17 | transactions 18 | paginated list of transaction objects 19 | paginator 20 | A Django Paginator instance 21 | page 22 | current page of transaction objects 23 | """ 24 | transaction_list = model_class.active.order_by('-date', '-created') 25 | try: 26 | paginator = Paginator(transaction_list, getattr(settings, 'BUDGET_LIST_PER_PAGE', 50)) 27 | page = paginator.page(request.GET.get('page', 1)) 28 | transactions = page.object_list 29 | except InvalidPage: 30 | raise Http404('Invalid page requested.') 31 | return render_to_response(template_name, { 32 | 'transactions': transactions, 33 | 'paginator': paginator, 34 | 'page': page, 35 | }, context_instance=RequestContext(request)) 36 | 37 | 38 | def transaction_add(request, form_class=TransactionForm, template_name='budget/transactions/add.html'): 39 | """ 40 | Create a new transaction object. 41 | 42 | Templates: ``budget/transactions/add.html`` 43 | Context: 44 | form 45 | a transaction form 46 | """ 47 | if request.POST: 48 | form = form_class(request.POST) 49 | 50 | if form.is_valid(): 51 | transaction = form.save() 52 | return HttpResponseRedirect(reverse('budget_transaction_list')) 53 | else: 54 | form = form_class() 55 | return render_to_response(template_name, { 56 | 'form': form, 57 | }, context_instance=RequestContext(request)) 58 | 59 | 60 | def transaction_edit(request, transaction_id, model_class=Transaction, form_class=TransactionForm, template_name='budget/transactions/edit.html'): 61 | """ 62 | Edit a transaction object. 63 | 64 | Templates: ``budget/transactions/edit.html`` 65 | Context: 66 | transaction 67 | the existing transaction object 68 | form 69 | a transaction form 70 | """ 71 | transaction = get_object_or_404(model_class.active.all(), pk=transaction_id) 72 | if request.POST: 73 | form = form_class(request.POST, instance=transaction) 74 | 75 | if form.is_valid(): 76 | category = form.save() 77 | return HttpResponseRedirect(reverse('budget_transaction_list')) 78 | else: 79 | form = form_class(instance=transaction) 80 | return render_to_response(template_name, { 81 | 'transaction': transaction, 82 | 'form': form, 83 | }, context_instance=RequestContext(request)) 84 | 85 | 86 | def transaction_delete(request, transaction_id, model_class=Transaction, template_name='budget/transactions/delete.html'): 87 | """ 88 | Delete a transaction object. 89 | 90 | Templates: ``budget/transactions/delete.html`` 91 | Context: 92 | transaction 93 | the existing transaction object 94 | """ 95 | transaction = get_object_or_404(Transaction.active.all(), pk=transaction_id) 96 | if request.POST: 97 | if request.POST.get('confirmed'): 98 | transaction.delete() 99 | return HttpResponseRedirect(reverse('budget_transaction_list')) 100 | return render_to_response(template_name, { 101 | 'transaction': transaction, 102 | }, context_instance=RequestContext(request)) 103 | -------------------------------------------------------------------------------- /budget/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | """ 4 | Needed Urls: 5 | Dashboard 6 | Year Summary 7 | Month Summary 8 | CRUD Budget 9 | CRUD BudgetEstimates 10 | 11 | Eventually: 12 | Custom date range 13 | Week Summary 14 | Day Summary 15 | ...? 16 | """ 17 | 18 | urlpatterns = patterns('budget.views', 19 | url(r'^$', 'dashboard', name='budget_dashboard'), 20 | url(r'^setup/$', 'setup', name='budget_setup'), 21 | 22 | # Summaries 23 | url(r'^summary/$', 'summary_list', name='budget_summary_list'), 24 | url(r'^summary/(?P\d{4})/$', 'summary_year', name='budget_summary_year'), 25 | url(r'^summary/(?P\d{4})/(?P\d{1,2})/$', 'summary_month', name='budget_summary_month'), 26 | 27 | # Categories 28 | url(r'^category/', include('budget.categories.urls')), 29 | 30 | # Budget 31 | url(r'^budget/$', 'budget_list', name='budget_budget_list'), 32 | url(r'^budget/add/$', 'budget_add', name='budget_budget_add'), 33 | url(r'^budget/edit/(?P[\w-]+)/$', 'budget_edit', name='budget_budget_edit'), 34 | url(r'^budget/delete/(?P[\w-]+)/$', 'budget_delete', name='budget_budget_delete'), 35 | 36 | # BudgetEstimates 37 | url(r'^budget/(?P[\w-]+)/estimate/$', 'estimate_list', name='budget_estimate_list'), 38 | url(r'^budget/(?P[\w-]+)/estimate/add/$', 'estimate_add', name='budget_estimate_add'), 39 | url(r'^budget/(?P[\w-]+)/estimate/edit/(?P\d+)/$', 'estimate_edit', name='budget_estimate_edit'), 40 | url(r'^budget/(?P[\w-]+)/estimate/delete/(?P\d+)/$', 'estimate_delete', name='budget_estimate_delete'), 41 | 42 | # Transaction 43 | url(r'^transaction/', include('budget.transactions.urls')), 44 | ) 45 | -------------------------------------------------------------------------------- /budget/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from decimal import Decimal 3 | from django.conf import settings 4 | from django.core.exceptions import ObjectDoesNotExist 5 | from django.core.paginator import Paginator, InvalidPage 6 | from django.core.urlresolvers import reverse 7 | from django.http import Http404, HttpResponseRedirect 8 | from django.shortcuts import render_to_response, get_object_or_404 9 | from django.template import RequestContext 10 | from budget.models import Budget, BudgetEstimate 11 | from budget.categories.models import Category 12 | from budget.transactions.models import Transaction 13 | from budget.forms import BudgetEstimateForm, BudgetForm 14 | 15 | 16 | def dashboard(request, budget_model_class=Budget, transaction_model_class=Transaction, template_name='budget/dashboard.html'): 17 | """ 18 | Provides a high-level rundown of recent activity and budget status. 19 | 20 | Template: ``budget/dashboard.html`` 21 | Context: 22 | budget 23 | the most current budget object 24 | latest_expenses 25 | the most recent expenses (by default, the last 10) 26 | latest_incomes 27 | the most recent incomes (by default, the last 10) 28 | estimated_amount 29 | the budget's estimated total for the month 30 | amount_used 31 | the actual amount spent for the month 32 | progress_bar_percent 33 | the percentage of the budget actually spent so far 34 | """ 35 | today = datetime.date.today() 36 | start_date = datetime.date(today.year, today.month, 1) 37 | end_year, end_month = today.year, today.month + 1 38 | 39 | if end_month > 12: 40 | end_year += 1 41 | end_month = 1 42 | 43 | end_date = datetime.date(end_year, end_month, 1) - datetime.timedelta(days=1) 44 | 45 | try: 46 | budget = budget_model_class.active.most_current_for_date(datetime.datetime.today()) 47 | except ObjectDoesNotExist: 48 | # Since there are no budgets at this point, pass them on to setup 49 | # as this view is meaningless without at least basic data in place. 50 | return HttpResponseRedirect(reverse('budget_setup')) 51 | 52 | latest_expenses = transaction_model_class.expenses.get_latest() 53 | latest_incomes = transaction_model_class.incomes.get_latest() 54 | 55 | estimated_amount = budget.monthly_estimated_total() 56 | amount_used = budget.actual_total(start_date, end_date) 57 | 58 | if estimated_amount == 0: 59 | progress_bar_percent = 100 60 | else: 61 | progress_bar_percent = int(amount_used / estimated_amount * 100) 62 | 63 | if progress_bar_percent >= 100: 64 | progress_bar_percent = 100 65 | 66 | return render_to_response(template_name, { 67 | 'budget': budget, 68 | 'latest_expenses': latest_expenses, 69 | 'latest_incomes': latest_incomes, 70 | 'estimated_amount': estimated_amount, 71 | 'amount_used': amount_used, 72 | 'progress_bar_percent': progress_bar_percent, 73 | }, context_instance=RequestContext(request)) 74 | 75 | 76 | def setup(request, template_name='budget/setup.html'): 77 | """ 78 | Displays a setup page which ties together the 79 | category/budget/budget estimate areas with explanatory text on how to use 80 | to set everything up properly. 81 | 82 | Templates: ``budget/setup.html`` 83 | """ 84 | return render_to_response(template_name, {}, context_instance=RequestContext(request)) 85 | 86 | 87 | def summary_list(request, transaction_model_class=Transaction, template_name='budget/summaries/summary_list.html'): 88 | """ 89 | Displays a list of all months that may have transactions for that month. 90 | 91 | Templates: ``budget/summaries/summary_list.html`` 92 | Context: 93 | dates 94 | a list of datetime objects representing all years/months that have transactions 95 | """ 96 | dates = transaction_model_class.active.all().dates('date', 'month') 97 | return render_to_response(template_name, { 98 | 'dates': dates, 99 | }, context_instance=RequestContext(request)) 100 | 101 | 102 | def summary_year(request, year, budget_model_class=Budget, template_name='budget/summaries/summary_year.html'): 103 | """ 104 | Displays a budget report for the year to date. 105 | 106 | Templates: ``budget/summaries/summary_year.html`` 107 | Context: 108 | budget 109 | the most current budget object for the year 110 | estimates_and_transactions 111 | a list of dictionaries containing each budget estimate, the corresponding transactions and total amount of the transactions 112 | actual_total 113 | the total amount of all transactions represented in the budget for the year 114 | start_date 115 | the first date for the year 116 | end_date 117 | the last date for the year 118 | """ 119 | start_date = datetime.date(int(year), 1, 1) 120 | end_date = datetime.date(int(year), 12, 31) 121 | budget = budget_model_class.active.most_current_for_date(end_date) 122 | estimates_and_transactions, actual_total = budget.estimates_and_transactions(start_date, end_date) 123 | return render_to_response(template_name, { 124 | 'budget': budget, 125 | 'estimates_and_transactions': estimates_and_transactions, 126 | 'actual_total': actual_total, 127 | 'start_date': start_date, 128 | 'end_date': end_date, 129 | }, context_instance=RequestContext(request)) 130 | 131 | 132 | def summary_month(request, year, month, budget_model_class=Budget, template_name='budget/summaries/summary_month.html'): 133 | """ 134 | Displays a budget report for the month to date. 135 | 136 | Templates: ``budget/summaries/summary_month.html`` 137 | Context: 138 | budget 139 | the most current budget object for the month 140 | estimates_and_transactions 141 | a list of dictionaries containing each budget estimate, the corresponding transactions and total amount of the transactions 142 | actual_total 143 | the total amount of all transactions represented in the budget for the month 144 | start_date 145 | the first date for the month 146 | end_date 147 | the last date for the month 148 | """ 149 | start_date = datetime.date(int(year), int(month), 1) 150 | end_year, end_month = int(year), int(month) + 1 151 | 152 | if end_month > 12: 153 | end_year += 1 154 | end_month = 1 155 | 156 | end_date = datetime.date(end_year, end_month, 1) - datetime.timedelta(days=1) 157 | budget = budget_model_class.active.most_current_for_date(end_date) 158 | estimates_and_transactions, actual_total = budget.estimates_and_transactions(start_date, end_date) 159 | return render_to_response(template_name, { 160 | 'budget': budget, 161 | 'estimates_and_transactions': estimates_and_transactions, 162 | 'actual_total': actual_total, 163 | 'start_date': start_date, 164 | 'end_date': end_date, 165 | }, context_instance=RequestContext(request)) 166 | 167 | 168 | def budget_list(request, model_class=Budget, template_name='budget/budgets/list.html'): 169 | """ 170 | A list of budget objects. 171 | 172 | Templates: ``budget/budgets/list.html`` 173 | Context: 174 | budgets 175 | paginated list of budget objects 176 | paginator 177 | A Django Paginator instance 178 | page 179 | current page of budget objects 180 | """ 181 | budgets_list = model_class.active.all() 182 | try: 183 | paginator = Paginator(budgets_list, getattr(settings, 'BUDGET_LIST_PER_PAGE', 50)) 184 | page = paginator.page(request.GET.get('page', 1)) 185 | budgets = page.object_list 186 | except InvalidPage: 187 | raise Http404('Invalid page requested.') 188 | return render_to_response(template_name, { 189 | 'budgets': budgets, 190 | 'paginator': paginator, 191 | 'page': page, 192 | }, context_instance=RequestContext(request)) 193 | 194 | 195 | def budget_add(request, form_class=BudgetForm, template_name='budget/budgets/add.html'): 196 | """ 197 | Create a new budget object. 198 | 199 | Templates: ``budget/budgets/add.html`` 200 | Context: 201 | form 202 | a budget form 203 | """ 204 | if request.POST: 205 | form = form_class(request.POST) 206 | 207 | if form.is_valid(): 208 | budget = form.save() 209 | return HttpResponseRedirect(reverse('budget_budget_list')) 210 | else: 211 | form = form_class() 212 | return render_to_response(template_name, { 213 | 'form': form, 214 | }, context_instance=RequestContext(request)) 215 | 216 | 217 | def budget_edit(request, slug, model_class=Budget, form_class=BudgetForm, template_name='budget/budgets/edit.html'): 218 | """ 219 | Edit a budget object. 220 | 221 | Templates: ``budget/budgets/edit.html`` 222 | Context: 223 | budget 224 | the existing budget object 225 | form 226 | a budget form 227 | """ 228 | budget = get_object_or_404(model_class.active.all(), slug=slug) 229 | if request.POST: 230 | form = form_class(request.POST, instance=budget) 231 | 232 | if form.is_valid(): 233 | category = form.save() 234 | return HttpResponseRedirect(reverse('budget_budget_list')) 235 | else: 236 | form = form_class(instance=budget) 237 | return render_to_response(template_name, { 238 | 'budget': budget, 239 | 'form': form, 240 | }, context_instance=RequestContext(request)) 241 | 242 | 243 | def budget_delete(request, slug, model_class=Budget, template_name='budget/budgets/delete.html'): 244 | """ 245 | Delete a budget object. 246 | 247 | Templates: ``budget/budgets/delete.html`` 248 | Context: 249 | budget 250 | the existing budget object 251 | """ 252 | budget = get_object_or_404(model_class.active.all(), slug=slug) 253 | if request.POST: 254 | if request.POST.get('confirmed'): 255 | budget.delete() 256 | return HttpResponseRedirect(reverse('budget_budget_list')) 257 | return render_to_response(template_name, { 258 | 'budget': budget, 259 | }, context_instance=RequestContext(request)) 260 | 261 | 262 | def estimate_list(request, budget_slug, budget_model_class=Budget, model_class=BudgetEstimate, template_name='budget/estimates/list.html'): 263 | """ 264 | A list of estimate objects. 265 | 266 | Templates: ``budget/estimates/list.html`` 267 | Context: 268 | budget 269 | the parent budget object for the estimates 270 | estimates 271 | paginated list of estimate objects 272 | paginator 273 | A Django Paginator instance 274 | page 275 | current page of estimate objects 276 | """ 277 | budget = get_object_or_404(budget_model_class.active.all(), slug=budget_slug) 278 | estimates_list = model_class.active.all() 279 | try: 280 | paginator = Paginator(estimates_list, getattr(settings, 'BUDGET_LIST_PER_PAGE', 50)) 281 | page = paginator.page(request.GET.get('page', 1)) 282 | estimates = page.object_list 283 | except InvalidPage: 284 | raise Http404('Invalid page requested.') 285 | return render_to_response(template_name, { 286 | 'budget': budget, 287 | 'estimates': estimates, 288 | 'paginator': paginator, 289 | 'page': page, 290 | }, context_instance=RequestContext(request)) 291 | 292 | 293 | def estimate_add(request, budget_slug, budget_model_class=Budget, form_class=BudgetEstimateForm, template_name='budget/estimates/add.html'): 294 | """ 295 | Create a new estimate object. 296 | 297 | Templates: ``budget/estimates/add.html`` 298 | Context: 299 | budget 300 | the parent budget object for the estimate 301 | form 302 | a estimate form 303 | """ 304 | budget = get_object_or_404(budget_model_class.active.all(), slug=budget_slug) 305 | if request.POST: 306 | form = form_class(request.POST) 307 | 308 | if form.is_valid(): 309 | estimate = form.save(budget=budget) 310 | return HttpResponseRedirect(reverse('budget_estimate_list', kwargs={'budget_slug': budget.slug})) 311 | else: 312 | form = form_class() 313 | return render_to_response(template_name, { 314 | 'budget': budget, 315 | 'form': form, 316 | }, context_instance=RequestContext(request)) 317 | 318 | 319 | def estimate_edit(request, budget_slug, estimate_id, budget_model_class=Budget, form_class=BudgetEstimateForm, template_name='budget/estimates/edit.html'): 320 | """ 321 | Edit a estimate object. 322 | 323 | Templates: ``budget/estimates/edit.html`` 324 | Context: 325 | budget 326 | the parent budget object for the estimate 327 | estimate 328 | the existing estimate object 329 | form 330 | a estimate form 331 | """ 332 | budget = get_object_or_404(budget_model_class.active.all(), slug=budget_slug) 333 | try: 334 | estimate = budget.estimates.get(pk=estimate_id, is_deleted=False) 335 | except ObjectDoesNotExist: 336 | raise Http404("The requested estimate could not be found.") 337 | if request.POST: 338 | form = form_class(request.POST, instance=estimate) 339 | 340 | if form.is_valid(): 341 | category = form.save(budget=budget) 342 | return HttpResponseRedirect(reverse('budget_estimate_list', kwargs={'budget_slug': budget.slug})) 343 | else: 344 | form = form_class(instance=estimate) 345 | return render_to_response(template_name, { 346 | 'budget': budget, 347 | 'estimate': estimate, 348 | 'form': form, 349 | }, context_instance=RequestContext(request)) 350 | 351 | 352 | def estimate_delete(request, budget_slug, estimate_id, budget_model_class=Budget, template_name='budget/estimates/delete.html'): 353 | """ 354 | Delete a estimate object. 355 | 356 | Templates: ``budget/estimates/delete.html`` 357 | Context: 358 | budget 359 | the parent budget object for the estimate 360 | estimate 361 | the existing estimate object 362 | """ 363 | budget = get_object_or_404(budget_model_class.active.all(), slug=budget_slug) 364 | try: 365 | estimate = budget.estimates.get(pk=estimate_id, is_deleted=False) 366 | except ObjectDoesNotExist: 367 | raise Http404("The requested estimate could not be found.") 368 | if request.POST: 369 | if request.POST.get('confirmed'): 370 | estimate.delete() 371 | return HttpResponseRedirect(reverse('budget_estimate_list', kwargs={'budget_slug': budget.slug})) 372 | return render_to_response(template_name, { 373 | 'budget': budget, 374 | 'estimate': estimate, 375 | }, context_instance=RequestContext(request)) 376 | -------------------------------------------------------------------------------- /docs/budget.txt: -------------------------------------------------------------------------------- 1 | ======================================================= 2 | django-budget: A simple personal budgeting application. 3 | ======================================================= 4 | 5 | ```django-budget``` is a simple budgeting application for use with Django. It 6 | is designed to manage personal finances. We used it to replace a complicated 7 | Excel spreadsheet that didn't retain all the details we wanted. 8 | 9 | It was implemented in Django based on familiarity, quick time to implement 10 | and the premise that it could be accessible everywhere. In practice, we run 11 | this locally (NOT on a publicly accessible website). 12 | 13 | 14 | A Note About Security 15 | ===================== 16 | 17 | It is recommended that anyone using this application add further security by 18 | either protecting the whole app with HTTP Auth, wrap the views with the 19 | ```login-required``` decorator, run it on a local machine or implement similar 20 | protections. This application is for your use and makes no assumptions about 21 | how viewable the data is to other people. 22 | 23 | 24 | Requirements 25 | ============ 26 | 27 | ```django-budget``` requires Python 2.3 or better and Django 1.0 or better. 28 | 29 | 30 | Installation 31 | ============ 32 | 33 | #. Either copy/symlink the ```budget``` app into your project or place it somewhere on your ```PYTHONPATH```. 34 | #. Add the ```budget.categories```, ```budget.transactions``` and ```budget``` apps to your ```INSTALLED_APPS```. 35 | #. Run ```./manage.py syncdb```. 36 | #. Add ```(r'^budget/', include('budgetproject.budget.urls')),``` to your ```urls.py```. 37 | --------------------------------------------------------------------------------