5 from __future__ import division
9 from decimal import Decimal, InvalidOperation
11 from bluechips.lib.base import *
13 from pylons import request, app_globals as g
14 from pylons.decorators import validate
15 from pylons.decorators.secure import authenticate_form
16 from pylons.controllers.util import abort
19 from formencode import validators, Schema
20 from formencode.foreach import ForEach
21 from formencode.variabledecode import NestedVariables
22 from formencode.schema import SimpleFormValidator
24 from mailer import Message
26 log = logging.getLogger(__name__)
28 class ExpenditureExpression(validators.FancyValidator):
29 goodChars = set('1234567890.+-/*() ')
31 def _to_python(self, value, state):
32 if (not set(value) <= self.goodChars or
33 re.search(r'([\+\-\*\/])\1', value)):
34 raise formencode.Invalid("Expression contains illegal characters", value, state)
37 return value, Decimal("0")
41 return value, Decimal(str(number))
43 raise formencode.Invalid("Not a valid mathematical expression", value, state)
46 class ShareSchema(Schema):
47 "Validate individual user shares."
48 allow_extra_fields = False
49 user_id = validators.Int(not_empty=True)
50 amount = ExpenditureExpression()
53 def validate_state(value_dict, state, validator):
54 if all(s['amount'] == 0 for s in value_dict['shares']):
55 return {'shares-0.amount': 'Need at least one non-zero share'}
56 ValidateNotAllZero = SimpleFormValidator(validate_state)
59 class ExpenditureSchema(Schema):
60 "Validate an expenditure."
61 allow_extra_fields = False
62 pre_validators = [NestedVariables()]
63 spender_id = validators.Int(not_empty=True)
64 amount = model.types.CurrencyValidator(not_empty=True)
65 description = validators.UnicodeString(not_empty=True)
66 date = validators.DateConverter()
67 shares = ForEach(ShareSchema)
68 chained_validators = [ValidateNotAllZero]
71 class SpendController(BaseController):
75 def edit(self, id=None):
78 c.title = 'Add a New Expenditure'
79 c.expenditure = model.Expenditure()
80 c.expenditure.spender_id = request.environ['user'].id
82 num_residents = meta.Session.query(model.User).\
83 filter_by(resident=True).count()
84 # Pre-populate split percentages for an even split.
86 for ii, user_row in enumerate(c.users):
87 user_id, user = user_row
90 val = Decimal(100) / Decimal(num_residents)
91 c.values['shares-%d.amount' % ii] = val
93 c.title = 'Edit an Expenditure'
94 c.expenditure = meta.Session.query(model.Expenditure).get(id)
95 if c.expenditure is None:
98 for ii, user_row in enumerate(c.users):
99 user_id, user = user_row
100 shares_by_user = dict(((sp.user, sp.share_text) for sp
101 in c.expenditure.splits))
102 share = shares_by_user.get(user, '')
103 c.values['shares-%d.amount' % ii] = share
105 return render('/spend/index.mako')
107 @redirect_on_get('edit')
109 @validate(schema=ExpenditureSchema(), form='edit', variable_decode=True)
110 def update(self, id=None):
111 # Either create a new object, or, if we're editing, get the
114 e = model.Expenditure()
118 e = meta.Session.query(model.Expenditure).get(id)
123 # Set the fields that were submitted
124 shares = self.form_result.pop('shares')
125 update_sar(e, self.form_result)
127 users = dict(meta.Session.query(model.User.id, model.User).all())
130 for share_params in shares:
131 user = users[share_params['user_id']]
132 amount_text, amount = share_params['amount'] or ('',Decimal('0'))
133 split_dict[user] = amount
134 split_text_dict[user] = amount_text
135 e.split(split_dict, split_text_dict)
137 meta.Session.commit()
139 show = ("Expenditure of %s paid for by %s %s." %
140 (e.amount, e.spender, op))
143 # Send email notification to involved users if they have an email set.
144 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
145 involved_users.add(e.spender)
146 body = render('/emails/expenditure.txt',
147 extra_vars={'expenditure': e,
149 g.handle_notification(involved_users, show, body)
151 return h.redirect_to('/')
153 def delete(self, id):
154 c.title = 'Delete an Expenditure'
155 c.expenditure = meta.Session.query(model.Expenditure).get(id)
156 if c.expenditure is None:
159 return render('/spend/delete.mako')
161 @redirect_on_get('delete')
163 def destroy(self, id):
164 e = meta.Session.query(model.Expenditure).get(id)
168 if 'delete' in request.params:
169 meta.Session.delete(e)
171 meta.Session.commit()
172 show = ("Expenditure of %s paid for by %s deleted." %
173 (e.amount, e.spender))
176 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
177 involved_users.add(e.spender)
178 body = render('/emails/expenditure.txt',
179 extra_vars={'expenditure': e,
181 g.handle_notification(involved_users, show, body)
183 return h.redirect_to('/')