]> asedeno.scripts.mit.edu Git - bluechips.git/blobdiff - bluechips/controllers/spend.py
Added UI for working with tags
[bluechips.git] / bluechips / controllers / spend.py
index 64318cf5139c4df770ebf96da8c7634d6436d11c..43293071bdca31592c60f10d575bd281a6872363 100644 (file)
 Handle expenditures
 """
 
+from __future__ import division
 import logging
 
+import re
+import string
+from decimal import Decimal, InvalidOperation
+
 from bluechips.lib.base import *
-from bluechips.widgets import spend
-from bluechips.lib.split import *
 
-from pylons import request
+from pylons import request, app_globals as g
+from pylons.decorators import validate
+from pylons.decorators.secure import authenticate_form
+from pylons.controllers.util import abort
+
+import formencode
+from formencode import validators, Schema
+from formencode.foreach import ForEach
+from formencode.variabledecode import NestedVariables
+from formencode.schema import SimpleFormValidator
+
+from mailer import Message
 
 log = logging.getLogger(__name__)
 
+class ExpenditureExpression(validators.FancyValidator):
+    goodChars = set('1234567890.+-/*() ')
+
+    def _to_python(self, value, state):
+        if (not set(value) <= self.goodChars or
+            re.search(r'([\+\-\*\/])\1', value)):
+            raise formencode.Invalid("Expression contains illegal characters", value, state)
+
+        if value == '':
+            return value, Decimal("0")
+
+        try:
+            number = eval(value)
+            return value, Decimal(str(number))
+        except:
+            raise formencode.Invalid("Not a valid mathematical expression", value, state)
+
+class TagValidator(validators.FancyValidator):
+    def _to_python(self, value,state):
+        try:
+            return set(map(string.strip, value.split(',')))
+        except:
+            raise formencode.Invalid("Unable to parse tags", value, state)
+
+class ShareSchema(Schema):
+    "Validate individual user shares."
+    allow_extra_fields = False
+    user_id = validators.Int(not_empty=True)
+    amount = ExpenditureExpression()
+
+
+def validate_state(value_dict, state, validator):
+    if all(s['amount'] == 0 for s in value_dict['shares']):
+        return {'shares-0.amount': 'Need at least one non-zero share'}
+ValidateNotAllZero = SimpleFormValidator(validate_state)
+
+
+def prune_tags():
+    for tag in meta.Session.query(model.Tag).all():
+        if not tag.expenditures:
+            meta.Session.delete(tag)
+    meta.Session.commit()
+
+class ExpenditureSchema(Schema):
+    "Validate an expenditure."
+    allow_extra_fields = False
+    pre_validators = [NestedVariables()]
+    spender_id = validators.Int(not_empty=True)
+    amount = model.types.CurrencyValidator(not_empty=True)
+    description = validators.UnicodeString(not_empty=True)
+    tags = TagValidator()
+    date = validators.DateConverter()
+    shares = ForEach(ShareSchema)
+    chained_validators = [ValidateNotAllZero]
+    
+
 class SpendController(BaseController):
     def index(self):
-        c.expenditure = dict()
-        c.expenditure['spender'] = request.environ['user']
-        
-        return render('/spend/index.mako')
+        return self.edit()
     
-    @validate(form=spend.new_spend_form, error_handler='index')
-    def new(self):
-        e = model.Expenditure()
-        update_sar(e, self.form_result)
-        meta.Session.save(e)
+    def edit(self, id=None):
+        c.users = get_users()
+        if id is None:
+            c.title = 'Add a New Expenditure'
+            c.expenditure = model.Expenditure()
+            c.expenditure.spender_id = request.environ['user'].id
+
+            num_residents = meta.Session.query(model.User).\
+                    filter_by(resident=True).count()
+            # Pre-populate split percentages for an even split.
+            c.values = {}
+            for ii, user_row in enumerate(c.users):
+                user_id, user = user_row
+                val = 0
+                if user.resident:
+                    val = Decimal(1)
+                c.values['shares-%d.amount' % ii] = val
+
+            c.values['tags'] = u""
+        else:
+            c.title = 'Edit an Expenditure'
+            c.expenditure = meta.Session.query(model.Expenditure).get(id)
+            if c.expenditure is None:
+                abort(404)
+            c.values = {}
+            for ii, user_row in enumerate(c.users):
+                user_id, user = user_row
+                shares_by_user = dict(((sp.user, sp.share_text) for sp
+                                       in c.expenditure.splits))
+                share = shares_by_user.get(user, '')
+                c.values['shares-%d.amount' % ii] = share
+
+            c.values['tags'] = ', '.join(c.expenditure.tags)
+
+        return render('/spend/index.mako')
+
+    @redirect_on_get('edit')
+    @authenticate_form
+    @validate(schema=ExpenditureSchema(), form='edit', variable_decode=True)
+    def update(self, id=None):
+        # Either create a new object, or, if we're editing, get the
+        # old one
+        if id is None:
+            e = model.Expenditure()
+            meta.Session.add(e)
+            op = 'created'
+        else:
+            e = meta.Session.query(model.Expenditure).get(id)
+            if e is None:
+                abort(404)
+            op = 'updated'
         
-        even_split(e)
+        # Set the fields that were submitted
+        shares = self.form_result.pop('shares')
+        tags = self.form_result.pop('tags') or set()
+        update_sar(e, self.form_result)
+
+        users = dict(meta.Session.query(model.User.id, model.User).all())
+        split_dict = {}
+        split_text_dict = {}
+        for share_params in shares:
+            user = users[share_params['user_id']]
+            amount_text, amount  = share_params['amount'] or ('',Decimal('0'))
+            split_dict[user] = amount
+            split_text_dict[user] = amount_text
+        e.split(split_dict, split_text_dict)
+        e.tags.clear()
+        e.tags |= tags
+
         meta.Session.commit()
-        
-        c.expenditure = e
-        
-        return render('/spend/new.mako')
+       
+        show = ("Expenditure of %s paid for by %s %s." %
+                (e.amount, e.spender, op))
+        h.flash(show)
+
+        # Send email notification to involved users if they have an email set.
+        involved_users = set(sp.user for sp in e.splits if sp.share != 0)
+        involved_users.add(e.spender)
+        body = render('/emails/expenditure.txt',
+                      extra_vars={'expenditure': e,
+                                  'op': op})
+        g.handle_notification(involved_users, show, body)
+
+        prune_tags()
+
+        return h.redirect_to('/')
+
+    def delete(self, id):
+        c.title = 'Delete an Expenditure'
+        c.expenditure = meta.Session.query(model.Expenditure).get(id)
+        if c.expenditure is None:
+            abort(404)
+
+        return render('/spend/delete.mako')
+
+    @redirect_on_get('delete')
+    @authenticate_form
+    def destroy(self, id):
+        e = meta.Session.query(model.Expenditure).get(id)
+        if e is None:
+            abort(404)
+
+        if 'delete' in request.params:
+            meta.Session.delete(e)
+
+            meta.Session.commit()
+            show = ("Expenditure of %s paid for by %s deleted." %
+                    (e.amount, e.spender))
+            h.flash(show)
+
+            involved_users = set(sp.user for sp in e.splits if sp.share != 0)
+            involved_users.add(e.spender)
+            body = render('/emails/expenditure.txt',
+                          extra_vars={'expenditure': e,
+                                      'op': 'deleted'})
+            g.handle_notification(involved_users, show, body)
+
+            prune_tags()
+
+        return h.redirect_to('/')