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
105 c.values['shares-%d.amount' % ii] = val
107 c.values['tags'] = u""
109 c.title = 'Edit an Expenditure'
110 c.expenditure = meta.Session.query(model.Expenditure).get(id)
111 if c.expenditure is None:
114 for ii, user_row in enumerate(c.users):
115 user_id, user = user_row
116 shares_by_user = dict(((sp.user, sp.share_text) for sp
117 in c.expenditure.splits))
118 share = shares_by_user.get(user, '')
119 c.values['shares-%d.amount' % ii] = share
121 c.values['tags'] = ', '.join(c.expenditure.tags)
123 return render('/spend/index.mako')
125 @redirect_on_get('edit')
127 @validate(schema=ExpenditureSchema(), form='edit', variable_decode=True)
128 def update(self, id=None):
129 # Either create a new object, or, if we're editing, get the
132 e = model.Expenditure()
136 e = meta.Session.query(model.Expenditure).get(id)
141 # Set the fields that were submitted
142 shares = self.form_result.pop('shares')
143 tags = self.form_result.pop('tags') or set()
144 update_sar(e, self.form_result)
146 users = dict(meta.Session.query(model.User.id, model.User).all())
149 for share_params in shares:
150 user = users[share_params['user_id']]
151 amount_text, amount = share_params['amount'] or ('',Decimal('0'))
152 split_dict[user] = amount
153 split_text_dict[user] = amount_text
154 e.split(split_dict, split_text_dict)
158 meta.Session.commit()
160 show = ("Expenditure of %s paid for by %s %s." %
161 (e.amount, e.spender, op))
164 # Send email notification to involved users if they have an email set.
165 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
166 involved_users.add(e.spender)
167 body = render('/emails/expenditure.txt',
168 extra_vars={'expenditure': e,
170 g.handle_notification(involved_users, show, body)
174 return h.redirect_to('/')
176 def delete(self, id):
177 c.title = 'Delete an Expenditure'
178 c.expenditure = meta.Session.query(model.Expenditure).get(id)
179 if c.expenditure is None:
182 return render('/spend/delete.mako')
184 @redirect_on_get('delete')
186 def destroy(self, id):
187 e = meta.Session.query(model.Expenditure).get(id)
191 if 'delete' in request.params:
192 meta.Session.delete(e)
194 meta.Session.commit()
195 show = ("Expenditure of %s paid for by %s deleted." %
196 (e.amount, e.spender))
199 involved_users = set(sp.user for sp in e.splits if sp.share != 0)
200 involved_users.add(e.spender)
201 body = render('/emails/expenditure.txt',
202 extra_vars={'expenditure': e,
204 g.handle_notification(involved_users, show, body)
208 return h.redirect_to('/')