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([tag.tag for tag in 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)
151 meta.Session.commit()
153 show = ("Expenditure of %s paid for by %s %s." %
154 (e.amount, e.spender, op))
157 # Send email notification to involved users if they have an email set.
158 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
159 involved_users.add(e.spender)
160 body = render('/emails/expenditure.txt',
161 extra_vars={'expenditure': e,
163 g.handle_notification(involved_users, show, body)
165 return h.redirect_to('/')
167 def delete(self, id):
168 c.title = 'Delete an Expenditure'
169 c.expenditure = meta.Session.query(model.Expenditure).get(id)
170 if c.expenditure is None:
173 return render('/spend/delete.mako')
175 @redirect_on_get('delete')
177 def destroy(self, id):
178 e = meta.Session.query(model.Expenditure).get(id)
182 if 'delete' in request.params:
183 meta.Session.delete(e)
185 meta.Session.commit()
186 show = ("Expenditure of %s paid for by %s deleted." %
187 (e.amount, e.spender))
190 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
191 involved_users.add(e.spender)
192 body = render('/emails/expenditure.txt',
193 extra_vars={'expenditure': e,
195 g.handle_notification(involved_users, show, body)
197 return h.redirect_to('/')