]> asedeno.scripts.mit.edu Git - bluechips.git/blob - bluechips/controllers/spend.py
9caa1b390defd714c292efb431cae9ff506a68ff
[bluechips.git] / bluechips / controllers / spend.py
1 """
2 Handle expenditures
3 """
4
5 from __future__ import division
6 import logging
7
8 import re
9 import string
10 from decimal import Decimal, InvalidOperation
11
12 from bluechips.lib.base import *
13
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
18
19 import formencode
20 from formencode import validators, Schema
21 from formencode.foreach import ForEach
22 from formencode.variabledecode import NestedVariables
23 from formencode.schema import SimpleFormValidator
24
25 from mailer import Message
26
27 log = logging.getLogger(__name__)
28
29 class ExpenditureExpression(validators.FancyValidator):
30     goodChars = set('1234567890.+-/*() ')
31
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)
36
37         if value == '':
38             return value, Decimal("0")
39
40         try:
41             number = eval(value)
42             return value, Decimal(str(number))
43         except:
44             raise formencode.Invalid("Not a valid mathematical expression", value, state)
45
46 class TagValidator(validators.FancyValidator):
47     def _to_python(self, value,state):
48         try:
49             return set(map(string.strip, value.split(',')))
50         except:
51             raise formencode.Invalid("Unable to parse tags", value, state)
52
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()
58
59
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)
64
65
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)
73     tags = TagValidator()
74     date = validators.DateConverter()
75     shares = ForEach(ShareSchema)
76     chained_validators = [ValidateNotAllZero]
77     
78
79 class SpendController(BaseController):
80     def index(self):
81         return self.edit()
82     
83     def edit(self, id=None):
84         c.users = get_users()
85         if id is None:
86             c.title = 'Add a New Expenditure'
87             c.expenditure = model.Expenditure()
88             c.expenditure.spender_id = request.environ['user'].id
89
90             num_residents = meta.Session.query(model.User).\
91                     filter_by(resident=True).count()
92             # Pre-populate split percentages for an even split.
93             c.values = {}
94             for ii, user_row in enumerate(c.users):
95                 user_id, user = user_row
96                 val = 0
97                 if user.resident:
98                     val = Decimal(1)
99                 c.values['shares-%d.amount' % ii] = val
100
101             c.values['tags'] = u""
102         else:
103             c.title = 'Edit an Expenditure'
104             c.expenditure = meta.Session.query(model.Expenditure).get(id)
105             if c.expenditure is None:
106                 abort(404)
107             c.values = {}
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
114
115             c.values['tags'] = ', '.join([tag.tag for tag in c.expenditure.tags])
116
117         return render('/spend/index.mako')
118
119     @redirect_on_get('edit')
120     @authenticate_form
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
124         # old one
125         if id is None:
126             e = model.Expenditure()
127             meta.Session.add(e)
128             op = 'created'
129         else:
130             e = meta.Session.query(model.Expenditure).get(id)
131             if e is None:
132                 abort(404)
133             op = 'updated'
134         
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)
139
140         users = dict(meta.Session.query(model.User.id, model.User).all())
141         split_dict = {}
142         split_text_dict = {}
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)
149         e.tag(tags)
150
151         meta.Session.commit()
152        
153         show = ("Expenditure of %s paid for by %s %s." %
154                 (e.amount, e.spender, op))
155         h.flash(show)
156
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,
162                                   'op': op})
163         g.handle_notification(involved_users, show, body)
164
165         return h.redirect_to('/')
166
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:
171             abort(404)
172
173         return render('/spend/delete.mako')
174
175     @redirect_on_get('delete')
176     @authenticate_form
177     def destroy(self, id):
178         e = meta.Session.query(model.Expenditure).get(id)
179         if e is None:
180             abort(404)
181
182         if 'delete' in request.params:
183             meta.Session.delete(e)
184
185             meta.Session.commit()
186             show = ("Expenditure of %s paid for by %s deleted." %
187                     (e.amount, e.spender))
188             h.flash(show)
189
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,
194                                       'op': 'deleted'})
195             g.handle_notification(involved_users, show, body)
196
197         return h.redirect_to('/')