diff --git a/accountant/api/views/accounts.py b/accountant/api/views/accounts.py index da53f6a..21df50f 100644 --- a/accountant/api/views/accounts.py +++ b/accountant/api/views/accounts.py @@ -15,36 +15,22 @@ along with Accountant. If not, see . """ from flask import json, request +from flask.ext.restful import Resource, fields, reqparse, marshal_with_field + +from sqlalchemy.orm.exc import NoResultFound from forms.accounts import AccountForm -from accountant import session_scope +from accountant import session_scope, session_aware from . import auth -from .. import api +from .. import api, api_api from ..models.accounts import Account from ..models.entries import Entry from ..models.operations import Operation - -@api.route("/accounts", methods=["GET"]) -@auth.login_required -def get_accounts(): - """ - Returns accounts with their solds. - """ - with session_scope() as session: - query = Account.get_accounts(session) - - return json.dumps([{ - "id": i.id, - "name": i.name, - "authorized_overdraft": i.authorized_overdraft, - "current": str(i.current), - "pointed": str(i.pointed), - "future": str(i.future) - } for i in query.all()]) +from ..fields import Object @api.route("/accounts////") @@ -82,50 +68,114 @@ def get_months(account_id): } for i in query.all()]) -@api.route("/accounts", methods=["PUT"]) -@auth.login_required -def add_account(): - with session_scope() as session: - account = Account(request.json['name'], - request.json['authorized_overdraft']) +resource_fields = { + 'id': fields.Integer, + 'name': fields.String, + 'authorized_overdraft': fields.Fixed(decimals=2), + 'current': fields.Float, + 'pointed': fields.Float, + 'future': fields.Float, +} + + +parser = reqparse.RequestParser() +parser.add_argument('name', type=str, required=True) +parser.add_argument('authorized_overdraft', type=float, required=True) + + +class AccountListResource(Resource): + @session_aware + @marshal_with_field(fields.List(Object(resource_fields))) + def get(self, session=None): + """ + Returns accounts with their balances. + """ + return Account.get_accounts(session).all(), 200 + + def put(self): + """ + Batch update, not implemented. + """ + raise NotImplementedError() + + @session_aware + @marshal_with_field(Object(resource_fields)) + def post(self, session=None): + """ + Create a new account. + """ + kwargs = parser.parse_args() + + account = Account(**kwargs) session.add(account) - return json.dumps("Account added.") + return account, 201 + + def delete(self): + """ + Batch delete, not implemented. + """ + raise NotImplementedError() -@api.route("/accounts/", methods=["PUT"]) -@auth.login_required -def update_account(account_id): - account_form = AccountForm() +class AccountResource(Resource): + @session_aware + @marshal_with_field(Object(resource_fields)) + def get(self, account_id, session=None): + """ + Get account. + """ + try: + return Account.get(session, account_id) + except NoResultFound: + return None, 404 - if account_form.validate(): - with session_scope() as session: - account = session.query(Account).get(account_id) - - account.name = request.json['name'] - account.authorized_overdraft = request.json['authorized_overdraft'] - - session.merge(account) - - return json.dumps("Account #%s updated." % account_id) - else: - return json.dumps({ - 'ok': False, - 'error_type': 'validation', - 'errors': account_form.errorsi - }) - - -@api.route("/accounts/", methods=["DELETE"]) -@auth.login_required -def delete_account(account_id): - with session_scope() as session: - query = session.query(Account) - query = query.filter(Account.id == account_id) - - account = query.first() + @session_aware + @marshal_with_field(Object(resource_fields)) + def delete(self, account_id, session=None): + try: + account = Account.get(session, account_id) + except NoResultFound: + return None, 404 session.delete(account) - return json.dumps("Account #%s deleted." % account_id) + return account + + def patch(self, id): + pass + + @session_aware + @marshal_with_field(Object(resource_fields)) + def put(self, account_id, session=None): + kwargs = parser.parse_args() + + assert (id not in kwargs or kwargs.id is None + or kwargs.id == account_id) + + account_form = AccountForm() + + if account_form.validate(): + try: + account = Account.get(session, account_id) + except NoResultFound: + return None, 404 + + # SQLAlchemy objects ignore __dict__.update() with merge. + for k, v in kwargs.items(): + setattr(account, k, v) + + session.merge(account) + + return account + else: + return json.dumps({ + 'ok': False, + 'error_type': 'validation', + 'errors': account_form.errors + }) + + +api_api.add_resource(AccountListResource, '/accounts') +api_api.add_resource(AccountResource, '/accounts/') diff --git a/accountant/api/views/entries.py b/accountant/api/views/entries.py index f70a7da..a8162be 100644 --- a/accountant/api/views/entries.py +++ b/accountant/api/views/entries.py @@ -14,15 +14,22 @@ You should have received a copy of the GNU Affero General Public License along with Accountant. If not, see . """ -from flask import json, request +import dateutil.parser -from accountant import session_scope +from flask import json +from flask.ext.restful import Resource, fields, reqparse, marshal_with_field -from .. import api +from sqlalchemy.orm.exc import NoResultFound + +from accountant import session_scope, session_aware + +from .. import api, api_api from ..models.entries import Entry from ..models.operations import Operation +from ..fields import Object + @api.route("/entries///") def get_entries(account_id, year, month): @@ -47,48 +54,90 @@ def get_entries(account_id, year, month): } for i in query.all()]) -@api.route("/entries", methods=["PUT"]) -def add_entry(): - with session_scope() as session: - entry = Entry( - operation_date=request.json['operation_date'], - pointed=request.json['pointed'], - label=request.json['label'], - value=request.json['value'], - category=request.json['category'], - account_id=request.json['account_id'], - scheduled_operation_id=request.json['scheduled_operation_id'] - ) +resource_fields = { + # 'id': fields.Integer, + 'operation_date': fields.DateTime(dt_format='iso8601'), + 'label': fields.String, + 'value': fields.Fixed(decimals=2), + 'pointed': fields.Boolean, + 'category': fields.String, + 'account_id': fields.Integer, + 'scheduled_operation_id': fields.Integer, +} + +parser = reqparse.RequestParser() +# Must use lambda because the parser passes other parameters badly interpreted +# by dateutil.parser.parse +parser.add_argument('operation_date', type=lambda a: dateutil.parser.parse(a)) +parser.add_argument('label', type=str) +parser.add_argument('value', type=float) +parser.add_argument('pointed', type=bool) +parser.add_argument('category', type=str) +parser.add_argument('account_id', type=int) +parser.add_argument('scheduled_operation_id', type=int) + + +class EntryListResource(Resource): + def put(self, *args): + return self.post() + + @session_aware + @marshal_with_field(Object(resource_fields)) + def post(self, session=None): + kwargs = parser.parse_args() + + entry = Entry(**kwargs) session.add(entry) - return json.dumps("Entry added.") + return entry -@api.route("/entries/", methods=["PUT"]) -def update_entry(entry_id): - with session_scope() as session: - entry = session.query(Entry).get(entry_id) +class EntryResource(Resource): + @session_aware + @marshal_with_field(Object(resource_fields)) + def get(self, entry_id, session=None): + """ + Get entry. + """ + try: + return Entry.get(session, entry_id) + except NoResultFound: + return None, 404 - entry.id = entry_id - entry.operation_date = request.json['operation_date'] - entry.pointed = request.json['pointed'] - entry.label = request.json['label'] - entry.value = request.json['value'] - entry.category = request.json['category'] - entry.account_id = request.json['account_id'] - entry.scheduled_operation_id = request.json['scheduled_operation_id'] + @session_aware + @marshal_with_field(Object(resource_fields)) + def put(self, entry_id, session=None): + kwargs = parser.parse_args() + + assert (id not in kwargs or kwargs.id is None + or kwargs.id == entry_id) + + try: + entry = Entry.get(session, entry_id) + except NoResultFound: + return None, 404 + + # SQLAlchemy objects ignore __dict__.update() with merge. + for k, v in kwargs.items(): + setattr(entry, k, v) session.merge(entry) - return json.dumps("Entry #%s updated." % entry_id) + return entry - -@api.route("/entries/", methods=["DELETE"]) -def delete_entry(entry_id): - with session_scope() as session: - entry = session.query(Entry).filter(Entry.id == entry_id).first() + @session_aware + @marshal_with_field(Object(resource_fields)) + def delete(self, entry_id, session=None): + try: + entry = Entry.get(session, entry_id) + except NoResultFound: + return None, 404 session.delete(entry) - return json.dumps("Entry #%s deleted." % entry_id) + return entry + + +api_api.add_resource(EntryListResource, "/entries") +api_api.add_resource(EntryResource, "/entries/") diff --git a/accountant/api/views/scheduled_operations.py b/accountant/api/views/scheduled_operations.py index 4538648..efc0654 100644 --- a/accountant/api/views/scheduled_operations.py +++ b/accountant/api/views/scheduled_operations.py @@ -14,16 +14,23 @@ You should have received a copy of the GNU Affero General Public License along with Accountant. If not, see . """ -from flask import json, request +import dateutil.parser -from accountant import session_scope +from flask import json, request +from flask.ext.restful import Resource, fields, reqparse, marshal_with_field + +from sqlalchemy.orm.exc import NoResultFound + +from accountant import session_scope, session_aware from ..models.scheduled_operations import ScheduledOperation -from .. import api +from .. import api, api_api + +from ..fields import Object -@api.route("/scheduled_operations/") +@api.route("/scheduled_operations/") def get_scheduled_operations(account_id): """ Return entries for an account, year, and month. @@ -45,57 +52,94 @@ def get_scheduled_operations(account_id): } for i in query.all()]) -@api.route("/scheduled_operations", methods=["PUT"]) -def add_scheduled_operation(): - with session_scope() as session: - scheduledOperation = ScheduledOperation( - start_date=request.json['start_date'], - stop_date=request.json['stop_date'], - day=request.json['day'], - frequency=request.json['frequency'], - label=request.json['label'], - value=request.json['value'], - category=request.json['category'], - account_id=request.json['account_id'] - ) +resource_fields = { + 'id': fields.Integer, + 'start_date': fields.DateTime(dt_format='iso8601'), + 'stop_date': fields.DateTime(dt_format='iso8601'), + 'day': fields.Integer, + 'frequency': fields.Integer, + 'label': fields.String, + 'value': fields.Fixed(decimals=2), + 'category': fields.String, + 'account_id': fields.Integer, +} + + +parser = reqparse.RequestParser() +parser.add_argument('start_date', type=lambda a: dateutil.parser.parse(a)) +parser.add_argument('stop_date', type=lambda a: dateutil.parser.parse(a)) +parser.add_argument('day', type=int) +parser.add_argument('frequency', type=int) +parser.add_argument('label', type=str) +parser.add_argument('value', type=float) +parser.add_argument('category', type=str) +parser.add_argument('account_id', type=int) + + +class ScheduledOperationListResource(Resource): + @session_aware + @marshal_with_field(Object(resource_fields)) + def put(self, session=None): + """ + Add a new scheduled operation. + """ + kwargs = parser.parse_args() + + scheduledOperation = ScheduledOperation(**kwargs) session.add(scheduledOperation) - return json.dumps("Scheduled operation added.") + return scheduledOperation, 201 -@api.route("/scheduled_operations/", methods=["PUT"]) -def update_scheduled_operation(scheduled_operation_id): - with session_scope() as session: - query = session.query(ScheduledOperation) - query = query.filter(ScheduledOperation.id == scheduled_operation_id) +class ScheduledOperationResource(Resource): + @session_aware + @marshal_with_field(Object(resource_fields)) + def get(self, scheduled_operation_id, session=None): + """ + Get scheduled operation. + """ + try: + return ScheduledOperation.get(session, scheduled_operation_id) + except NoResultFound: + return None, 404 - scheduledOperation = query.first() + @session_aware + @marshal_with_field(Object(resource_fields)) + def delete(self, scheduled_operation_id, session=None): + try: + scheduled_operation = ScheduledOperation.get( + session, scheduled_operation_id) + except NoResultFound: + return None, 404 - scheduledOperation.id = scheduled_operation_id - scheduledOperation.start_date = request.json['start_date'], - scheduledOperation.stop_date = request.json['stop_date'], - scheduledOperation.day = request.json['day'], - scheduledOperation.frequency = request.json['frequency'], - scheduledOperation.label = request.json['label'] - scheduledOperation.value = request.json['value'] - scheduledOperation.category = request.json['category'] - scheduledOperation.account_id = request.json['account_id'] + session.delete(scheduled_operation) - session.merge(scheduledOperation) + return scheduled_operation - return json.dumps( - "Scheduled operation #%s updated." % scheduled_operation_id) + @session_aware + @marshal_with_field(Object(resource_fields)) + def put(self, scheduled_operation_id, session=None): + kwargs = parser.parse_args() + + assert (id not in kwargs or kwargs.id is None + or kwargs.id == scheduled_operation_id) + + try: + scheduled_operation = ScheduledOperation.get( + session, scheduled_operation_id) + except NoResultFound: + return None, 404 + + # SQLAlchemy objects ignore __dict__.update() with merge. + for k, v in kwargs.items(): + setattr(scheduled_operation, k, v) + + session.merge(scheduled_operation) + + return scheduled_operation -@api.route("/scheduled_operations/", methods=["DELETE"]) -def delete_scheduled_operation(scheduled_operation_id): - with session_scope() as session: - query = session.query(ScheduledOperation) - query = query.filter(ScheduledOperation.id == scheduled_operation_id) - scheduledOperation = query.first() - - session.delete(scheduledOperation) - - return json.dumps( - "Scheduled operation #%s deleted." % scheduled_operation_id) +api_api.add_resource(ScheduledOperationListResource, "/scheduled_operations") +api_api.add_resource(ScheduledOperationResource, + "/scheduled_operations/")