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 pylons import request
-from pylons.decorators.rest import dispatch_on
+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
-from decimal import Decimal, InvalidOperation
+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)
+
+
+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.title = 'Add a New Expenditure'
-
- c.expenditure = dict()
- c.expenditure['spender'] = request.environ['user']
-
- return render('/spend/index.mako')
+ return self.edit()
- def edit(self, id):
- c.title = 'Edit an Expenditure'
-
- c.expenditure = meta.Session.query(model.Expenditure).get(id)
-
+ 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):
- # Validate the submission
- if not valid(self, spend.new_spend_form):
- if id is None:
- return self.index()
- else:
- return self.edit(id)
-
# 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'
# 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)
- meta.Session.save_or_update(e)
-
- if id is None:
- e.even_split()
- else:
- e.update_split()
-
+
+ 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()
-
- h.flash('Expenditure recorded.')
- h.flash("""Want to do something unusual?
-
-<ul id="expenditure_options">
- <li>%s</li>
- <li>%s</li>
-</ul>""" % (h.link_to('Change the split', h.url_for(controller='spend',
- action='split',
- id=e.id)),
- h.link_to('Spin off a subitem', h.url_for(controller='spend',
- action='subitem',
- id=e.id))))
-
+
+ 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)
+
return h.redirect_to('/')
-
- @dispatch_on(POST='_post_split',
- GET='_get_split')
- def split(self, id):
- abort(500)
-
- def _get_split(self, id):
- c.title = 'Change Expenditure Split'
-
+
+ def delete(self, id):
+ c.title = 'Delete an Expenditure'
c.expenditure = meta.Session.query(model.Expenditure).get(id)
- c.users = meta.Session.query(model.User)
-
- return render('/spend/split.mako')
-
- def _post_split(self, id):
- c.values = request.params
- c.errors = dict()
-
- split_dict = dict()
-
- for username, percent in c.values.iteritems():
- try:
- user = meta.Session.query(model.User).\
- filter(model.User.username==username).one()
- split_dict[user] = Decimal(percent)
- except InvalidOperation:
- c.errors[username] = 'Please enter a number'
- if c.errors != dict():
- return self._get_split(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)
- e.split(split_dict)
-
- meta.Session.commit()
-
- h.flash('Expenditure redivided')
-
+ 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)
+
return h.redirect_to('/')