]> asedeno.scripts.mit.edu Git - bluechips.git/blob - bluechips/controllers/spend.py
43293071bdca31592c60f10d575bd281a6872363
[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 def prune_tags():
67     for tag in meta.Session.query(model.Tag).all():
68         if not tag.expenditures:
69             meta.Session.delete(tag)
70     meta.Session.commit()
71
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)
79     tags = TagValidator()
80     date = validators.DateConverter()
81     shares = ForEach(ShareSchema)
82     chained_validators = [ValidateNotAllZero]
83     
84
85 class SpendController(BaseController):
86     def index(self):
87         return self.edit()
88     
89     def edit(self, id=None):
90         c.users = get_users()
91         if id is None:
92             c.title = 'Add a New Expenditure'
93             c.expenditure = model.Expenditure()
94             c.expenditure.spender_id = request.environ['user'].id
95
96             num_residents = meta.Session.query(model.User).\
97                     filter_by(resident=True).count()
98             # Pre-populate split percentages for an even split.
99             c.values = {}
100             for ii, user_row in enumerate(c.users):
101                 user_id, user = user_row
102                 val = 0
103                 if user.resident:
104                     val = Decimal(1)
105                 c.values['shares-%d.amount' % ii] = val
106
107             c.values['tags'] = u""
108         else:
109             c.title = 'Edit an Expenditure'
110             c.expenditure = meta.Session.query(model.Expenditure).get(id)
111             if c.expenditure is None:
112                 abort(404)
113             c.values = {}
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
120
121             c.values['tags'] = ', '.join(c.expenditure.tags)
122
123         return render('/spend/index.mako')
124
125     @redirect_on_get('edit')
126     @authenticate_form
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
130         # old one
131         if id is None:
132             e = model.Expenditure()
133             meta.Session.add(e)
134             op = 'created'
135         else:
136             e = meta.Session.query(model.Expenditure).get(id)
137             if e is None:
138                 abort(404)
139             op = 'updated'
140         
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)
145
146         users = dict(meta.Session.query(model.User.id, model.User).all())
147         split_dict = {}
148         split_text_dict = {}
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)
155         e.tags.clear()
156         e.tags |= tags
157
158         meta.Session.commit()
159        
160         show = ("Expenditure of %s paid for by %s %s." %
161                 (e.amount, e.spender, op))
162         h.flash(show)
163
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,
169                                   'op': op})
170         g.handle_notification(involved_users, show, body)
171
172         prune_tags()
173
174         return h.redirect_to('/')
175
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:
180             abort(404)
181
182         return render('/spend/delete.mako')
183
184     @redirect_on_get('delete')
185     @authenticate_form
186     def destroy(self, id):
187         e = meta.Session.query(model.Expenditure).get(id)
188         if e is None:
189             abort(404)
190
191         if 'delete' in request.params:
192             meta.Session.delete(e)
193
194             meta.Session.commit()
195             show = ("Expenditure of %s paid for by %s deleted." %
196                     (e.amount, e.spender))
197             h.flash(show)
198
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,
203                                       'op': 'deleted'})
204             g.handle_notification(involved_users, show, body)
205
206             prune_tags()
207
208         return h.redirect_to('/')