Convert to flask-restplus and document for swagger.
This commit is contained in:
parent
4b1313e65c
commit
bb708e81b3
@ -16,12 +16,20 @@
|
|||||||
"""
|
"""
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
|
||||||
from flask.ext.restful import Api
|
from flask.ext.restplus import Api
|
||||||
from flask.ext.cors import CORS
|
from flask.ext.cors import CORS
|
||||||
|
|
||||||
blueprint = Blueprint('api', __name__)
|
blueprint = Blueprint('api', __name__)
|
||||||
api = Api(blueprint)
|
|
||||||
|
|
||||||
|
authorizations = {
|
||||||
|
'apikey': {
|
||||||
|
'type': 'apiKey',
|
||||||
|
'in': 'header',
|
||||||
|
'name': 'Authorization'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api = Api(blueprint, doc='/doc/', authorizations=authorizations)
|
||||||
CORS(blueprint)
|
CORS(blueprint)
|
||||||
|
|
||||||
# Load all views.
|
# Load all views.
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from flask.ext.restful import marshal, fields
|
from flask.ext.restplus import marshal, fields
|
||||||
|
|
||||||
|
|
||||||
class Object(fields.Raw):
|
class Object(fields.Raw):
|
||||||
@ -24,17 +24,20 @@ class Object(fields.Raw):
|
|||||||
SQLAlchemy rows are viewed as tuples by Restful marshaller, and must be
|
SQLAlchemy rows are viewed as tuples by Restful marshaller, and must be
|
||||||
translated into a dict before marshaling.
|
translated into a dict before marshaling.
|
||||||
"""
|
"""
|
||||||
def __init__(self, fields, **kwargs):
|
def __init__(self, model, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param fields: field declaration.
|
:param model: the target model of the object.
|
||||||
"""
|
"""
|
||||||
self.fields = fields
|
self.model = model
|
||||||
|
|
||||||
super(Object, self).__init__(**kwargs)
|
super(Object, self).__init__(**kwargs)
|
||||||
|
|
||||||
def format(self, value):
|
def format(self, value):
|
||||||
# First transform object in dict with fields in attribute.
|
# First transform object in dict with fields in attribute.
|
||||||
result = {key: getattr(value, key, None) for key in self.fields.keys()}
|
result = {key: getattr(value, key, None) for key in self.model.keys()}
|
||||||
|
|
||||||
# Marshal the dict
|
# Marshal the dict
|
||||||
return marshal(result, self.fields)
|
return marshal(result, self.model)
|
||||||
|
|
||||||
|
def schema(self):
|
||||||
|
return self.model.__schema__
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from flask.ext.restful import Resource, fields, marshal_with_field
|
from flask.ext.restplus import Resource, fields, marshal_with_field
|
||||||
|
|
||||||
from accountant import db
|
from accountant import db
|
||||||
|
|
||||||
@ -27,12 +27,22 @@ from ..fields import Object
|
|||||||
|
|
||||||
from .models import (account_model, solds_model, balance_model,
|
from .models import (account_model, solds_model, balance_model,
|
||||||
category_model, ohlc_model)
|
category_model, ohlc_model)
|
||||||
from .parsers import account_parser, range_parser
|
from .parsers import range_parser
|
||||||
from .users import requires_auth
|
from .users import requires_auth
|
||||||
|
|
||||||
|
|
||||||
|
ns = api.namespace('account', description='Account management')
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/')
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
401: 'Unauthorized'
|
||||||
|
})
|
||||||
class AccountListResource(Resource):
|
class AccountListResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', [account_model])
|
||||||
@marshal_with_field(fields.List(Object(account_model)))
|
@marshal_with_field(fields.List(Object(account_model)))
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
@ -41,51 +51,92 @@ class AccountListResource(Resource):
|
|||||||
return Account.query().all(), 200
|
return Account.query().all(), 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.expect(account_model)
|
||||||
|
@api.response(201, 'Account created', account_model)
|
||||||
|
@api.response(406, 'Invalid account data')
|
||||||
@marshal_with_field(Object(account_model))
|
@marshal_with_field(Object(account_model))
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Create a new account.
|
Create a new account.
|
||||||
"""
|
"""
|
||||||
data = account_parser.parse_args()
|
data = api.payload
|
||||||
|
|
||||||
|
# A new account MUST NOT have an id;
|
||||||
|
if 'id' in data and data['id']:
|
||||||
|
api.abort(
|
||||||
|
406,
|
||||||
|
error_message='Id must not be provided on creation.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Instantiate account with data.
|
||||||
account = Account(**data)
|
account = Account(**data)
|
||||||
|
|
||||||
|
# Add new account in session.
|
||||||
db.session.add(account)
|
db.session.add(account)
|
||||||
|
|
||||||
# Flush session to have id in account.
|
# Flush session to have id in account.
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Return account data.
|
# Return account.
|
||||||
return account, 201
|
return account, 201
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/<int:id>')
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
params={
|
||||||
|
'id': 'Id of the account to manage'
|
||||||
|
},
|
||||||
|
responses={
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Account not found'
|
||||||
|
})
|
||||||
class AccountResource(Resource):
|
class AccountResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', account_model)
|
||||||
@marshal_with_field(Object(account_model))
|
@marshal_with_field(Object(account_model))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
Get account.
|
Get an account.
|
||||||
"""
|
"""
|
||||||
account = Account.query().get(id)
|
account = Account.query().get(id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note: if we don't pass the code, the result is seen as a tuple and
|
||||||
|
# causes error on marshalling.
|
||||||
return account, 200
|
return account, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.expect(account_model)
|
||||||
|
@api.response(200, 'OK', account_model)
|
||||||
|
@api.response(406, 'Invalid account data')
|
||||||
@marshal_with_field(Object(account_model))
|
@marshal_with_field(Object(account_model))
|
||||||
def post(self, id):
|
def post(self, id):
|
||||||
data = account_parser.parse_args()
|
"""
|
||||||
|
Update an account.
|
||||||
|
"""
|
||||||
|
data = api.payload
|
||||||
|
|
||||||
assert (id not in data or data.id is None
|
# Check ID consistency.
|
||||||
or data.id == id)
|
if 'id' in data and data['id'] and data['id'] != id:
|
||||||
|
api.abort(
|
||||||
|
406,
|
||||||
|
error_message='Id must not be provided or changed on update.'
|
||||||
|
)
|
||||||
|
|
||||||
# Need to get the object to update it.
|
# Need to get the object to update it.
|
||||||
account = Account.query().get(id)
|
account = Account.query().get(id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
# SQLAlchemy objects ignore __dict__.update() with merge.
|
# SQLAlchemy objects ignore __dict__.update() with merge.
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
@ -97,25 +148,37 @@ class AccountResource(Resource):
|
|||||||
return account, 200
|
return account, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(204, 'Account deleted', account_model)
|
||||||
@marshal_with_field(Object(account_model))
|
@marshal_with_field(Object(account_model))
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
|
"""
|
||||||
|
Delete an account.
|
||||||
|
"""
|
||||||
|
|
||||||
# Need to get the object to update it.
|
# Need to get the object to update it.
|
||||||
account = Account.query().get(id)
|
account = Account.query().get(id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
db.session.delete(account)
|
db.session.delete(account)
|
||||||
|
|
||||||
return None, 204
|
return None, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AccountListResource, '/account')
|
@ns.route('/<int:id>/solds')
|
||||||
api.add_resource(AccountResource, '/account/<int:id>')
|
|
||||||
|
|
||||||
|
|
||||||
class SoldsResource(Resource):
|
class SoldsResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
200: ('OK', solds_model),
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Account not found'
|
||||||
|
})
|
||||||
@marshal_with_field(Object(solds_model))
|
@marshal_with_field(Object(solds_model))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
@ -124,15 +187,27 @@ class SoldsResource(Resource):
|
|||||||
account = Account.query().get(id)
|
account = Account.query().get(id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
# Note: if we don't pass the code, the result is seen as a tuple and
|
# Note: if we don't pass the code, the result is seen as a tuple and
|
||||||
# causes error on marshalling.
|
# causes error on marshalling.
|
||||||
return account.solds(), 200
|
return account.solds(), 200
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/<int:id>/balance')
|
||||||
class BalanceResource(Resource):
|
class BalanceResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
200: ('OK', balance_model),
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Account not found'
|
||||||
|
})
|
||||||
|
@api.expect(range_parser)
|
||||||
@marshal_with_field(Object(balance_model))
|
@marshal_with_field(Object(balance_model))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
@ -141,7 +216,10 @@ class BalanceResource(Resource):
|
|||||||
account = Account.query().get(id)
|
account = Account.query().get(id)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
data = range_parser.parse_args()
|
data = range_parser.parse_args()
|
||||||
|
|
||||||
@ -150,25 +228,43 @@ class BalanceResource(Resource):
|
|||||||
return account.balance(**data), 200
|
return account.balance(**data), 200
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route("/<int:id>/category")
|
||||||
class CategoryResource(Resource):
|
class CategoryResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
200: ('OK', [category_model]),
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Account not found'
|
||||||
|
})
|
||||||
|
@api.expect(range_parser)
|
||||||
@marshal_with_field(fields.List(Object(category_model)))
|
@marshal_with_field(fields.List(Object(category_model)))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
|
"""
|
||||||
|
Get account category balances for a specific date range.
|
||||||
|
"""
|
||||||
data = range_parser.parse_args()
|
data = range_parser.parse_args()
|
||||||
|
|
||||||
return Operation.get_categories_for_range(id, **data).all()
|
return Operation.get_categories_for_range(id, **data).all()
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/<int:id>/ohlc')
|
||||||
class OHLCResource(Resource):
|
class OHLCResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
200: ('OK', [ohlc_model]),
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Account not found'
|
||||||
|
})
|
||||||
|
@api.expect(range_parser)
|
||||||
@marshal_with_field(fields.List(Object(ohlc_model)))
|
@marshal_with_field(fields.List(Object(ohlc_model)))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
|
"""
|
||||||
|
Get OHLC data for a specific date range and account.
|
||||||
|
"""
|
||||||
data = range_parser.parse_args()
|
data = range_parser.parse_args()
|
||||||
|
|
||||||
return Operation.get_ohlc_per_day_for_range(id, **data).all()
|
return Operation.get_ohlc_per_day_for_range(id, **data).all()
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(SoldsResource, "/account/<int:id>/solds")
|
|
||||||
api.add_resource(BalanceResource, "/account/<int:id>/balance")
|
|
||||||
api.add_resource(CategoryResource, "/account/<int:id>/category")
|
|
||||||
api.add_resource(OHLCResource, "/account/<int:id>/ohlc")
|
|
||||||
|
@ -14,87 +14,217 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from copy import deepcopy
|
from flask.ext.restplus import fields
|
||||||
|
|
||||||
from flask.ext.restful import fields
|
from .. import api
|
||||||
|
|
||||||
# Account model.
|
# Account model.
|
||||||
account_model = {
|
account_model = api.model('Account', {
|
||||||
'id': fields.Integer(default=None),
|
'id': fields.Integer(
|
||||||
'name': fields.String,
|
default=None,
|
||||||
'authorized_overdraft': fields.Float,
|
readonly=True,
|
||||||
}
|
description='Id of the account'),
|
||||||
|
'name': fields.String(
|
||||||
|
required=True,
|
||||||
|
description='Name of the account'),
|
||||||
|
'authorized_overdraft': fields.Float(
|
||||||
|
required=True,
|
||||||
|
description='Authorized overdraft')
|
||||||
|
})
|
||||||
|
|
||||||
# Account status model.
|
# Account status model.
|
||||||
solds_model = {
|
solds_model = api.model('Solds', {
|
||||||
'current': fields.Float,
|
'current': fields.Float(
|
||||||
'pointed': fields.Float,
|
readonly=True,
|
||||||
'future': fields.Float,
|
description='Current sold of the account'),
|
||||||
}
|
'pointed': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Pointed sold of the account'),
|
||||||
|
'future': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Future sold of the account')
|
||||||
|
})
|
||||||
|
|
||||||
# Account balance model.
|
# Account balance model.
|
||||||
balance_model = {
|
balance_model = api.model('Balance', {
|
||||||
'expenses': fields.Float,
|
'expenses': fields.Float(
|
||||||
'revenues': fields.Float,
|
readonly=True,
|
||||||
'balance': fields.Float,
|
description='Total amount of expenses'),
|
||||||
}
|
'revenues': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Total amount of revenues'),
|
||||||
|
'balance': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Balance'),
|
||||||
|
})
|
||||||
|
|
||||||
# Category with expenses and revenues.
|
# Category with expenses and revenues.
|
||||||
category_model = {
|
category_model = api.model('Category', {
|
||||||
'category': fields.String,
|
'category': fields.String(
|
||||||
'expenses': fields.Float,
|
readonly=True,
|
||||||
'revenues': fields.Float
|
description='Category name'),
|
||||||
}
|
'expenses': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Total amount of expenses for the category'),
|
||||||
|
'revenues': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Total amount of revenues for the category')
|
||||||
|
})
|
||||||
|
|
||||||
# OHLC model.
|
# OHLC model.
|
||||||
ohlc_model = {
|
ohlc_model = api.model('OHLC', {
|
||||||
'operation_date': fields.DateTime(dt_format='iso8601'),
|
'operation_date': fields.DateTime(
|
||||||
'open': fields.Float,
|
dt_format='iso8601',
|
||||||
'high': fields.Float,
|
readonly=True,
|
||||||
'low': fields.Float,
|
required=True,
|
||||||
'close': fields.Float
|
description='Date of the OHLC object'
|
||||||
}
|
),
|
||||||
|
'open': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
required=True,
|
||||||
|
description='Open value'
|
||||||
|
),
|
||||||
|
'high': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
required=True,
|
||||||
|
description='High value'
|
||||||
|
),
|
||||||
|
'low': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
required=True,
|
||||||
|
description='Low value'
|
||||||
|
),
|
||||||
|
'close': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
required=True,
|
||||||
|
description='Close value'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
# Operation with sold model.
|
# Operation with sold model.
|
||||||
operation_model = {
|
operation_model = api.model('Operation', {
|
||||||
'id': fields.Integer(default=None),
|
'id': fields.Integer(
|
||||||
'operation_date': fields.DateTime(dt_format='iso8601'),
|
default=None,
|
||||||
'label': fields.String,
|
readonly=True,
|
||||||
'value': fields.Float,
|
description='Id of the operation'),
|
||||||
'pointed': fields.Boolean,
|
'operation_date': fields.DateTime(
|
||||||
'category': fields.String,
|
dt_format='iso8601',
|
||||||
'account_id': fields.Integer,
|
required=True,
|
||||||
'scheduled_operation_id': fields.Integer(default=None),
|
description='Date of the operation'),
|
||||||
'confirmed': fields.Boolean,
|
'label': fields.String(
|
||||||
'canceled': fields.Boolean,
|
required=True,
|
||||||
}
|
description='Label of the operation'),
|
||||||
|
'value': fields.Float(
|
||||||
|
required=True,
|
||||||
|
description='Value of the operation'),
|
||||||
|
'pointed': fields.Boolean(
|
||||||
|
required=True,
|
||||||
|
description='Pointed status of the operation'),
|
||||||
|
'category': fields.String(
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
description='Category of the operation'),
|
||||||
|
'account_id': fields.Integer(
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
description='Account id of the operation'),
|
||||||
|
'scheduled_operation_id': fields.Integer(
|
||||||
|
default=None,
|
||||||
|
readonly=True,
|
||||||
|
description='Scheduled operation ID of the operation'),
|
||||||
|
'confirmed': fields.Boolean(
|
||||||
|
description='Confirmed status of the operation'),
|
||||||
|
'canceled': fields.Boolean(
|
||||||
|
description='Canceled status of the operation (for a scheduled one)')
|
||||||
|
})
|
||||||
|
|
||||||
operation_with_sold_model = deepcopy(operation_model)
|
operation_with_sold_model = api.extend(
|
||||||
operation_with_sold_model['sold'] = fields.Float
|
'OperationWithSold', operation_model, {
|
||||||
|
'sold': fields.Float(
|
||||||
|
readonly=True,
|
||||||
|
description='Cumulated sold'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Scheduled operation model.
|
# Scheduled operation model.
|
||||||
scheduled_operation_model = {
|
scheduled_operation_model = api.model('ScheduledOperation', {
|
||||||
'id': fields.Integer,
|
'id': fields.Integer(
|
||||||
'start_date': fields.DateTime(dt_format='iso8601'),
|
description='Id of the scheduled operation',
|
||||||
'stop_date': fields.DateTime(dt_format='iso8601'),
|
readonly=True,
|
||||||
'day': fields.Integer,
|
default=None),
|
||||||
'frequency': fields.Integer,
|
'start_date': fields.DateTime(
|
||||||
'label': fields.String,
|
dt_format='iso8601',
|
||||||
'value': fields.Float,
|
required=True,
|
||||||
'category': fields.String,
|
description='Start date of the scheduled operation'),
|
||||||
'account_id': fields.Integer,
|
'stop_date': fields.DateTime(
|
||||||
}
|
dt_format='iso8601',
|
||||||
|
required=True,
|
||||||
|
description='End date of the scheduled operation'),
|
||||||
|
'day': fields.Integer(
|
||||||
|
required=True,
|
||||||
|
description='Day of month for the scheduled operation'),
|
||||||
|
'frequency': fields.Integer(
|
||||||
|
required=True,
|
||||||
|
description='Frequency of the scheduling in months'),
|
||||||
|
'label': fields.String(
|
||||||
|
required=True,
|
||||||
|
description='Label of the generated operations'),
|
||||||
|
'value': fields.Float(
|
||||||
|
required=True,
|
||||||
|
description='Value of the generated operations'),
|
||||||
|
'category': fields.String(
|
||||||
|
required=False,
|
||||||
|
description='Category of the generated operations'),
|
||||||
|
'account_id': fields.Integer(
|
||||||
|
default=None,
|
||||||
|
readonly=True,
|
||||||
|
required=True,
|
||||||
|
description='Account id of the scheduled operation'),
|
||||||
|
})
|
||||||
|
|
||||||
# Token with expiration time and type.
|
# Token with expiration time and type.
|
||||||
token_model = {
|
token_model = api.model('Token', {
|
||||||
'token': fields.String,
|
'token': fields.String(
|
||||||
'expiration': fields.DateTime(dt_format='iso8601'),
|
required=True,
|
||||||
'token_type': fields.String
|
readonly=True,
|
||||||
}
|
description='Token value'),
|
||||||
|
'expiration': fields.DateTime(
|
||||||
|
dt_format='iso8601',
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
description='Expiration time of the token'),
|
||||||
|
'token_type': fields.String(
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
description='Token type')
|
||||||
|
})
|
||||||
|
|
||||||
# User model.
|
# User model.
|
||||||
user_model = {
|
user_model = api.model('User', {
|
||||||
'id': fields.Integer(default=None),
|
'id': fields.Integer(
|
||||||
'email': fields.String,
|
default=None,
|
||||||
'active': fields.Boolean
|
required=True,
|
||||||
}
|
readonly=True,
|
||||||
|
description='Id of the user'),
|
||||||
|
'email': fields.String(
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
decription='Email address of the user'),
|
||||||
|
'active': fields.Boolean(
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
description='Active state of the user')
|
||||||
|
})
|
||||||
|
|
||||||
|
# Login model.
|
||||||
|
login_model = api.model('Login', {
|
||||||
|
'email': fields.String(
|
||||||
|
required=True,
|
||||||
|
description='Email to use for login'
|
||||||
|
),
|
||||||
|
'password': fields.String(
|
||||||
|
required=True,
|
||||||
|
description='Plain text password to use for login'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@ -14,25 +14,40 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from flask.ext.restful import Resource, fields, marshal_with_field
|
from flask.ext.restplus import Resource, fields, marshal_with_field
|
||||||
|
|
||||||
from accountant import db
|
from accountant import db
|
||||||
|
|
||||||
from .. import api
|
from .. import api
|
||||||
|
|
||||||
|
from ..models.accounts import Account
|
||||||
from ..models.operations import Operation
|
from ..models.operations import Operation
|
||||||
|
|
||||||
from .models import operation_model, operation_with_sold_model
|
from .models import operation_model, operation_with_sold_model
|
||||||
from .parsers import operation_parser, account_range_parser
|
from .parsers import account_range_parser
|
||||||
from .users import requires_auth
|
from .users import requires_auth
|
||||||
|
|
||||||
from ..fields import Object
|
from ..fields import Object
|
||||||
|
|
||||||
|
|
||||||
|
ns = api.namespace('operation', description='Operation management')
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/')
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
401: 'Unauthorized'
|
||||||
|
})
|
||||||
class OperationListResource(Resource):
|
class OperationListResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', [operation_with_sold_model])
|
||||||
|
@api.expect(parser=account_range_parser)
|
||||||
@marshal_with_field(fields.List(Object(operation_with_sold_model)))
|
@marshal_with_field(fields.List(Object(operation_with_sold_model)))
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get operations with solds for a specific account.
|
||||||
|
"""
|
||||||
data = account_range_parser.parse_args()
|
data = account_range_parser.parse_args()
|
||||||
|
|
||||||
return Operation.query(
|
return Operation.query(
|
||||||
@ -43,9 +58,31 @@ class OperationListResource(Resource):
|
|||||||
).all(), 200
|
).all(), 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(201, 'Operation created', operation_model)
|
||||||
|
@api.response(404, 'Account not found')
|
||||||
|
@api.response(406, 'Invalid operation data')
|
||||||
@marshal_with_field(Object(operation_model))
|
@marshal_with_field(Object(operation_model))
|
||||||
def post(self):
|
def post(self):
|
||||||
data = operation_parser.parse_args()
|
"""
|
||||||
|
Create a new operation.
|
||||||
|
"""
|
||||||
|
data = api.payload
|
||||||
|
|
||||||
|
account_id = data['account_id']
|
||||||
|
account = Account.query().get(account_id)
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % account_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# A new operation MUST NOT have an id;
|
||||||
|
if 'id' in data and data['id']:
|
||||||
|
api.abort(
|
||||||
|
406,
|
||||||
|
error_message='Id must not be provided on creation.'
|
||||||
|
)
|
||||||
|
|
||||||
operation = Operation(**data)
|
operation = Operation(**data)
|
||||||
|
|
||||||
@ -54,8 +91,19 @@ class OperationListResource(Resource):
|
|||||||
return operation, 201
|
return operation, 201
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/<int:id>')
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
params={
|
||||||
|
'id': 'Id of the operation to manage'
|
||||||
|
},
|
||||||
|
responses={
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Operation not found'
|
||||||
|
})
|
||||||
class OperationResource(Resource):
|
class OperationResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', operation_model)
|
||||||
@marshal_with_field(Object(operation_model))
|
@marshal_with_field(Object(operation_model))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
@ -64,22 +112,37 @@ class OperationResource(Resource):
|
|||||||
operation = db.session.query(Operation).get(id)
|
operation = db.session.query(Operation).get(id)
|
||||||
|
|
||||||
if not operation:
|
if not operation:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Operation with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
return operation, 200
|
return operation, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.expect(operation_model)
|
||||||
|
@api.response(200, 'OK', operation_model)
|
||||||
|
@api.response(406, 'Invalid operation data')
|
||||||
@marshal_with_field(Object(operation_model))
|
@marshal_with_field(Object(operation_model))
|
||||||
def post(self, id):
|
def post(self, id):
|
||||||
data = operation_parser.parse_args()
|
data = api.payload
|
||||||
|
|
||||||
assert (id not in data or data.id is None
|
# Check ID consistency.
|
||||||
or data.id == id)
|
if 'id' in data and data['id'] and data['id'] != id:
|
||||||
|
api.abort(
|
||||||
|
406,
|
||||||
|
error_message='Id must not be provided or changed on update.'
|
||||||
|
)
|
||||||
|
|
||||||
operation = db.session.query(Operation).get(id)
|
operation = db.session.query(Operation).get(id)
|
||||||
|
|
||||||
if not operation:
|
if not operation:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Operation with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME check account_id consistency.
|
||||||
|
|
||||||
# SQLAlchemy objects ignore __dict__.update() with merge.
|
# SQLAlchemy objects ignore __dict__.update() with merge.
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
@ -90,17 +153,17 @@ class OperationResource(Resource):
|
|||||||
return operation, 200
|
return operation, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(204, 'Operation deleted', operation_model)
|
||||||
@marshal_with_field(Object(operation_model))
|
@marshal_with_field(Object(operation_model))
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
operation = db.session.query(Operation).get(id)
|
operation = db.session.query(Operation).get(id)
|
||||||
|
|
||||||
if not operation:
|
if not operation:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Operation with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
db.session.delete(operation)
|
db.session.delete(operation)
|
||||||
|
|
||||||
return None, 204
|
return None, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(OperationListResource, "/operation")
|
|
||||||
api.add_resource(OperationResource, "/operation/<int:id>")
|
|
||||||
|
@ -19,61 +19,39 @@ import dateutil.parser
|
|||||||
|
|
||||||
from flask.ext.restful import reqparse
|
from flask.ext.restful import reqparse
|
||||||
|
|
||||||
|
from .. import api
|
||||||
|
|
||||||
# Parser for a date range.
|
# Parser for a date range.
|
||||||
range_parser = reqparse.RequestParser()
|
range_parser = api.parser()
|
||||||
range_parser.add_argument(
|
range_parser.add_argument(
|
||||||
'begin',
|
'begin',
|
||||||
type=lambda a: dateutil.parser.parse(a) if a else None
|
type=lambda a: dateutil.parser.parse(a) if a else None,
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
location='args',
|
||||||
|
help='Begin date of the time period'
|
||||||
)
|
)
|
||||||
range_parser.add_argument(
|
range_parser.add_argument(
|
||||||
'end',
|
'end',
|
||||||
type=lambda a: dateutil.parser.parse(a) if a else None
|
type=lambda a: dateutil.parser.parse(a) if a else None,
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
location='args',
|
||||||
|
help='End date of the time period'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parser for an account id.
|
# Parser for an account id.
|
||||||
account_id_parser = reqparse.RequestParser()
|
account_id_parser = reqparse.RequestParser()
|
||||||
account_id_parser.add_argument('account_id', type=int)
|
account_id_parser.add_argument(
|
||||||
|
'account_id',
|
||||||
|
type=int,
|
||||||
|
required=True,
|
||||||
|
location='args',
|
||||||
|
help='Id of the account'
|
||||||
|
)
|
||||||
|
|
||||||
# Parser for a date range and an account id.
|
# Parser for a date range and an account id.
|
||||||
account_range_parser = range_parser.copy()
|
account_range_parser = range_parser.copy()
|
||||||
account_range_parser.add_argument(
|
account_range_parser.add_argument(
|
||||||
deepcopy(account_id_parser.args[0])
|
deepcopy(account_id_parser.args[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parser for an account.
|
|
||||||
account_parser = reqparse.RequestParser()
|
|
||||||
account_parser.add_argument('name', type=str, required=True)
|
|
||||||
account_parser.add_argument('authorized_overdraft', type=float, required=True)
|
|
||||||
|
|
||||||
# Parser for an operation.
|
|
||||||
operation_parser = reqparse.RequestParser()
|
|
||||||
# Must use lambda because the parser passes other parameters badly interpreted
|
|
||||||
# by dateutil.parser.parse
|
|
||||||
operation_parser.add_argument(
|
|
||||||
'operation_date', type=lambda a: dateutil.parser.parse(a))
|
|
||||||
operation_parser.add_argument('label', type=str)
|
|
||||||
operation_parser.add_argument('value', type=float)
|
|
||||||
operation_parser.add_argument('pointed', type=bool)
|
|
||||||
operation_parser.add_argument('category', type=str)
|
|
||||||
operation_parser.add_argument('account_id', type=int)
|
|
||||||
operation_parser.add_argument('scheduled_operation_id', type=int)
|
|
||||||
operation_parser.add_argument('confirmed', type=bool)
|
|
||||||
operation_parser.add_argument('canceled', type=bool)
|
|
||||||
|
|
||||||
# Parser for a scheduled operation.
|
|
||||||
scheduled_operation_parser = reqparse.RequestParser()
|
|
||||||
scheduled_operation_parser.add_argument(
|
|
||||||
'start_date', type=lambda a: dateutil.parser.parse(a))
|
|
||||||
scheduled_operation_parser.add_argument(
|
|
||||||
'stop_date', type=lambda a: dateutil.parser.parse(a))
|
|
||||||
scheduled_operation_parser.add_argument('day', type=int)
|
|
||||||
scheduled_operation_parser.add_argument('frequency', type=int)
|
|
||||||
scheduled_operation_parser.add_argument('label', type=str)
|
|
||||||
scheduled_operation_parser.add_argument('value', type=float)
|
|
||||||
scheduled_operation_parser.add_argument('category', type=str)
|
|
||||||
scheduled_operation_parser.add_argument('account_id', type=int)
|
|
||||||
|
|
||||||
# Parser for a login.
|
|
||||||
login_parser = reqparse.RequestParser()
|
|
||||||
login_parser.add_argument('email', type=str, required=True)
|
|
||||||
login_parser.add_argument('password', type=str, required=True)
|
|
||||||
|
@ -14,26 +14,41 @@
|
|||||||
You should have received a copy of the GNU Affero General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
from flask.ext.restful import Resource, fields, marshal_with_field
|
from flask.ext.restplus import Resource, fields, marshal_with_field
|
||||||
|
|
||||||
from sqlalchemy import true
|
from sqlalchemy import true
|
||||||
|
|
||||||
from accountant import db
|
from accountant import db
|
||||||
|
|
||||||
from ..models.scheduled_operations import ScheduledOperation
|
|
||||||
from ..models.operations import Operation
|
|
||||||
|
|
||||||
from .. import api
|
from .. import api
|
||||||
|
|
||||||
|
from ..models.accounts import Account
|
||||||
|
from ..models.operations import Operation
|
||||||
|
from ..models.scheduled_operations import ScheduledOperation
|
||||||
|
|
||||||
from .models import scheduled_operation_model
|
from .models import scheduled_operation_model
|
||||||
from .parsers import account_id_parser, scheduled_operation_parser
|
from .parsers import account_id_parser
|
||||||
from .users import requires_auth
|
from .users import requires_auth
|
||||||
|
|
||||||
from ..fields import Object
|
from ..fields import Object
|
||||||
|
|
||||||
|
|
||||||
|
ns = api.namespace(
|
||||||
|
'scheduled_operation',
|
||||||
|
description='Scheduled operation management'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/')
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
401: 'Unauthorized',
|
||||||
|
})
|
||||||
class ScheduledOperationListResource(Resource):
|
class ScheduledOperationListResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.expect(account_id_parser)
|
||||||
|
@api.response(200, 'OK', [scheduled_operation_model])
|
||||||
@marshal_with_field(fields.List(Object(scheduled_operation_model)))
|
@marshal_with_field(fields.List(Object(scheduled_operation_model)))
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
@ -44,12 +59,32 @@ class ScheduledOperationListResource(Resource):
|
|||||||
return ScheduledOperation.query().filter_by(**data).all(), 200
|
return ScheduledOperation.query().filter_by(**data).all(), 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.expect(scheduled_operation_model)
|
||||||
|
@api.response(200, 'OK', scheduled_operation_model)
|
||||||
|
@api.response(404, 'Account not found')
|
||||||
|
@api.response(406, 'Invalid operation data')
|
||||||
@marshal_with_field(Object(scheduled_operation_model))
|
@marshal_with_field(Object(scheduled_operation_model))
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Add a new scheduled operation.
|
Add a new scheduled operation.
|
||||||
"""
|
"""
|
||||||
data = scheduled_operation_parser.parse_args()
|
data = api.payload
|
||||||
|
|
||||||
|
account_id = data['account_id']
|
||||||
|
account = Account.query().get(account_id)
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Account with id %d not found.' % account_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# A new scheduled operation MUST NOT have an id;
|
||||||
|
if 'id' in data and data['id']:
|
||||||
|
api.abort(
|
||||||
|
406,
|
||||||
|
error_message='Id must not be provided on creation.'
|
||||||
|
)
|
||||||
|
|
||||||
scheduled_operation = ScheduledOperation(**data)
|
scheduled_operation = ScheduledOperation(**data)
|
||||||
|
|
||||||
@ -62,8 +97,19 @@ class ScheduledOperationListResource(Resource):
|
|||||||
return scheduled_operation, 201
|
return scheduled_operation, 201
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/<int:id>')
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
params={
|
||||||
|
'id': 'Id of the scheduled operation to manage'
|
||||||
|
},
|
||||||
|
responses={
|
||||||
|
401: 'Unauthorized',
|
||||||
|
404: 'Scheduled operation not found'
|
||||||
|
})
|
||||||
class ScheduledOperationResource(Resource):
|
class ScheduledOperationResource(Resource):
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', scheduled_operation_model)
|
||||||
@marshal_with_field(Object(scheduled_operation_model))
|
@marshal_with_field(Object(scheduled_operation_model))
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
@ -72,25 +118,40 @@ class ScheduledOperationResource(Resource):
|
|||||||
scheduled_operation = ScheduledOperation.query().get(id)
|
scheduled_operation = ScheduledOperation.query().get(id)
|
||||||
|
|
||||||
if not scheduled_operation:
|
if not scheduled_operation:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Scheduled operation with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
return scheduled_operation, 200
|
return scheduled_operation, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', scheduled_operation_model)
|
||||||
|
@api.response(406, 'Invalid scheduled operation data')
|
||||||
|
@api.expect(scheduled_operation_model)
|
||||||
@marshal_with_field(Object(scheduled_operation_model))
|
@marshal_with_field(Object(scheduled_operation_model))
|
||||||
def post(self, id):
|
def post(self, id):
|
||||||
"""
|
"""
|
||||||
Update a scheduled operation.
|
Update a scheduled operation.
|
||||||
"""
|
"""
|
||||||
data = scheduled_operation_parser.parse_args()
|
data = api.payload
|
||||||
|
|
||||||
assert (id not in data or data.id is None
|
# Check ID consistency.
|
||||||
or data.id == id)
|
if 'id' in data and data['id'] and data['id'] != id:
|
||||||
|
api.abort(
|
||||||
|
406,
|
||||||
|
error_message='Id must not be provided or changed on update.'
|
||||||
|
)
|
||||||
|
|
||||||
scheduled_operation = ScheduledOperation.query().get(id)
|
scheduled_operation = ScheduledOperation.query().get(id)
|
||||||
|
|
||||||
if not scheduled_operation:
|
if not scheduled_operation:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Scheduled operation with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
|
# FIXME check account_id consistency.
|
||||||
|
|
||||||
# SQLAlchemy objects ignore __dict__.update() with merge.
|
# SQLAlchemy objects ignore __dict__.update() with merge.
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
@ -105,6 +166,8 @@ class ScheduledOperationResource(Resource):
|
|||||||
return scheduled_operation, 200
|
return scheduled_operation, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.response(200, 'OK', scheduled_operation_model)
|
||||||
|
@api.response(409, 'Cannot be deleted')
|
||||||
@marshal_with_field(Object(scheduled_operation_model))
|
@marshal_with_field(Object(scheduled_operation_model))
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
"""
|
"""
|
||||||
@ -113,24 +176,24 @@ class ScheduledOperationResource(Resource):
|
|||||||
scheduled_operation = ScheduledOperation.query().get(id)
|
scheduled_operation = ScheduledOperation.query().get(id)
|
||||||
|
|
||||||
if not scheduled_operation:
|
if not scheduled_operation:
|
||||||
return None, 404
|
api.abort(
|
||||||
|
404,
|
||||||
|
error_message='Scheduled operation with id %d not found.' % id
|
||||||
|
)
|
||||||
|
|
||||||
operations = scheduled_operation.operations.filter(
|
operations = scheduled_operation.operations.filter(
|
||||||
Operation.confirmed == true()
|
Operation.confirmed == true()
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
if operations:
|
if operations:
|
||||||
return "There are still confirmed operations associated to this \
|
api.abort(
|
||||||
scheduled operation.", 409
|
409,
|
||||||
|
error_message='There are still confirmed operations \
|
||||||
|
associated to this scheduled operation.')
|
||||||
|
|
||||||
# Delete unconfirmed operations
|
# Delete unconfirmed operations
|
||||||
operations = scheduled_operation.operations.delete()
|
scheduled_operation.operations.delete()
|
||||||
|
|
||||||
db.session.delete(scheduled_operation)
|
db.session.delete(scheduled_operation)
|
||||||
|
|
||||||
return None, 204
|
return None, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ScheduledOperationListResource, "/scheduled_operation")
|
|
||||||
api.add_resource(ScheduledOperationResource,
|
|
||||||
"/scheduled_operation/<int:id>")
|
|
||||||
|
@ -20,7 +20,7 @@ import arrow
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from flask import request, g
|
from flask import request, g
|
||||||
from flask.ext.restful import Resource, marshal_with, marshal_with_field
|
from flask.ext.restplus import Resource, marshal_with_field
|
||||||
|
|
||||||
from accountant import app
|
from accountant import app
|
||||||
|
|
||||||
@ -30,8 +30,7 @@ from ..fields import Object
|
|||||||
|
|
||||||
from ..models.users import User
|
from ..models.users import User
|
||||||
|
|
||||||
from .models import token_model, user_model
|
from .models import user_model, token_model, login_model
|
||||||
from .parsers import login_parser
|
|
||||||
|
|
||||||
|
|
||||||
def load_user_from_token(token):
|
def load_user_from_token(token):
|
||||||
@ -43,10 +42,6 @@ def load_user_from_auth(auth):
|
|||||||
return load_user_from_token(token)
|
return load_user_from_token(token)
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
|
||||||
return {'error': 'Please login before executing this request.'}, 401
|
|
||||||
|
|
||||||
|
|
||||||
def requires_auth(f):
|
def requires_auth(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapped(*args, **data):
|
def wrapped(*args, **data):
|
||||||
@ -60,24 +55,37 @@ def requires_auth(f):
|
|||||||
g.user = user
|
g.user = user
|
||||||
return f(*args, **data)
|
return f(*args, **data)
|
||||||
|
|
||||||
return authenticate()
|
api.abort(
|
||||||
|
401,
|
||||||
|
error_message='Please login before executing this request.'
|
||||||
|
)
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
ns = api.namespace('user', description='User management')
|
||||||
|
|
||||||
|
|
||||||
|
@ns.route('/login')
|
||||||
class LoginResource(Resource):
|
class LoginResource(Resource):
|
||||||
@marshal_with(token_model)
|
@api.marshal_with(token_model)
|
||||||
|
@api.doc(
|
||||||
|
responses={
|
||||||
|
200: ('OK', token_model),
|
||||||
|
401: 'Unauthorized'
|
||||||
|
})
|
||||||
|
@api.expect(login_model)
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Login to retrieve authentication token.
|
Login to retrieve authentication token.
|
||||||
"""
|
"""
|
||||||
data = login_parser.parse_args()
|
data = api.payload
|
||||||
|
|
||||||
user = User.query().filter(
|
user = User.query().filter(
|
||||||
User.email == data['email']
|
User.email == data['email']
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
|
||||||
if not user or not user.verify_password(data['password']):
|
if not user or not user.verify_password(data['password']):
|
||||||
authenticate()
|
api.abort(401, error_message="Bad user or password.")
|
||||||
|
|
||||||
token = user.generate_auth_token()
|
token = user.generate_auth_token()
|
||||||
expiration_time = arrow.now().replace(
|
expiration_time = arrow.now().replace(
|
||||||
@ -91,9 +99,14 @@ class LoginResource(Resource):
|
|||||||
}, 200
|
}, 200
|
||||||
|
|
||||||
@requires_auth
|
@requires_auth
|
||||||
|
@api.doc(
|
||||||
|
security='apikey',
|
||||||
|
responses={
|
||||||
|
200: ('OK', user_model)
|
||||||
|
})
|
||||||
@marshal_with_field(Object(user_model))
|
@marshal_with_field(Object(user_model))
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get authenticated user information.
|
||||||
|
"""
|
||||||
return g.user, 200
|
return g.user, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(LoginResource, "/user/login")
|
|
||||||
|
@ -5,7 +5,7 @@ Flask-Assets==0.11
|
|||||||
Flask-Bower==1.2.1
|
Flask-Bower==1.2.1
|
||||||
Flask-Login==0.2.11
|
Flask-Login==0.2.11
|
||||||
Flask-Migrate==1.5.1
|
Flask-Migrate==1.5.1
|
||||||
Flask-RESTful==0.3.4
|
flask-restplus=0.8.6
|
||||||
flask-cors=2.1.2
|
flask-cors=2.1.2
|
||||||
Flask-Script==2.0.5
|
Flask-Script==2.0.5
|
||||||
Flask-SQLAlchemy==2.0
|
Flask-SQLAlchemy==2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user