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)
66 class ExpenditureSchema(Schema):
67 "Validate an expenditure."
68 allow_extra_fields = False
69 pre_validators = [NestedVariables()]
70 spender_id = validators.Int(not_empty=True)
71 amount = model.types.CurrencyValidator(not_empty=True)
72 description = validators.UnicodeString(not_empty=True)
74 date = validators.DateConverter()
75 shares = ForEach(ShareSchema)
76 chained_validators = [ValidateNotAllZero]
79 class SpendController(BaseController):
83 def edit(self, id=None):
86 c.title = 'Add a New Expenditure'
87 c.expenditure = model.Expenditure()
88 c.expenditure.spender_id = request.environ['user'].id
90 num_residents = meta.Session.query(model.User).\
91 filter_by(resident=True).count()
92 # Pre-populate split percentages for an even split.
94 for ii, user_row in enumerate(c.users):
95 user_id, user = user_row
99 c.values['shares-%d.amount' % ii] = val
101 c.values['tags'] = u""
103 c.title = 'Edit an Expenditure'
104 c.expenditure = meta.Session.query(model.Expenditure).get(id)
105 if c.expenditure is None:
108 for ii, user_row in enumerate(c.users):
109 user_id, user = user_row
110 shares_by_user = dict(((sp.user, sp.share_text) for sp
111 in c.expenditure.splits))
112 share = shares_by_user.get(user, '')
113 c.values['shares-%d.amount' % ii] = share
115 c.values['tags'] = ', '.join(c.expenditure.tags)
117 return render('/spend/index.mako')
119 @redirect_on_get('edit')
121 @validate(schema=ExpenditureSchema(), form='edit', variable_decode=True)
122 def update(self, id=None):
123 # Either create a new object, or, if we're editing, get the
126 e = model.Expenditure()
130 e = meta.Session.query(model.Expenditure).get(id)
135 # Set the fields that were submitted
136 shares = self.form_result.pop('shares')
137 tags = self.form_result.pop('tags') or set()
138 update_sar(e, self.form_result)
140 users = dict(meta.Session.query(model.User.id, model.User).all())
143 for share_params in shares:
144 user = users[share_params['user_id']]
145 amount_text, amount = share_params['amount'] or ('',Decimal('0'))
146 split_dict[user] = amount
147 split_text_dict[user] = amount_text
148 e.split(split_dict, split_text_dict)
152 meta.Session.commit()
154 show = ("Expenditure of %s paid for by %s %s." %
155 (e.amount, e.spender, op))
158 # Send email notification to involved users if they have an email set.
159 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
160 involved_users.add(e.spender)
161 body = render('/emails/expenditure.txt',
162 extra_vars={'expenditure': e,
164 g.handle_notification(involved_users, show, body)
166 return h.redirect_to('/')
168 def delete(self, id):
169 c.title = 'Delete an Expenditure'
170 c.expenditure = meta.Session.query(model.Expenditure).get(id)
171 if c.expenditure is None:
174 return render('/spend/delete.mako')
176 @redirect_on_get('delete')
178 def destroy(self, id):
179 e = meta.Session.query(model.Expenditure).get(id)
183 if 'delete' in request.params:
184 meta.Session.delete(e)
186 meta.Session.commit()
187 show = ("Expenditure of %s paid for by %s deleted." %
188 (e.amount, e.spender))
191 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
192 involved_users.add(e.spender)
193 body = render('/emails/expenditure.txt',
194 extra_vars={'expenditure': e,
196 g.handle_notification(involved_users, show, body)
198 return h.redirect_to('/')