]> asedeno.scripts.mit.edu Git - bluechips.git/blob - bluechips/controllers/spend.py
d27ec40d2dc448507b8d50ef1c83a9bddaf61867
[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 from decimal import Decimal, InvalidOperation
10
11 from bluechips.lib.base import *
12
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
17
18 import formencode
19 from formencode import validators, Schema
20 from formencode.foreach import ForEach
21 from formencode.variabledecode import NestedVariables
22 from formencode.schema import SimpleFormValidator
23
24 from mailer import Message
25
26 log = logging.getLogger(__name__)
27
28 class ExpenditureExpression(validators.FancyValidator):
29     goodChars = set('1234567890.+-/*() ')
30
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)
35
36         if value == '':
37             return value, Decimal("0")
38
39         try:
40             number = eval(value)
41             return value, Decimal(str(number))
42         except:
43             raise formencode.Invalid("Not a valid mathematical expression", value, state)
44
45
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()
51
52
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)
57
58
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]
69     
70
71 class SpendController(BaseController):
72     def index(self):
73         return self.edit()
74     
75     def edit(self, id=None):
76         c.users = get_users()
77         if id is None:
78             c.title = 'Add a New Expenditure'
79             c.expenditure = model.Expenditure()
80             c.expenditure.spender_id = request.environ['user'].id
81
82             num_residents = meta.Session.query(model.User).\
83                     filter_by(resident=True).count()
84             # Pre-populate split percentages for an even split.
85             c.values = {}
86             for ii, user_row in enumerate(c.users):
87                 user_id, user = user_row
88                 val = 0
89                 if user.resident:
90                     val = Decimal(100) / Decimal(num_residents)
91                 c.values['shares-%d.amount' % ii] = val
92         else:
93             c.title = 'Edit an Expenditure'
94             c.expenditure = meta.Session.query(model.Expenditure).get(id)
95             if c.expenditure is None:
96                 abort(404)
97             c.values = {}
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
104
105         return render('/spend/index.mako')
106
107     @redirect_on_get('edit')
108     @authenticate_form
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
112         # old one
113         if id is None:
114             e = model.Expenditure()
115             meta.Session.add(e)
116             op = 'created'
117         else:
118             e = meta.Session.query(model.Expenditure).get(id)
119             if e is None:
120                 abort(404)
121             op = 'updated'
122         
123         # Set the fields that were submitted
124         shares = self.form_result.pop('shares')
125         update_sar(e, self.form_result)
126
127         users = dict(meta.Session.query(model.User.id, model.User).all())
128         split_dict = {}
129         split_text_dict = {}
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)
136         
137         meta.Session.commit()
138        
139         show = ("Expenditure of %s paid for by %s %s." %
140                 (e.amount, e.spender, op))
141         h.flash(show)
142
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,
148                                   'op': op})
149         g.handle_notification(involved_users, show, body)
150
151         return h.redirect_to('/')
152
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:
157             abort(404)
158
159         return render('/spend/delete.mako')
160
161     @redirect_on_get('delete')
162     @authenticate_form
163     def destroy(self, id):
164         e = meta.Session.query(model.Expenditure).get(id)
165         if e is None:
166             abort(404)
167
168         if 'delete' in request.params:
169             meta.Session.delete(e)
170
171             meta.Session.commit()
172             show = ("Expenditure of %s paid for by %s deleted." %
173                     (e.amount, e.spender))
174             h.flash(show)
175
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,
180                                       'op': 'deleted'})
181             g.handle_notification(involved_users, show, body)
182
183         return h.redirect_to('/')