5 from __future__ import division
10 from decimal import Decimal, InvalidOperation
12 from bluechips.lib.base import *
14 from pylons import request, app_globals as g
15 from pylons.decorators import validate
16 from pylons.decorators.secure import authenticate_form
17 from pylons.controllers.util import abort
20 from formencode import validators, Schema
21 from formencode.foreach import ForEach
22 from formencode.variabledecode import NestedVariables
23 from formencode.schema import SimpleFormValidator
25 from mailer import Message
27 log = logging.getLogger(__name__)
29 class ExpenditureExpression(validators.FancyValidator):
30 goodChars = set('1234567890.+-/*() ')
32 def _to_python(self, value, state):
33 if (not set(value) <= self.goodChars or
34 re.search(r'([\+\-\*\/])\1', value)):
35 raise formencode.Invalid("Expression contains illegal characters", value, state)
38 return value, Decimal("0")
42 return value, Decimal(str(number))
44 raise formencode.Invalid("Not a valid mathematical expression", value, state)
46 class TagValidator(validators.FancyValidator):
47 def _to_python(self, value,state):
49 return set(map(string.strip, value.split(',')))
51 raise formencode.Invalid("Unable to parse tags", value, state)
53 class ShareSchema(Schema):
54 "Validate individual user shares."
55 allow_extra_fields = False
56 user_id = validators.Int(not_empty=True)
57 amount = ExpenditureExpression()
60 def validate_state(value_dict, state, validator):
61 if all(s['amount'] == 0 for s in value_dict['shares']):
62 return {'shares-0.amount': 'Need at least one non-zero share'}
63 ValidateNotAllZero = SimpleFormValidator(validate_state)
67 for tag in meta.Session.query(model.Tag).all():
68 if not tag.expenditures:
69 meta.Session.delete(tag)
72 class ExpenditureSchema(Schema):
73 "Validate an expenditure."
74 allow_extra_fields = False
75 pre_validators = [NestedVariables()]
76 spender_id = validators.Int(not_empty=True)
77 amount = model.types.CurrencyValidator(not_empty=True)
78 description = validators.UnicodeString(not_empty=True)
80 date = validators.DateConverter()
81 shares = ForEach(ShareSchema)
82 chained_validators = [ValidateNotAllZero]
85 class SpendController(BaseController):
89 def edit(self, id=None):
92 c.title = 'Add a New Expenditure'
93 c.expenditure = model.Expenditure()
94 c.expenditure.spender_id = request.environ['user'].id
96 num_residents = meta.Session.query(model.User).\
97 filter_by(resident=True).count()
98 # Pre-populate split percentages for an even split.
100 for ii, user_row in enumerate(c.users):
101 user_id, user = user_row
102 c.values['shares-%d.amount' % ii] = 1 if user.resident else ""
106 c.title = 'Edit an Expenditure'
107 c.expenditure = meta.Session.query(model.Expenditure).get(id)
108 if c.expenditure is None:
111 for ii, user_row in enumerate(c.users):
112 user_id, user = user_row
113 shares_by_user = dict(((sp.user, sp.share_text) for sp
114 in c.expenditure.splits))
115 share = shares_by_user.get(user, '')
116 c.values['shares-%d.amount' % ii] = share
118 c.tags = ', '.join(c.expenditure.tags)
120 return render('/spend/index.mako')
122 @redirect_on_get('edit')
124 @validate(schema=ExpenditureSchema(), form='edit', variable_decode=True)
125 def update(self, id=None):
126 # Either create a new object, or, if we're editing, get the
129 e = model.Expenditure()
133 e = meta.Session.query(model.Expenditure).get(id)
138 # Set the fields that were submitted
139 shares = self.form_result.pop('shares')
140 tags = self.form_result.pop('tags') or set()
141 update_sar(e, self.form_result)
143 users = dict(meta.Session.query(model.User.id, model.User).all())
146 for share_params in shares:
147 user = users[share_params['user_id']]
148 amount_text, amount = share_params['amount'] or ('',Decimal('0'))
149 split_dict[user] = amount
150 split_text_dict[user] = amount_text
151 e.split(split_dict, split_text_dict)
155 meta.Session.commit()
157 show = ("Expenditure of %s paid for by %s %s." %
158 (e.amount, e.spender, op))
161 # Send email notification to involved users if they have an email set.
162 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
163 involved_users.add(e.spender)
164 body = render('/emails/expenditure.txt',
165 extra_vars={'expenditure': e,
167 g.handle_notification(involved_users, show, body)
171 return h.redirect_to('/')
173 def delete(self, id):
174 c.title = 'Delete an Expenditure'
175 c.expenditure = meta.Session.query(model.Expenditure).get(id)
176 if c.expenditure is None:
179 return render('/spend/delete.mako')
181 @redirect_on_get('delete')
183 def destroy(self, id):
184 e = meta.Session.query(model.Expenditure).get(id)
188 if 'delete' in request.params:
189 meta.Session.delete(e)
191 meta.Session.commit()
192 show = ("Expenditure of %s paid for by %s deleted." %
193 (e.amount, e.spender))
196 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
197 involved_users.add(e.spender)
198 body = render('/emails/expenditure.txt',
199 extra_vars={'expenditure': e,
201 g.handle_notification(involved_users, show, body)
205 return h.redirect_to('/')