]> asedeno.scripts.mit.edu Git - bluechips.git/blob - bluechips/controllers/spend.py
Tweak tag implementation
[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                 c.values['shares-%d.amount' % ii] = 1 if user.resident else ""
103
104             c.tags = u""
105         else:
106             c.title = 'Edit an Expenditure'
107             c.expenditure = meta.Session.query(model.Expenditure).get(id)
108             if c.expenditure is None:
109                 abort(404)
110             c.values = {}
111             for ii, user_row in enumerate(c.users):
112                 user_id, user = user_row
113                 shares_by_user = dict(((sp.user, sp.share_text) for sp
114                                        in c.expenditure.splits))
115                 share = shares_by_user.get(user, '')
116                 c.values['shares-%d.amount' % ii] = share
117
118             c.tags = ', '.join(c.expenditure.tags)
119
120         return render('/spend/index.mako')
121
122     @redirect_on_get('edit')
123     @authenticate_form
124     @validate(schema=ExpenditureSchema(), form='edit', variable_decode=True)
125     def update(self, id=None):
126         # Either create a new object, or, if we're editing, get the
127         # old one
128         if id is None:
129             e = model.Expenditure()
130             meta.Session.add(e)
131             op = 'created'
132         else:
133             e = meta.Session.query(model.Expenditure).get(id)
134             if e is None:
135                 abort(404)
136             op = 'updated'
137         
138         # Set the fields that were submitted
139         shares = self.form_result.pop('shares')
140         tags = self.form_result.pop('tags') or set()
141         update_sar(e, self.form_result)
142
143         users = dict(meta.Session.query(model.User.id, model.User).all())
144         split_dict = {}
145         split_text_dict = {}
146         for share_params in shares:
147             user = users[share_params['user_id']]
148             amount_text, amount  = share_params['amount'] or ('',Decimal('0'))
149             split_dict[user] = amount
150             split_text_dict[user] = amount_text
151         e.split(split_dict, split_text_dict)
152         e.tags.clear()
153         e.tags |= tags
154
155         meta.Session.commit()
156        
157         show = ("Expenditure of %s paid for by %s %s." %
158                 (e.amount, e.spender, op))
159         h.flash(show)
160
161         # Send email notification to involved users if they have an email set.
162         involved_users = set(sp.user for sp in e.splits if sp.share != 0)
163         involved_users.add(e.spender)
164         body = render('/emails/expenditure.txt',
165                       extra_vars={'expenditure': e,
166                                   'op': op})
167         g.handle_notification(involved_users, show, body)
168
169         prune_tags()
170
171         return h.redirect_to('/')
172
173     def delete(self, id):
174         c.title = 'Delete an Expenditure'
175         c.expenditure = meta.Session.query(model.Expenditure).get(id)
176         if c.expenditure is None:
177             abort(404)
178
179         return render('/spend/delete.mako')
180
181     @redirect_on_get('delete')
182     @authenticate_form
183     def destroy(self, id):
184         e = meta.Session.query(model.Expenditure).get(id)
185         if e is None:
186             abort(404)
187
188         if 'delete' in request.params:
189             meta.Session.delete(e)
190
191             meta.Session.commit()
192             show = ("Expenditure of %s paid for by %s deleted." %
193                     (e.amount, e.spender))
194             h.flash(show)
195
196             involved_users = set(sp.user for sp in e.splits if sp.share != 0)
197             involved_users.add(e.spender)
198             body = render('/emails/expenditure.txt',
199                           extra_vars={'expenditure': e,
200                                       'op': 'deleted'})
201             g.handle_notification(involved_users, show, body)
202
203             prune_tags()
204
205         return h.redirect_to('/')