From 0b20a68edcb5cfe723e7fa8122193c4d46daa55f Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 10 May 2016 22:18:26 +0100 Subject: [PATCH 01/29] Created skeletal template for bucketlist api --- .gitignore | 3 +++ app/__init__.py | 8 ++++++++ app/models.py | 8 ++++++++ app/resource.py | 29 +++++++++++++++++++++++++++++ app/views.py | 14 ++++++++++++++ config.py | 6 ++++++ run.py | 3 +++ 7 files changed, 71 insertions(+) create mode 100644 .gitignore create mode 100644 app/__init__.py create mode 100644 app/models.py create mode 100644 app/resource.py create mode 100644 app/views.py create mode 100644 config.py create mode 100644 run.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..903b579 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*/*.pyc +*.db diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b0b5a3a --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,8 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config.from_object('config') +db = SQLAlchemy(app) + +from app import views \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..0978c70 --- /dev/null +++ b/app/models.py @@ -0,0 +1,8 @@ +from app import db + +class BucketListModel(db.Model): + id = db.Column(db.Integer(), primary_key=True) + name = db.Column(db.String(255), unique=True) + + def __init__(self): + print 'here' diff --git a/app/resource.py b/app/resource.py new file mode 100644 index 0000000..f8850f8 --- /dev/null +++ b/app/resource.py @@ -0,0 +1,29 @@ +from flask_restful import reqparse, abort, Resource +from app.models import * + + +parser = reqparse.RequestParser() +parser.add_argument('task') + + +class BucketList(Resource): + def get(self, id): + return {'id':id} + + def delete(self, id): + return '', 204 + + def put(self, id): + #args = parser.parse_args() + #task = {'task': args['task']} + return {}, 201 + + +class BucketLists(Resource): + def get(self): + return [] + + def post(self): + #args = parser.parse_args() + #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 + return {}, 201 diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..e2d80ab --- /dev/null +++ b/app/views.py @@ -0,0 +1,14 @@ +from app import app +from flask import jsonify, abort, make_response, request, url_for +from flask_restful import Api +from app.resource import * + +api = Api(app) + +api.add_resource(BucketLists, '/api/v1.0/bucketlists') +api.add_resource(BucketList, '/api/v1.0/bucketlists/') + +@app.route('/') +@app.route('/index') +def index(): + return "Hello, BucketList!" diff --git a/config.py b/config.py new file mode 100644 index 0000000..ca33db6 --- /dev/null +++ b/config.py @@ -0,0 +1,6 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') +#SQLALCHEMY_DATABASE_URI = 'postgresql://sunday:@localhost/bucketlist' +SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'migrations') \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..1f0b926 --- /dev/null +++ b/run.py @@ -0,0 +1,3 @@ +from app import app + +app.run(debug=True) \ No newline at end of file From 82f41389f33edceefbc63c4f1f72c676863c7bd0 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 10 May 2016 22:25:43 +0100 Subject: [PATCH 02/29] Added placeholder class for bucketlist items --- app/resource.py | 23 +++++++++++++++++++++++ app/views.py | 2 ++ 2 files changed, 25 insertions(+) diff --git a/app/resource.py b/app/resource.py index f8850f8..42db004 100644 --- a/app/resource.py +++ b/app/resource.py @@ -27,3 +27,26 @@ def post(self): #args = parser.parse_args() #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 return {}, 201 + + +class BucketListItem(Resource): + def get(self, id, item_id): + return {'bucketlist_id':id, 'item_id':item_id} + + def delete(self, id, item_id): + return '', 204 + + def put(self, id, item_id): + #args = parser.parse_args() + #task = {'task': args['task']} + return {}, 201 + + +class BucketListItems(Resource): + def get(self, id): + return [] + + def post(self, id): + #args = parser.parse_args() + #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 + return {}, 201 diff --git a/app/views.py b/app/views.py index e2d80ab..93592d4 100644 --- a/app/views.py +++ b/app/views.py @@ -7,6 +7,8 @@ api.add_resource(BucketLists, '/api/v1.0/bucketlists') api.add_resource(BucketList, '/api/v1.0/bucketlists/') +api.add_resource(BucketListItems, '/api/v1.0/bucketlists//items') +api.add_resource(BucketListItem, '/api/v1.0/bucketlists//items/') @app.route('/') @app.route('/index') From 6c525e5a521a6fc4ebf9dabd6433994f8f7fc7e5 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 10 May 2016 23:27:50 +0100 Subject: [PATCH 03/29] Added User and BucketListItem Model --- app/models.py | 31 +++++++++++++++++++++++++++++-- app/views.py | 10 +++++----- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/models.py b/app/models.py index 0978c70..e9db122 100644 --- a/app/models.py +++ b/app/models.py @@ -4,5 +4,32 @@ class BucketListModel(db.Model): id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), unique=True) - def __init__(self): - print 'here' + def __init__(self, name): + self.name = name + + def __repr__(): + return '' % self.name + + +class BucketListItemModel(db.Model): + id = db.Column(db.Integer(), primary_key=True) + task = db.Column(db.String(255)) + + def __init__(self, task): + self.task = task + + def __repr__(): + return '' % self.name + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(100), unique=True) + email = db.Column(db.String(200), unique=True) + + def __init__(self, username, email): + self.username = username + self.email = email + + def __repr__(self): + return '' % self.username \ No newline at end of file diff --git a/app/views.py b/app/views.py index 93592d4..156f9db 100644 --- a/app/views.py +++ b/app/views.py @@ -3,12 +3,12 @@ from flask_restful import Api from app.resource import * -api = Api(app) +api = Api(app, '/api/v1.0') -api.add_resource(BucketLists, '/api/v1.0/bucketlists') -api.add_resource(BucketList, '/api/v1.0/bucketlists/') -api.add_resource(BucketListItems, '/api/v1.0/bucketlists//items') -api.add_resource(BucketListItem, '/api/v1.0/bucketlists//items/') +api.add_resource(BucketLists, '/bucketlists') +api.add_resource(BucketList, '/bucketlists/') +api.add_resource(BucketListItems, '/bucketlists//items') +api.add_resource(BucketListItem, '/bucketlists//items/') @app.route('/') @app.route('/index') From 50bed8847e720d7e54a539d61f3707f53699b0ea Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 10 May 2016 23:48:07 +0100 Subject: [PATCH 04/29] Added more columns and relations to models --- app/models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/models.py b/app/models.py index e9db122..2303546 100644 --- a/app/models.py +++ b/app/models.py @@ -1,8 +1,13 @@ from app import db +from datetime import datetime class BucketListModel(db.Model): id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), unique=True) + items = db.relationship('BucketListItemModel', backref='bucketlist', lazy='dynamic') + created_by = db.Column(db.Integer, db.ForeignKey('user.id')) + date_created = db.Column(db.DateTime, default=datetime.utcnow) + date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) def __init__(self, name): self.name = name @@ -14,6 +19,10 @@ def __repr__(): class BucketListItemModel(db.Model): id = db.Column(db.Integer(), primary_key=True) task = db.Column(db.String(255)) + done = db.Column(db.Boolean(), default=False) + date_created = db.Column(db.DateTime, default=datetime.utcnow) + date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) + bucketlist_id = db.Column(db.Integer, db.ForeignKey('bucketlist.id')) def __init__(self, task): self.task = task @@ -26,6 +35,7 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True) email = db.Column(db.String(200), unique=True) + bucketlists = db.relationship('BucketList', backref='user') def __init__(self, username, email): self.username = username From 08c9bf692a98799ac92006b941ee418b82755314 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Wed, 11 May 2016 09:31:04 +0100 Subject: [PATCH 05/29] Completed migration script --- app/models.py | 8 ++- migrations/README | 1 + migrations/alembic.ini | 45 ++++++++++++++ migrations/env.py | 87 ++++++++++++++++++++++++++++ migrations/script.py.mako | 22 +++++++ migrations/versions/d26feb2b3fde_.py | 55 ++++++++++++++++++ script.py | 11 ++++ 7 files changed, 226 insertions(+), 3 deletions(-) create mode 100755 migrations/README create mode 100644 migrations/alembic.ini create mode 100755 migrations/env.py create mode 100755 migrations/script.py.mako create mode 100644 migrations/versions/d26feb2b3fde_.py create mode 100755 script.py diff --git a/app/models.py b/app/models.py index 2303546..19196ec 100644 --- a/app/models.py +++ b/app/models.py @@ -4,10 +4,11 @@ class BucketListModel(db.Model): id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), unique=True) - items = db.relationship('BucketListItemModel', backref='bucketlist', lazy='dynamic') + #items = db.relationship('BucketListItemModel', backref='bucketlist') created_by = db.Column(db.Integer, db.ForeignKey('user.id')) date_created = db.Column(db.DateTime, default=datetime.utcnow) date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) + user = db.relationship('User', backref=db.backref('bucketlists', lazy='dynamic')) def __init__(self, name): self.name = name @@ -22,7 +23,8 @@ class BucketListItemModel(db.Model): done = db.Column(db.Boolean(), default=False) date_created = db.Column(db.DateTime, default=datetime.utcnow) date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) - bucketlist_id = db.Column(db.Integer, db.ForeignKey('bucketlist.id')) + bucketlist_id = db.Column(db.Integer, db.ForeignKey('bucket_list_model.id')) + bucketlist = db.relationship('BucketListModel', backref=db.backref('items', lazy='dynamic')) def __init__(self, task): self.task = task @@ -35,7 +37,7 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True) email = db.Column(db.String(200), unique=True) - bucketlists = db.relationship('BucketList', backref='user') + #bucketlists = db.relationship('bucketlistmodel', backref='user') def __init__(self, username, email): self.username = username diff --git a/migrations/README b/migrations/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100755 index 0000000..4593816 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,87 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.readthedocs.org/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100755 index 0000000..9570201 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,22 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/d26feb2b3fde_.py b/migrations/versions/d26feb2b3fde_.py new file mode 100644 index 0000000..8f31bee --- /dev/null +++ b/migrations/versions/d26feb2b3fde_.py @@ -0,0 +1,55 @@ +"""empty message + +Revision ID: d26feb2b3fde +Revises: None +Create Date: 2016-05-11 09:23:43.012470 + +""" + +# revision identifiers, used by Alembic. +revision = 'd26feb2b3fde' +down_revision = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=100), nullable=True), + sa.Column('email', sa.String(length=200), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_table('bucket_list_model', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('date_modified', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('bucket_list_item_model', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('task', sa.String(length=255), nullable=True), + sa.Column('done', sa.Boolean(), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('date_modified', sa.DateTime(), nullable=True), + sa.Column('bucketlist_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['bucketlist_id'], ['bucket_list_model.id'], ), + sa.PrimaryKeyConstraint('id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('bucket_list_item_model') + op.drop_table('bucket_list_model') + op.drop_table('user') + ### end Alembic commands ### diff --git a/script.py b/script.py new file mode 100755 index 0000000..95a6702 --- /dev/null +++ b/script.py @@ -0,0 +1,11 @@ +from app import app, db +from flask_script import Manager +from flask_migrate import Migrate, MigrateCommand +from app.models import * + +migrate = Migrate(app, db) +manager = Manager(app) +manager.add_command('db', MigrateCommand) + +if __name__ == '__main__': + manager.run() \ No newline at end of file From 8e9883b34193100d1c2ff7f3d8ddc34777b8f25c Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 12 May 2016 16:18:49 +0100 Subject: [PATCH 06/29] Added a helper file and test placeholder for resource --- app/helper.py | 4 ++++ app/models.py | 24 +++++++++++++++++++----- app/resource.py | 4 +++- migrations/versions/02e15b7e0013_.py | 28 ++++++++++++++++++++++++++++ tests/test_resource.py | 8 ++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 app/helper.py create mode 100644 migrations/versions/02e15b7e0013_.py create mode 100644 tests/test_resource.py diff --git a/app/helper.py b/app/helper.py new file mode 100644 index 0000000..4850ea0 --- /dev/null +++ b/app/helper.py @@ -0,0 +1,4 @@ +from app import db + +def save(): + print 'called save' \ No newline at end of file diff --git a/app/models.py b/app/models.py index 19196ec..eb532d8 100644 --- a/app/models.py +++ b/app/models.py @@ -6,28 +6,41 @@ class BucketListModel(db.Model): name = db.Column(db.String(255), unique=True) #items = db.relationship('BucketListItemModel', backref='bucketlist') created_by = db.Column(db.Integer, db.ForeignKey('user.id')) - date_created = db.Column(db.DateTime, default=datetime.utcnow) + date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) user = db.relationship('User', backref=db.backref('bucketlists', lazy='dynamic')) - def __init__(self, name): + def __init__(self, name, user): self.name = name + self.user = user + + def get(self): + return { + 'id':self.id, + 'name':self.name, + 'created_by':self.user.username, + 'date_created':str(self.date_created), + 'date_modified':str(self.date_modified), + } def __repr__(): return '' % self.name + + class BucketListItemModel(db.Model): id = db.Column(db.Integer(), primary_key=True) task = db.Column(db.String(255)) done = db.Column(db.Boolean(), default=False) - date_created = db.Column(db.DateTime, default=datetime.utcnow) + date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) bucketlist_id = db.Column(db.Integer, db.ForeignKey('bucket_list_model.id')) bucketlist = db.relationship('BucketListModel', backref=db.backref('items', lazy='dynamic')) - def __init__(self, task): + def __init__(self, task, bucketlist): self.task = task + self.bucketlist = bucketlist def __repr__(): return '' % self.name @@ -37,7 +50,8 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True) email = db.Column(db.String(200), unique=True) - #bucketlists = db.relationship('bucketlistmodel', backref='user') + date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) + date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) def __init__(self, username, email): self.username = username diff --git a/app/resource.py b/app/resource.py index 42db004..6a6adef 100644 --- a/app/resource.py +++ b/app/resource.py @@ -1,5 +1,6 @@ from flask_restful import reqparse, abort, Resource from app.models import * +from app.helper import * parser = reqparse.RequestParser() @@ -21,7 +22,8 @@ def put(self, id): class BucketLists(Resource): def get(self): - return [] + bucketlists = BucketListModel.query.all() + return [bucketlist.get() for bucketlist in bucketlists] def post(self): #args = parser.parse_args() diff --git a/migrations/versions/02e15b7e0013_.py b/migrations/versions/02e15b7e0013_.py new file mode 100644 index 0000000..09ed1c5 --- /dev/null +++ b/migrations/versions/02e15b7e0013_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 02e15b7e0013 +Revises: d26feb2b3fde +Create Date: 2016-05-11 12:11:47.981244 + +""" + +# revision identifiers, used by Alembic. +revision = '02e15b7e0013' +down_revision = 'd26feb2b3fde' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('date_created', sa.DateTime(), nullable=True)) + op.add_column('user', sa.Column('date_modified', sa.DateTime(), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'date_modified') + op.drop_column('user', 'date_created') + ### end Alembic commands ### diff --git a/tests/test_resource.py b/tests/test_resource.py new file mode 100644 index 0000000..8ee97c8 --- /dev/null +++ b/tests/test_resource.py @@ -0,0 +1,8 @@ +import unittest +from os import sys, path +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) + + +class TestResources(unittest.TestCase): + """Test cases for Resources""" + From c874618454ca906d7f27c92e0c2656e460058349 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 12 May 2016 18:19:53 +0100 Subject: [PATCH 07/29] [Feature] Implemented /auth/register endpoint --- app/helper.py | 28 +++++++++++++++++++++++-- app/models.py | 15 ++++++++++++-- app/resource.py | 1 + app/views.py | 31 ++++++++++++++++++++++++++++ migrations/versions/f7600f862e03_.py | 26 +++++++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/f7600f862e03_.py diff --git a/app/helper.py b/app/helper.py index 4850ea0..e483fd0 100644 --- a/app/helper.py +++ b/app/helper.py @@ -1,4 +1,28 @@ +from Crypto.Cipher import AES +import hashlib from app import db -def save(): - print 'called save' \ No newline at end of file +def save(model): + try: + db.session.add(model) + db.session.commit() + return True + except: + return False + + +def validate_registeration_data(request): + if not request.json or not 'email' in request.json or not 'username' in request.json: + return False + return True + +def md5(string): + return hashlib.md5(string.encode("utf")).hexdigest() + +def encrypt(string): + aes = AES.new('bucketlist', AES.MODE_CBC) + return aes.encrypt(string) + +def decrypt(string): + aes = AES.new('bucketlist', AES.MODE_CBC) + return aes.decrypt(ciphertext) \ No newline at end of file diff --git a/app/models.py b/app/models.py index eb532d8..0dc6f61 100644 --- a/app/models.py +++ b/app/models.py @@ -49,13 +49,24 @@ def __repr__(): class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True) + password = db.Column(db.String(20)) email = db.Column(db.String(200), unique=True) date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) - def __init__(self, username, email): + def __init__(self, username, email, password): self.username = username self.email = email + self.password = password def __repr__(self): - return '' % self.username \ No newline at end of file + return '' % self.username + + def get(self): + return { + 'id':self.id, + 'username':self.username, + 'email':self.email, + 'date_created':str(self.date_created), + 'date_modified':str(self.date_modified), + } diff --git a/app/resource.py b/app/resource.py index 6a6adef..b962d09 100644 --- a/app/resource.py +++ b/app/resource.py @@ -52,3 +52,4 @@ def post(self, id): #args = parser.parse_args() #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 return {}, 201 + diff --git a/app/views.py b/app/views.py index 156f9db..0ae111a 100644 --- a/app/views.py +++ b/app/views.py @@ -14,3 +14,34 @@ @app.route('/index') def index(): return "Hello, BucketList!" + +@app.route('/api/v1.0/auth/register', methods=['POST']) +def register(): + if not validate_registeration_data(request): + abort(400) + + user = User(request.json['username'], request.json['email'],md5(request.json.get('password', ""))) + if not save(user): + abort(401) + + return jsonify({'data': user.get()}), 201 + +# handling errors 405 +@app.errorhandler(405) +def not_found(error): + return make_response(jsonify({'error': 'Invalid request method ' + request.method,'code':405}), 405) + +# handling errors 404 +@app.errorhandler(404) +def not_found(error): + return make_response(jsonify({'error': 'Resource not found ','code':404}), 404) + +# handling errors 400 +@app.errorhandler(400) +def not_found(error): + return make_response(jsonify({'error': 'Bad request','code':400}), 400) + +# handling errors 401 +@app.errorhandler(401) +def not_found(error): + return make_response(jsonify({'error': 'Unable to save records','code':401}), 401) diff --git a/migrations/versions/f7600f862e03_.py b/migrations/versions/f7600f862e03_.py new file mode 100644 index 0000000..ebfa754 --- /dev/null +++ b/migrations/versions/f7600f862e03_.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: f7600f862e03 +Revises: 02e15b7e0013 +Create Date: 2016-05-12 17:38:42.044363 + +""" + +# revision identifiers, used by Alembic. +revision = 'f7600f862e03' +down_revision = '02e15b7e0013' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('password', sa.String(length=20), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'password') + ### end Alembic commands ### From 52301f3a82e90fccc4c3e7dd6ae254e03108bcd9 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 12 May 2016 20:39:14 +0100 Subject: [PATCH 08/29] [Feature] Implement /auth/login endpoint --- app/helper.py | 25 +++++++++++++++++-------- app/models.py | 5 +++++ app/views.py | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/app/helper.py b/app/helper.py index e483fd0..198eaa0 100644 --- a/app/helper.py +++ b/app/helper.py @@ -1,7 +1,9 @@ -from Crypto.Cipher import AES +from itsdangerous import TimestampSigner import hashlib from app import db +encrypt_key = 'bucketlists api' + def save(model): try: db.session.add(model) @@ -11,18 +13,25 @@ def save(model): return False -def validate_registeration_data(request): - if not request.json or not 'email' in request.json or not 'username' in request.json: +def validate_data(request, required=[]): + if not request.json: return False + + for field in required: + if not field in request.json: + return False return True def md5(string): return hashlib.md5(string.encode("utf")).hexdigest() def encrypt(string): - aes = AES.new('bucketlist', AES.MODE_CBC) - return aes.encrypt(string) + signer = TimestampSigner(encrypt_key) + return signer.sign(string) -def decrypt(string): - aes = AES.new('bucketlist', AES.MODE_CBC) - return aes.decrypt(ciphertext) \ No newline at end of file +def decrypt(string, max_age=5): + try: + signer = TimestampSigner(encrypt_key) + return signer.unsign(string, max_age=max_age) + except: + return False diff --git a/app/models.py b/app/models.py index 0dc6f61..3d7df01 100644 --- a/app/models.py +++ b/app/models.py @@ -1,5 +1,6 @@ from app import db from datetime import datetime +from app.helper import encrypt, decrypt class BucketListModel(db.Model): id = db.Column(db.Integer(), primary_key=True) @@ -70,3 +71,7 @@ def get(self): 'date_created':str(self.date_created), 'date_modified':str(self.date_modified), } + + def generate_token(self): + string = str(self.password) + '|' + str(self.id) + return encrypt(string) diff --git a/app/views.py b/app/views.py index 0ae111a..be65130 100644 --- a/app/views.py +++ b/app/views.py @@ -17,7 +17,15 @@ def index(): @app.route('/api/v1.0/auth/register', methods=['POST']) def register(): - if not validate_registeration_data(request): + """ + This will register a new user and returns the user credentials + + Endpoint: /auth/register + Method: POST + Parameters: username, email, password + Response: JSON + """ + if not validate_data(request, ['username','email']): abort(400) user = User(request.json['username'], request.json['email'],md5(request.json.get('password', ""))) @@ -26,6 +34,25 @@ def register(): return jsonify({'data': user.get()}), 201 +@app.route('/api/v1.0/auth/login', methods=['POST']) +def login(): + """ + This will authenticate user and provide token used to access other resources + + Endpoint: /auth/login + Method: POST + Parameters: username, password + Response: JSON + """ + if not validate_data(request,['username','password']): + abort(400) + + user = User.query.filter_by(username=request.json['username'],password=md5(request.json['password'])).first() + if not user: + abort(403) + + return jsonify({'token': user.generate_token(),'data': user.get()}), 200 + # handling errors 405 @app.errorhandler(405) def not_found(error): @@ -36,6 +63,11 @@ def not_found(error): def not_found(error): return make_response(jsonify({'error': 'Resource not found ','code':404}), 404) +# handling errors 403 +@app.errorhandler(403) +def not_found(error): + return make_response(jsonify({'error': 'Unauthorized access','code':403}), 403) + # handling errors 400 @app.errorhandler(400) def not_found(error): From 392b0d7950860ac9647a22cea202f6bf78b196c5 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Sun, 15 May 2016 21:08:49 +0100 Subject: [PATCH 09/29] [Feature] Complete implementation of bucketlists endpoint --- app/helper.py | 44 ++++++++++-- app/models.py | 4 +- app/resource.py | 174 ++++++++++++++++++++++++++++++++++++++++++++---- app/views.py | 67 +------------------ 4 files changed, 207 insertions(+), 82 deletions(-) diff --git a/app/helper.py b/app/helper.py index 198eaa0..50e4b5f 100644 --- a/app/helper.py +++ b/app/helper.py @@ -12,8 +12,30 @@ def save(model): except: return False +def delete(model): + try: + db.session.delete(model) + db.session.commit() + return True + except: + return False + -def validate_data(request, required=[]): +def get_user_id_from_token(token): + """ + This method extracts the user id from provided access token + """ + data = token.split('|') + try: + return data[1] + except: + return 0 + +def validate_required_fields(request, required=[]): + """ + This method helps to validate provided required fields. + It will return False if the require field is not in request + """ if not request.json: return False @@ -23,13 +45,27 @@ def validate_data(request, required=[]): return True def md5(string): + """ + This method will return md5 hash of a given string + """ return hashlib.md5(string.encode("utf")).hexdigest() def encrypt(string): - signer = TimestampSigner(encrypt_key) - return signer.sign(string) + """ + This method will return encrypted version of a string. + It will return False if encryption fails + """ + try: + signer = TimestampSigner(encrypt_key) + return signer.sign(string) + except: + return False -def decrypt(string, max_age=5): +def decrypt(string, max_age=15000): + """ + This method will return decrypted version of an encrypted string. + If age of encryption is greater than max age it will return False + """ try: signer = TimestampSigner(encrypt_key) return signer.unsign(string, max_age=max_age) diff --git a/app/models.py b/app/models.py index 3d7df01..9bcce73 100644 --- a/app/models.py +++ b/app/models.py @@ -21,7 +21,7 @@ def get(self): 'name':self.name, 'created_by':self.user.username, 'date_created':str(self.date_created), - 'date_modified':str(self.date_modified), + 'date_modified':str(self.date_modified) } def __repr__(): @@ -73,5 +73,5 @@ def get(self): } def generate_token(self): - string = str(self.password) + '|' + str(self.id) + string = str(self.password) + '|' + str(self.id) + '|' return encrypt(string) diff --git a/app/resource.py b/app/resource.py index b962d09..540a5d8 100644 --- a/app/resource.py +++ b/app/resource.py @@ -1,34 +1,132 @@ from flask_restful import reqparse, abort, Resource +from flask_httpauth import HTTPBasicAuth +from flask import request, make_response, jsonify from app.models import * from app.helper import * - +auth = HTTPBasicAuth() parser = reqparse.RequestParser() + parser.add_argument('task') +@auth.verify_password +def verify_token(username, password): + """ + This method will verify the token and allow access to any resource that requires authentication + """ + global token + token = request.headers.get('AccessToken','') + if not token: + return False + + return decrypt(token) + + +@auth.error_handler +def unauthorized(): + """ + This will return a JSON error 403 response when token validation fails + """ + return make_response(jsonify({'error': 'Unauthorized access','code':403}), 403) class BucketList(Resource): + @auth.login_required def get(self, id): - return {'id':id} + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + return {'data':bucketlist.get()} + @auth.login_required def delete(self, id): - return '', 204 + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + if not delete(bucketlist): + abort(401, message="Unable to delete record") + + return {}, 204 + @auth.login_required def put(self, id): - #args = parser.parse_args() - #task = {'task': args['task']} - return {}, 201 + if not validate_required_fields(request, ['name']): + abort(400, message="Missing required parameter") + + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + bucketlist.name = request.json['name'] + + if not save(bucketlist): + abort(409, message="Item already exists") + + return {'data':bucketlist.get()}, 201 class BucketLists(Resource): - def get(self): - bucketlists = BucketListModel.query.all() - return [bucketlist.get() for bucketlist in bucketlists] + @auth.login_required + def get(self): + """ + This endpoint returns list of bucketlists based on access token. + Method: GET + Parameters: + limit (optional) default=25 + page (optional) default=1 + Header: + AccessToken (required) + + Response: JSON + """ + limit = int(request.args.get('limit',25)) + page = int(request.args.get('page',1)) + offset = (page * limit) - limit + user_id = get_user_id_from_token(token) + next_page = page + 1 + prev_page = page - 1 if page > 1 else 1 + all_list = BucketListModel.query.filter_by(created_by=int(user_id)).all() + bucketlists = all_list[offset:(limit + offset)] + result = {'data':[bucketlist.get() for bucketlist in bucketlists],'page':{}} + if (limit + offset) < len(all_list): + result['page']['next'] = "/api/v1.0/bucketlists?limit=" + str(limit) + "&page=" + str(next_page) + + if offset: + result['page']['prev'] = "/api/v1.0/bucketlists?limit=" + str(limit) + "&page=" + str(prev_page) + + return result + + @auth.login_required def post(self): - #args = parser.parse_args() - #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 - return {}, 201 + """ + This endpoint returns created bucketlist details. + Method: POST + Parameters: + name (required) + Header: + AccessToken (required) + + Response: JSON + """ + + if not validate_required_fields(request, ['name']): + abort(400, message="Missing required parameter") + + user_id = get_user_id_from_token(token) + user = User.query.filter_by(id=user_id).first() + if not user: + abort(403, message="Unauthorized access") + + bucketlist = BucketListModel(request.json['name'], user) + if not save(bucketlist): + abort(409, message="Item already exists") + + return {'data': bucketlist.get()}, 201 class BucketListItem(Resource): @@ -53,3 +151,55 @@ def post(self, id): #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 return {}, 201 + + +class Login(Resource): + def post(self): + """ + This will authenticate user and provide token used to access other resources + + Method: POST + Parameters: + username (required) + password (required) + Response: JSON + """ + + if not validate_required_fields(request,['username','password']): + abort(400) + + user = User.query.filter_by(username=request.json['username'],password=md5(request.json['password'])).first() + + if not user: + abort(403) + + return {'token': user.generate_token(),'data': user.get()}, 200 + + +class Register(Resource): + + def post(self): + """ + This will register a new user and returns the user credentials + + Method: POST + Parameters: + username (required) + email (required) + password (optional) default='' + + Response: JSON + """ + + if not validate_required_fields(request, ['username','email']): + abort(400) + + user = User(request.json['username'], request.json['email'],md5(request.json.get('password', ""))) + if not save(user): + abort(401) + + return {'data': user.get()}, 201 + + + + diff --git a/app/views.py b/app/views.py index be65130..5966cfd 100644 --- a/app/views.py +++ b/app/views.py @@ -3,8 +3,10 @@ from flask_restful import Api from app.resource import * -api = Api(app, '/api/v1.0') +api = Api(app, prefix='/api/v1.0') +api.add_resource(Login, '/auth/login') +api.add_resource(Register, '/auth/register') api.add_resource(BucketLists, '/bucketlists') api.add_resource(BucketList, '/bucketlists/') api.add_resource(BucketListItems, '/bucketlists//items') @@ -14,66 +16,3 @@ @app.route('/index') def index(): return "Hello, BucketList!" - -@app.route('/api/v1.0/auth/register', methods=['POST']) -def register(): - """ - This will register a new user and returns the user credentials - - Endpoint: /auth/register - Method: POST - Parameters: username, email, password - Response: JSON - """ - if not validate_data(request, ['username','email']): - abort(400) - - user = User(request.json['username'], request.json['email'],md5(request.json.get('password', ""))) - if not save(user): - abort(401) - - return jsonify({'data': user.get()}), 201 - -@app.route('/api/v1.0/auth/login', methods=['POST']) -def login(): - """ - This will authenticate user and provide token used to access other resources - - Endpoint: /auth/login - Method: POST - Parameters: username, password - Response: JSON - """ - if not validate_data(request,['username','password']): - abort(400) - - user = User.query.filter_by(username=request.json['username'],password=md5(request.json['password'])).first() - if not user: - abort(403) - - return jsonify({'token': user.generate_token(),'data': user.get()}), 200 - -# handling errors 405 -@app.errorhandler(405) -def not_found(error): - return make_response(jsonify({'error': 'Invalid request method ' + request.method,'code':405}), 405) - -# handling errors 404 -@app.errorhandler(404) -def not_found(error): - return make_response(jsonify({'error': 'Resource not found ','code':404}), 404) - -# handling errors 403 -@app.errorhandler(403) -def not_found(error): - return make_response(jsonify({'error': 'Unauthorized access','code':403}), 403) - -# handling errors 400 -@app.errorhandler(400) -def not_found(error): - return make_response(jsonify({'error': 'Bad request','code':400}), 400) - -# handling errors 401 -@app.errorhandler(401) -def not_found(error): - return make_response(jsonify({'error': 'Unable to save records','code':401}), 401) From d51bfb9ffa24395e9e0944bc2aa0472d6c78ce5a Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 17 May 2016 23:52:31 +0100 Subject: [PATCH 10/29] [Chores] Refactored and added test cases for bucketlists endpoint --- app/__init__.py | 5 +- app/helper.py | 15 ++++- app/resource.py | 27 +++----- migrations/versions/ac8bcafddd94_.py | 58 +++++++++++++++++ config.py => production_config.py | 0 test_config.py | 6 ++ tests/test_resource.py | 94 ++++++++++++++++++++++++++++ 7 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 migrations/versions/ac8bcafddd94_.py rename config.py => production_config.py (100%) create mode 100644 test_config.py diff --git a/app/__init__.py b/app/__init__.py index b0b5a3a..e815af4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,8 +1,11 @@ from flask import Flask from flask_sqlalchemy import SQLAlchemy +from flask import request app = Flask(__name__) -app.config.from_object('config') + +#server = request.args.get('server','production').lower() +app.config.from_object('production_config') db = SQLAlchemy(app) from app import views \ No newline at end of file diff --git a/app/helper.py b/app/helper.py index 50e4b5f..3cac0f7 100644 --- a/app/helper.py +++ b/app/helper.py @@ -1,5 +1,6 @@ -from itsdangerous import TimestampSigner +from flask_restful import reqparse import hashlib +from itsdangerous import TimestampSigner from app import db encrypt_key = 'bucketlists api' @@ -44,6 +45,18 @@ def validate_required_fields(request, required=[]): return False return True +def validate_args(fields={}): + """ + This method helps to parse and validate provided parameters. + It will return parsed argument if the require fields are in request + """ + parser = reqparse.RequestParser() + for field in fields.keys(): + help_message = field + ' can not be blank' + parser.add_argument(field, required=fields[field], help=help_message) + + return parser.parse_args() + def md5(string): """ This method will return md5 hash of a given string diff --git a/app/resource.py b/app/resource.py index 540a5d8..ebc7518 100644 --- a/app/resource.py +++ b/app/resource.py @@ -53,15 +53,13 @@ def delete(self, id): @auth.login_required def put(self, id): - if not validate_required_fields(request, ['name']): - abort(400, message="Missing required parameter") - + args = validate_args({'name':True}) user_id = get_user_id_from_token(token) bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() if not bucketlist: abort(403, message="Unauthorized access") - bucketlist.name = request.json['name'] + bucketlist.name = args['name'] if not save(bucketlist): abort(409, message="Item already exists") @@ -114,15 +112,13 @@ def post(self): Response: JSON """ - if not validate_required_fields(request, ['name']): - abort(400, message="Missing required parameter") - + args = validate_args({'name':True}) user_id = get_user_id_from_token(token) user = User.query.filter_by(id=user_id).first() if not user: abort(403, message="Unauthorized access") - bucketlist = BucketListModel(request.json['name'], user) + bucketlist = BucketListModel(args['name'], user) if not save(bucketlist): abort(409, message="Item already exists") @@ -164,11 +160,8 @@ def post(self): password (required) Response: JSON """ - - if not validate_required_fields(request,['username','password']): - abort(400) - - user = User.query.filter_by(username=request.json['username'],password=md5(request.json['password'])).first() + args = validate_args({'username':True, 'password':True}) + user = User.query.filter_by(username=args['username'],password=md5(args['password'])).first() if not user: abort(403) @@ -191,12 +184,10 @@ def post(self): Response: JSON """ - if not validate_required_fields(request, ['username','email']): - abort(400) - - user = User(request.json['username'], request.json['email'],md5(request.json.get('password', ""))) + args = validate_args({'username':True, 'password':True, 'email':True}) + user = User(args['username'], args['email'], md5(args['password'])) if not save(user): - abort(401) + abort(401, message="User already exists") return {'data': user.get()}, 201 diff --git a/migrations/versions/ac8bcafddd94_.py b/migrations/versions/ac8bcafddd94_.py new file mode 100644 index 0000000..13cdd44 --- /dev/null +++ b/migrations/versions/ac8bcafddd94_.py @@ -0,0 +1,58 @@ +"""empty message + +Revision ID: ac8bcafddd94 +Revises: f7600f862e03 +Create Date: 2016-05-17 22:13:38.480965 + +""" + +# revision identifiers, used by Alembic. +revision = 'ac8bcafddd94' +down_revision = 'f7600f862e03' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=100), nullable=True), + sa.Column('password', sa.String(length=20), nullable=True), + sa.Column('email', sa.String(length=200), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('date_modified', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_table('bucket_list_model', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('date_modified', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['created_by'], ['user.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('bucket_list_item_model', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('task', sa.String(length=255), nullable=True), + sa.Column('done', sa.Boolean(), nullable=True), + sa.Column('date_created', sa.DateTime(), nullable=True), + sa.Column('date_modified', sa.DateTime(), nullable=True), + sa.Column('bucketlist_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['bucketlist_id'], ['bucket_list_model.id'], ), + sa.PrimaryKeyConstraint('id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('bucket_list_item_model') + op.drop_table('bucket_list_model') + op.drop_table('user') + ### end Alembic commands ### diff --git a/config.py b/production_config.py similarity index 100% rename from config.py rename to production_config.py diff --git a/test_config.py b/test_config.py new file mode 100644 index 0000000..bfc8c3b --- /dev/null +++ b/test_config.py @@ -0,0 +1,6 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'test.db') +#SQLALCHEMY_DATABASE_URI = 'postgresql://sunday:@localhost/bucketlist' +SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'migrations') \ No newline at end of file diff --git a/tests/test_resource.py b/tests/test_resource.py index 8ee97c8..1d70a70 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -1,8 +1,102 @@ import unittest +import json +import os from os import sys, path sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) +from app import app, db +from app.models import * +from app.helper import delete +os.system('python ../script.py db migrate') +os.system('python ../script.py db upgrade') class TestResources(unittest.TestCase): """Test cases for Resources""" + def register(self): + user = {'username':'tuser', 'password':'test','email':'tuser@mail.com'} + response = self.app.post('/api/v1.0/auth/register',data=user) + self.assertEqual(response.status_code, 201) + result = json.loads(response.data) + if not result.get('message'): + self.uid = result['data']['id'] + self.token = self.get_token() + + + def tearDown(self): + self.remove_user() + self.remove_bucketlist() + + def get_token(self): + user = {'username':'tuser', 'password':'test'} + response = self.app.post('/api/v1.0/auth/login',data=user) + result = json.loads(response.data) + return result.get('token','') + + def setUp(self): + app.config.from_object('test_config') + self.app = app.test_client() + self.uid = 0 + self.token = '' + self.bucketlist_id = 0 + self.bucketlist_item_id = 0 + + + def remove_user(self): + user = User.query.filter_by(id=self.uid).first() + if user: + delete(user) + + def remove_bucketlist(self): + bucketlist = BucketListModel.query.filter_by(id=self.bucketlist_id).first() + if bucketlist: + delete(bucketlist) + + + + def test_bucketlists_endpoint(self): + self.register() + + # posting to bucketlists endpoint + new_bucketlists = self.app.post('/api/v1.0/bucketlists', data={'name':'test item 1'}, + headers={'AccessToken':self.token}) + self.assertEqual(new_bucketlists.status_code, 201) + new_bucketlists_result = json.loads(new_bucketlists.data) + if new_bucketlists_result.get('data'): + self.bucketlist_id = new_bucketlists_result['data']['id'] + duplicate_bucketlists = self.app.post('/api/v1.0/bucketlists',data={'name':'test item 1'} , + headers={'AccessToken':self.token}) + self.assertEqual(duplicate_bucketlists.status_code, 409) + + # Get bucketlists + get_bucketlists = self.app.get('/api/v1.0/bucketlists', headers={'AccessToken':self.token}) + self.assertEqual(get_bucketlists.status_code, 200) + get_result = json.loads(get_bucketlists.data) + self.assertNotEqual(get_result.get('data'), None) + + # Get bucketlists single item + get_bucketlists = self.app.get('/api/v1.0/bucketlists/'+ str(self.bucketlist_id), + headers={'AccessToken':self.token}) + self.assertEqual(get_bucketlists.status_code, 200) + get_result = json.loads(get_bucketlists.data) + self.assertNotEqual(get_result.get('data'), None) + + # Update bucketlists + put_bucketlists = self.app.put('/api/v1.0/bucketlists/'+ str(self.bucketlist_id), + data={'name':'test item modified'}, headers={'AccessToken':self.token}) + self.assertEqual(put_bucketlists.status_code, 201) + put_result = json.loads(put_bucketlists.data) + data = put_result.get('data') + print data['name'] + self.assertEqual(str(data['name']), 'test item modified') + + # delete bucketlists + del_bucketlists = self.app.delete('/api/v1.0/bucketlists/'+ str(self.bucketlist_id), + headers={'AccessToken':self.token}) + self.assertEqual(del_bucketlists.status_code, 204) + + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From ee27adde1ff17522e1151ec6dea42e947e025e86 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 17 May 2016 23:53:32 +0100 Subject: [PATCH 11/29] [Chores] Refactored and added test cases for bucketlists endpoint --- tests/test_resource.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_resource.py b/tests/test_resource.py index 1d70a70..172e00a 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -1,14 +1,11 @@ import unittest import json -import os from os import sys, path sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) from app import app, db from app.models import * from app.helper import delete -os.system('python ../script.py db migrate') -os.system('python ../script.py db upgrade') class TestResources(unittest.TestCase): """Test cases for Resources""" From 167da66e398d61c0ecbff589e3396f9d5d900ba5 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Wed, 18 May 2016 20:56:48 +0100 Subject: [PATCH 12/29] [Feature] complete implementation of bucketlist item and complete test cases for bucketlists endpoint --- app/models.py | 11 +++ app/resource.py | 179 ++++++++++++++++++++++++++++++++++++++--- tests/test_resource.py | 26 ++++-- 3 files changed, 197 insertions(+), 19 deletions(-) diff --git a/app/models.py b/app/models.py index 9bcce73..bda3a58 100644 --- a/app/models.py +++ b/app/models.py @@ -2,6 +2,7 @@ from datetime import datetime from app.helper import encrypt, decrypt + class BucketListModel(db.Model): id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), unique=True) @@ -46,6 +47,16 @@ def __init__(self, task, bucketlist): def __repr__(): return '' % self.name + def get(self): + return { + 'id':self.id, + 'task':self.task, + 'done':self.done, + 'bucketlists':self.bucketlist.name, + 'date_created':str(self.date_created), + 'date_modified':str(self.date_modified) + } + class User(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/resource.py b/app/resource.py index ebc7518..39e210c 100644 --- a/app/resource.py +++ b/app/resource.py @@ -5,15 +5,13 @@ from app.helper import * auth = HTTPBasicAuth() -parser = reqparse.RequestParser() - -parser.add_argument('task') @auth.verify_password def verify_token(username, password): """ This method will verify the token and allow access to any resource that requires authentication """ + global token token = request.headers.get('AccessToken','') if not token: @@ -21,17 +19,26 @@ def verify_token(username, password): return decrypt(token) - @auth.error_handler def unauthorized(): """ This will return a JSON error 403 response when token validation fails """ + return make_response(jsonify({'error': 'Unauthorized access','code':403}), 403) class BucketList(Resource): @auth.login_required def get(self, id): + """ + This endpoint returns bucketlists details of a given bucketlist id. + Method: GET + Header: + AccessToken (required) + + Response: JSON + """ + user_id = get_user_id_from_token(token) bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() if not bucketlist: @@ -41,6 +48,15 @@ def get(self, id): @auth.login_required def delete(self, id): + """ + This endpoint deletes a given bucketlist id from the database. + Method: DELETE + Header: + AccessToken (required) + + Response: JSON + """ + user_id = get_user_id_from_token(token) bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() if not bucketlist: @@ -53,6 +69,17 @@ def delete(self, id): @auth.login_required def put(self, id): + """ + This endpoint returns updated bucketlist details of a given bucketlist id. + Method: PUT + Parameters: + name (required) + Header: + AccessToken (required) + + Response: JSON + """ + args = validate_args({'name':True}) user_id = get_user_id_from_token(token) bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() @@ -82,6 +109,7 @@ def get(self): Response: JSON """ + limit = int(request.args.get('limit',25)) page = int(request.args.get('page',1)) offset = (page * limit) - limit @@ -126,26 +154,150 @@ def post(self): class BucketListItem(Resource): + + @auth.login_required def get(self, id, item_id): - return {'bucketlist_id':id, 'item_id':item_id} + """ + This endpoint returns bucketlists item details of a given bucketlist item id. + Method: GET + Header: + AccessToken (required) + + Response: JSON + """ + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + item = BucketListItemModel.query.filter_by(bucketlist_id=bucketlist.id, id=int(item_id)).first() + if not item: + abort(400, message="Item does not exist") + + return {'data':item.get()}, 200 + + @auth.login_required def delete(self, id, item_id): - return '', 204 + """ + This endpoint deletes a given bucketlist item id from the database. + Method: DELETE + Header: + AccessToken (required) + + Response: JSON + """ + + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + item = BucketListItemModel.query.filter_by(bucketlist_id=bucketlist.id, id=int(item_id)).first() + if not item: + abort(400, message="Item does not exist") + + if not delete(item): + abort(401, message="Unable to delete record") + + return {}, 204 + @auth.login_required def put(self, id, item_id): - #args = parser.parse_args() - #task = {'task': args['task']} - return {}, 201 + """ + This endpoint returns updated bucketlist item details of a given bucketlist item id. + Method: PUT + Parameters: + task (optional) + done (optional) + Header: + AccessToken (required) + + Response: JSON + """ + + args = validate_args({'task':False,'done':False}) + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + item = BucketListItemModel.query.filter_by(bucketlist_id=bucketlist.id, id=int(item_id)).first() + if not item: + abort(400, message="Item does not exist") + + item.task = args.get('task',item.task) + done = args.get('done',False) + item.done = True if done == 'True' else Fals + + if not save(item): + abort(409, message="Unable to update record") + + return {'data':item.get()}, 201 class BucketListItems(Resource): + + @auth.login_required def get(self, id): - return [] + """ + This endpoint returns list of bucketlist items based on access token and bucketlist id. + Method: GET + Parameters: + limit (optional) default=25 + page (optional) default=1 + Header: + AccessToken (required) + + Response: JSON + """ + + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + limit = int(request.args.get('limit',25)) + page = int(request.args.get('page',1)) + offset = (page * limit) - limit + user_id = get_user_id_from_token(token) + next_page = page + 1 + prev_page = page - 1 if page > 1 else 1 + all_items = BucketListItemModel.query.filter_by(bucketlist_id=bucketlist.id).all() + items = all_items[offset:(limit + offset)] + result = {'data':[item.get() for item in items],'page':{}} + if (limit + offset) < len(all_items): + result['page']['next'] = "/api/v1.0/bucketlists/" + str(id) + "?limit=" + str(limit) + "&page=" + str(next_page) + + if offset: + result['page']['prev'] = "/api/v1.0/bucketlists" + str(id) + "?limit=" + str(limit) + "&page=" + str(prev_page) + + return result + + @auth.login_required def post(self, id): - #args = parser.parse_args() - #todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 - return {}, 201 + """ + This endpoint returns created bucketlist item details. + Method: POST + Parameters: + task (required) + Header: + AccessToken (required) + + Response: JSON + """ + + args = validate_args({'task':True}) + user_id = get_user_id_from_token(token) + bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() + if not bucketlist: + abort(403, message="Unauthorized access") + + item = BucketListItemModel(args['task'], bucketlist) + if not save(item): + abort(409, message="Item already exists") + + return {'data': item.get()}, 201 @@ -160,6 +312,7 @@ def post(self): password (required) Response: JSON """ + args = validate_args({'username':True, 'password':True}) user = User.query.filter_by(username=args['username'],password=md5(args['password'])).first() diff --git a/tests/test_resource.py b/tests/test_resource.py index 172e00a..a211c1f 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -11,6 +11,9 @@ class TestResources(unittest.TestCase): """Test cases for Resources""" def register(self): + if self.uid: + return + user = {'username':'tuser', 'password':'test','email':'tuser@mail.com'} response = self.app.post('/api/v1.0/auth/register',data=user) self.assertEqual(response.status_code, 201) @@ -50,10 +53,7 @@ def remove_bucketlist(self): delete(bucketlist) - - def test_bucketlists_endpoint(self): - self.register() - + def new_bucketlist(self): # posting to bucketlists endpoint new_bucketlists = self.app.post('/api/v1.0/bucketlists', data={'name':'test item 1'}, headers={'AccessToken':self.token}) @@ -64,6 +64,22 @@ def test_bucketlists_endpoint(self): duplicate_bucketlists = self.app.post('/api/v1.0/bucketlists',data={'name':'test item 1'} , headers={'AccessToken':self.token}) self.assertEqual(duplicate_bucketlists.status_code, 409) + + def new_bucketlist_item(self): + # posting to bucketlists endpoint + new_bucketlist_item = self.app.post('/api/v1.0/bucketlists/' + str(self.bucketlist_id) + '/items', + data={'task':'buy a private jet'}, + headers={'AccessToken':self.token}) + + self.assertEqual(new_bucketlist_item.status_code, 201) + new_bucketlist_item_result = json.loads(new_bucketlist_item.data) + self.assertNotEqual(new_bucketlist_item_result.get('data'), None) + + + def test_bucketlists_endpoint(self): + self.register() + + self.new_bucketlist() # Get bucketlists get_bucketlists = self.app.get('/api/v1.0/bucketlists', headers={'AccessToken':self.token}) @@ -84,7 +100,6 @@ def test_bucketlists_endpoint(self): self.assertEqual(put_bucketlists.status_code, 201) put_result = json.loads(put_bucketlists.data) data = put_result.get('data') - print data['name'] self.assertEqual(str(data['name']), 'test item modified') # delete bucketlists @@ -92,7 +107,6 @@ def test_bucketlists_endpoint(self): headers={'AccessToken':self.token}) self.assertEqual(del_bucketlists.status_code, 204) - if __name__ == '__main__': From c11d6dc1f46d2a303c39dc94e354ec098fcc3127 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 10:11:41 +0100 Subject: [PATCH 13/29] [Chores] Added test cases for bucketlist and bucketlist item endpoint --- .../{test_resource.py => test_bucketlists.py} | 39 +++-- tests/test_bucketlists_items.py | 144 ++++++++++++++++++ 2 files changed, 173 insertions(+), 10 deletions(-) rename tests/{test_resource.py => test_bucketlists.py} (86%) create mode 100644 tests/test_bucketlists_items.py diff --git a/tests/test_resource.py b/tests/test_bucketlists.py similarity index 86% rename from tests/test_resource.py rename to tests/test_bucketlists.py index a211c1f..773634d 100644 --- a/tests/test_resource.py +++ b/tests/test_bucketlists.py @@ -24,10 +24,13 @@ def register(self): def tearDown(self): + """ Clean up database """ self.remove_user() self.remove_bucketlist() def get_token(self): + """ Login to retrieve access token """ + user = {'username':'tuser', 'password':'test'} response = self.app.post('/api/v1.0/auth/login',data=user) result = json.loads(response.data) @@ -43,18 +46,23 @@ def setUp(self): def remove_user(self): + """ Remove user record """ + user = User.query.filter_by(id=self.uid).first() if user: delete(user) def remove_bucketlist(self): + """ Remove bucketlist record """ + bucketlist = BucketListModel.query.filter_by(id=self.bucketlist_id).first() if bucketlist: delete(bucketlist) def new_bucketlist(self): - # posting to bucketlists endpoint + """ Post to bucketlists endpoint """ + new_bucketlists = self.app.post('/api/v1.0/bucketlists', data={'name':'test item 1'}, headers={'AccessToken':self.token}) self.assertEqual(new_bucketlists.status_code, 201) @@ -75,26 +83,26 @@ def new_bucketlist_item(self): new_bucketlist_item_result = json.loads(new_bucketlist_item.data) self.assertNotEqual(new_bucketlist_item_result.get('data'), None) + def get_bucketlists(self): + """ Get bucketlists """ - def test_bucketlists_endpoint(self): - self.register() - - self.new_bucketlist() - - # Get bucketlists get_bucketlists = self.app.get('/api/v1.0/bucketlists', headers={'AccessToken':self.token}) self.assertEqual(get_bucketlists.status_code, 200) get_result = json.loads(get_bucketlists.data) self.assertNotEqual(get_result.get('data'), None) - # Get bucketlists single item + def get_single_bucketlist(self): + """ Get bucketlists single item """ + get_bucketlists = self.app.get('/api/v1.0/bucketlists/'+ str(self.bucketlist_id), headers={'AccessToken':self.token}) self.assertEqual(get_bucketlists.status_code, 200) get_result = json.loads(get_bucketlists.data) self.assertNotEqual(get_result.get('data'), None) - # Update bucketlists + def update_bucketist(self): + """ Update bucketlists """ + put_bucketlists = self.app.put('/api/v1.0/bucketlists/'+ str(self.bucketlist_id), data={'name':'test item modified'}, headers={'AccessToken':self.token}) self.assertEqual(put_bucketlists.status_code, 201) @@ -102,12 +110,23 @@ def test_bucketlists_endpoint(self): data = put_result.get('data') self.assertEqual(str(data['name']), 'test item modified') - # delete bucketlists + def delete_bucketlist(self): + """ Delete bucketlists """ + del_bucketlists = self.app.delete('/api/v1.0/bucketlists/'+ str(self.bucketlist_id), headers={'AccessToken':self.token}) self.assertEqual(del_bucketlists.status_code, 204) + def test_bucketlists_endpoint(self): + """ Test all endpoints of bucketlists """ + self.register() + self.new_bucketlist() + self.get_bucketlists() + self.get_single_bucketlist() + self.update_bucketist() + self.delete_bucketlist() + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/tests/test_bucketlists_items.py b/tests/test_bucketlists_items.py new file mode 100644 index 0000000..bc5b2dd --- /dev/null +++ b/tests/test_bucketlists_items.py @@ -0,0 +1,144 @@ +import unittest +import json +from os import sys, path +sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) +from app import app, db +from app.models import * +from app.helper import delete + + +class TestResources(unittest.TestCase): + """Test cases for Bucketlist Items""" + + def register(self): + """ Register new user """ + if self.uid: + return + + user = {'username':'tuser', 'password':'test','email':'tuser@mail.com'} + response = self.app.post('/api/v1.0/auth/register',data=user) + self.assertEqual(response.status_code, 201) + result = json.loads(response.data) + if not result.get('message'): + self.uid = result['data']['id'] + self.token = self.get_token() + + + def tearDown(self): + """ Clean up the database """ + + self.remove_user() + self.remove_bucketlist() + self.remove_bucketlist_item() + + def get_token(self): + """ Login to retrieve access token """ + + user = {'username':'tuser', 'password':'test'} + response = self.app.post('/api/v1.0/auth/login',data=user) + result = json.loads(response.data) + return result.get('token','') + + def setUp(self): + app.config.from_object('test_config') + self.app = app.test_client() + self.uid = 0 + self.token = '' + self.bucketlist_id = 0 + self.bucketlist_item_id = 0 + + + def remove_user(self): + """ Remove user record """ + + user = User.query.filter_by(id=self.uid).first() + if user: + delete(user) + + def remove_bucketlist(self): + """ Remove bucketlist record """ + + bucketlist = BucketListModel.query.filter_by(id=self.bucketlist_id).first() + if bucketlist: + delete(bucketlist) + + def remove_bucketlist_item(self): + """ Remove bucketlist item """ + + item = BucketListItemModel.query.filter_by(id=self.bucketlist_item_id).first() + if item: + delete(item) + + + def new_bucketlist(self): + """ Posting to bucketlists endpoint """ + + new_bucketlists = self.app.post('/api/v1.0/bucketlists', data={'name':'test item 1'}, + headers={'AccessToken':self.token}) + self.assertEqual(new_bucketlists.status_code, 201) + new_bucketlists_result = json.loads(new_bucketlists.data) + if new_bucketlists_result.get('data'): + self.bucketlist_id = new_bucketlists_result['data']['id'] + + + def new_bucketlist_item(self): + """ Posting to bucketlists endpoint """ + + new_bucketlist_item = self.app.post('/api/v1.0/bucketlists/' + str(self.bucketlist_id) + '/items', + data={'task':'buy a private jet'}, + headers={'AccessToken':self.token}) + + self.assertEqual(new_bucketlist_item.status_code, 201) + new_bucketlist_item_result = json.loads(new_bucketlist_item.data) + self.assertNotEqual(new_bucketlist_item_result.get('data'), None) + if new_bucketlist_item_result: + self.bucketlist_item_id = new_bucketlist_item_result['data']['id'] + + def get_items(self): + """ Get bucketlist Items """ + get_items = self.app.get('/api/v1.0/bucketlists/' + str(self.bucketlist_id) + '/items', + headers={'AccessToken':self.token}) + self.assertEqual(get_items.status_code, 200) + get_result = json.loads(get_items.data) + self.assertNotEqual(get_result.get('data'), None) + + + def get_single_item(self): + """ Get bucketlist Item """ + + get_item = self.app.get('/api/v1.0/bucketlists/' + str(self.bucketlist_id) + '/items/' + str(self.bucketlist_item_id), + headers={'AccessToken':self.token}) + self.assertEqual(get_item.status_code, 200) + get_result = json.loads(get_item.data) + self.assertNotEqual(get_result.get('data'), None) + + def update_item(self): + """ Update bucketlist item """ + + put_item = self.app.put('/api/v1.0/bucketlists/' + str(self.bucketlist_id) + '/items/' + str(self.bucketlist_item_id), + data={'done':True}, headers={'AccessToken':self.token}) + self.assertEqual(put_item.status_code, 201) + put_result = json.loads(put_item.data) + data = put_result.get('data') + self.assertEqual(data['done'], True) + + def delete_item(self): + """ Delete bucketlist item """ + del_item = self.app.delete('/api/v1.0/bucketlists/'+ str(self.bucketlist_id) + '/items/' + str(self.bucketlist_item_id), + headers={'AccessToken':self.token}) + self.assertEqual(del_item.status_code, 204) + + def test_bucketlists_endpoint(self): + """ Test all endpoint of bucketlist items """ + + self.register() + self.new_bucketlist() + self.new_bucketlist_item() + self.get_items() + self.update_item() + self.delete_item() + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 5384ee8e54e4847e3db0d667723c0facd642f4ff Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 12:21:51 +0100 Subject: [PATCH 14/29] [Feature] Implement search by name in bucketlist endpoint and bucketlist item endpoint --- app/models.py | 1 - app/resource.py | 14 +++++++++++--- app/views.py | 5 +++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/models.py b/app/models.py index bda3a58..93143db 100644 --- a/app/models.py +++ b/app/models.py @@ -6,7 +6,6 @@ class BucketListModel(db.Model): id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), unique=True) - #items = db.relationship('BucketListItemModel', backref='bucketlist') created_by = db.Column(db.Integer, db.ForeignKey('user.id')) date_created = db.Column(db.DateTime, default=db.func.current_timestamp()) date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) diff --git a/app/resource.py b/app/resource.py index 39e210c..a6ebd92 100644 --- a/app/resource.py +++ b/app/resource.py @@ -62,6 +62,9 @@ def delete(self, id): if not bucketlist: abort(403, message="Unauthorized access") + if len(bucketlist.items.all()): + abort(400, message="This Item is associated with bucketlist items") + if not delete(bucketlist): abort(401, message="Unable to delete record") @@ -112,11 +115,12 @@ def get(self): limit = int(request.args.get('limit',25)) page = int(request.args.get('page',1)) + q = request.args.get('q','') offset = (page * limit) - limit user_id = get_user_id_from_token(token) next_page = page + 1 prev_page = page - 1 if page > 1 else 1 - all_list = BucketListModel.query.filter_by(created_by=int(user_id)).all() + all_list = BucketListModel.query.filter(BucketListModel.name.like("%" + q + "%"), BucketListModel.created_by.is_(user_id)).all() bucketlists = all_list[offset:(limit + offset)] result = {'data':[bucketlist.get() for bucketlist in bucketlists],'page':{}} if (limit + offset) < len(all_list): @@ -192,7 +196,7 @@ def delete(self, id, item_id): bucketlist = BucketListModel.query.filter_by(id=id, created_by=int(user_id)).first() if not bucketlist: abort(403, message="Unauthorized access") - + item = BucketListItemModel.query.filter_by(bucketlist_id=bucketlist.id, id=int(item_id)).first() if not item: abort(400, message="Item does not exist") @@ -259,11 +263,15 @@ def get(self, id): limit = int(request.args.get('limit',25)) page = int(request.args.get('page',1)) + q = request.args.get('q','') offset = (page * limit) - limit user_id = get_user_id_from_token(token) next_page = page + 1 prev_page = page - 1 if page > 1 else 1 - all_items = BucketListItemModel.query.filter_by(bucketlist_id=bucketlist.id).all() + all_items = BucketListItemModel.query.filter( + BucketListItemModel.task.like("%" + q + "%"), + BucketListItemModel.bucketlist_id.is_(bucketlist.id) + ).all() items = all_items[offset:(limit + offset)] result = {'data':[item.get() for item in items],'page':{}} if (limit + offset) < len(all_items): diff --git a/app/views.py b/app/views.py index 5966cfd..13b526e 100644 --- a/app/views.py +++ b/app/views.py @@ -16,3 +16,8 @@ @app.route('/index') def index(): return "Hello, BucketList!" + + +@app.errorhandler(404) +def not_found(error): + return make_response(jsonify({'error': 'Resource not found ','code':404}), 404) From 91576624c9b890ee81ce18ead4155a70c8d0b5f6 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 19:43:26 +0100 Subject: [PATCH 15/29] [Chores] Removed unused method and modified read me --- README.md | 10 +++++++++- app/helper.py | 15 ++------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 6cde440..c7565e8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ -# bucketlist-api +# bucketlist API According to Merriam-Webster Dictionary, a Bucket List is a list of things that one has not done before but wants to do before dying. +This is a checkpoint 2 project used to evaluate Python beginner + +## Features and Endpoints +| Features | Endpoint +----------------------- +| Register | /auth/register + +| Authentication | /auth/login diff --git a/app/helper.py b/app/helper.py index 3cac0f7..d10d548 100644 --- a/app/helper.py +++ b/app/helper.py @@ -6,6 +6,7 @@ encrypt_key = 'bucketlists api' def save(model): + """ Save a row in the database """ try: db.session.add(model) db.session.commit() @@ -14,6 +15,7 @@ def save(model): return False def delete(model): + """ Deletes a row from the database """ try: db.session.delete(model) db.session.commit() @@ -32,19 +34,6 @@ def get_user_id_from_token(token): except: return 0 -def validate_required_fields(request, required=[]): - """ - This method helps to validate provided required fields. - It will return False if the require field is not in request - """ - if not request.json: - return False - - for field in required: - if not field in request.json: - return False - return True - def validate_args(fields={}): """ This method helps to parse and validate provided parameters. From 455d37e02529da45e22f9301254943e42398bc38 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 19:48:34 +0100 Subject: [PATCH 16/29] [Chores] Modified readme --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c7565e8..2deb36a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,17 @@ According to Merriam-Webster Dictionary, a Bucket List is a list of things that This is a checkpoint 2 project used to evaluate Python beginner ## Features and Endpoints -| Features | Endpoint ------------------------ -| Register | /auth/register - -| Authentication | /auth/login + + + + + + + + + + + + + +
Features Endpoint
Register /auth/register
Authentication/auth/login
From 9572055c34534618c05432e2acc3ca91591b7336 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 20:17:15 +0100 Subject: [PATCH 17/29] [Chores] Added How to use section to readme --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2deb36a..bd5d488 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,98 @@ According to Merriam-Webster Dictionary, a Bucket List is a list of things that one has not done before but wants to do before dying. This is a checkpoint 2 project used to evaluate Python beginner -## Features and Endpoints +## Features, Endpoints and Accessiblity + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Features Endpoint Public
Register /auth/registerPOST /auth/register True
Authentication/auth/loginPOST /auth/loginTrue
Create BucketlistPOST /bucketlists/ False
Fetch BucketlistsGET /bucketlists/ False
Fetch Single BucketlistsGET /bucketlists/:id False
Update bucketlist recordPUT /bucketlists/:id False
Delete bucketlist recordDELETE /bucketlists/:id False
Create Bucketlist ItemPOST /bucketlists/:id/items False
Fetch Bucketlists ItemsGET /bucketlists/:id/items False
Fetch Single Bucketlists itemGET /bucketlists/:id/items/:itemId False
Update bucketlist item recordPUT /bucketlists/:id/items/:itemId False
Delete bucketlist item recordDELETE /bucketlists/:id/items/:itemId False
+ +## Dependecies +All dependecies can be found in requirements.txt + +## How to use +- Clone project git clone git@github.com:andela-snwuguru/bucketlist-api.git +- Create a virtual environment `` mkvirtualenv bucketlist `` +- Install dependecies `` pip install -r requirements.txt `` +- Navigate to project folder `` cd ~/bucketlist-api `` +- Run migrationscript +``` + python script.py db makemigrations + python script.py db migrate + python script.py db upgrade +``` +- Run Project `` python run.py `` From 9add6413314b1b018a40bfe8779b5210fcbb5f6f Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 20:44:08 +0100 Subject: [PATCH 18/29] [Chores] Added more sections to readme --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd5d488..c5a9b03 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,59 @@ This is a checkpoint 2 project used to evaluate Python beginner All dependecies can be found in requirements.txt ## How to use -- Clone project git clone git@github.com:andela-snwuguru/bucketlist-api.git +- Clone project git clone `` git@github.com:andela-snwuguru/bucketlist-api.git `` - Create a virtual environment `` mkvirtualenv bucketlist `` - Install dependecies `` pip install -r requirements.txt `` - Navigate to project folder `` cd ~/bucketlist-api `` - Run migrationscript ``` - python script.py db makemigrations python script.py db migrate python script.py db upgrade ``` - Run Project `` python run.py `` + +## Sample Request + +#### Register new User +``` +Request +-------- +http POST http://127.0.0.1:5000/api/v1.0/auth/register username=guru password=test email=guru@mail.com + +Response +-------- +{ + "data": { + "date_created": "2016-05-19 19:37:20", + "date_modified": "2016-05-19 19:37:20", + "email": "guru@mail.com", + "id": 7, + "username": "guru" + } +} +``` + +#### Retrieve Access Token +``` +Request +-------- +http POST http://127.0.0.1:5000/api/v1.0/auth/login username=guru password=test + +Response +-------- +{ + "data": { + "date_created": "2016-05-19 19:37:20", + "date_modified": "2016-05-19 19:37:20", + "email": "guru@mail.com", + "id": 7, + "username": "guru" + }, + "token": "098f6bcd4621d373cade4e832627b4f6|7|.Ch-n_A._bH9Hx_kpibiIlRHvFRZbVt-6UM" +} + +``` + +### Documentation + +Api documentation is still in progress, it Will soon be available. \ No newline at end of file From 2dcb156d41ed97a48224ed6e12d4c09e2ed596ac Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 20:58:34 +0100 Subject: [PATCH 19/29] [Chores] Added travis file --- .coveragerc | 6 ++++++ .travis.yml | 10 ++++++++++ 2 files changed, 16 insertions(+) create mode 100644 .coveragerc create mode 100644 .travis.yml diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..54f1fe8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[report] +omit = + */python?.?/* + */site-packages/nose/* + *__init__* + src/migration.py \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5273fdf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.7" +# command to install dependencies +install: + - pip install -r requirements.txt +script: + - nosetests --with-coverage +after_success: + - coveralls \ No newline at end of file From cc4ad794d04904ea0ab333e97e49fafa1fcb8ab3 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 21:09:44 +0100 Subject: [PATCH 20/29] [Chores] Added test.py to run all test using coverage --- .coverage | 1 + .coveragerc | 2 +- test.py | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .coverage create mode 100644 test.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..13ad946 --- /dev/null +++ b/.coverage @@ -0,0 +1 @@ +!coverage.py: This is a private format, don't read it directly!{"lines": {"/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/views.py": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 15, 16, 21], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/test.py": [1, 3, 4], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/resource.py": [1, 2, 3, 4, 5, 7, 9, 16, 17, 20, 22, 30, 31, 42, 43, 44, 47, 49, 60, 61, 62, 65, 68, 71, 73, 86, 87, 88, 89, 92, 94, 97, 100, 102, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 129, 132, 134, 147, 148, 149, 150, 153, 154, 155, 157, 160, 162, 184, 195, 196, 197, 200, 201, 204, 207, 209, 223, 224, 225, 226, 229, 230, 233, 234, 235, 237, 240, 243, 245, 259, 260, 261, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 275, 276, 277, 280, 283, 285, 298, 299, 300, 301, 304, 305, 308, 312, 313, 324, 325, 327, 330, 333, 335, 348, 349, 350, 353], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/helper.py": [1, 2, 3, 4, 6, 8, 10, 11, 12, 13, 14, 15, 17, 19, 20, 21, 22, 27, 31, 32, 33, 37, 42, 43, 44, 45, 47, 49, 53, 55, 60, 61, 62, 66, 71, 72, 73], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/test_config.py": [1, 2, 4, 6], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/models.py": [1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 46, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65, 66, 68, 69, 70, 71, 73, 76, 77, 78, 79, 80, 81, 82, 85, 86, 87], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/production_config.py": [1, 2, 4, 6]}} \ No newline at end of file diff --git a/.coveragerc b/.coveragerc index 54f1fe8..0a3814c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [report] omit = */python?.?/* + tests/* */site-packages/nose/* *__init__* - src/migration.py \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..9ea02e4 --- /dev/null +++ b/test.py @@ -0,0 +1,4 @@ +import unittest + +tests = unittest.TestLoader().discover('tests') +unittest.TextTestRunner().run(tests) \ No newline at end of file From 558297b6b9239eda3cb0f99108549bcaf4becabb Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 21:12:47 +0100 Subject: [PATCH 21/29] [Chores] Added requirements.txt file --- requirements.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2a8828e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +alembic==0.8.6 +aniso8601==1.1.0 +coverage==4.0.3 +coveralls==1.1 +docopt==0.6.2 +Flask==0.10.1 +Flask-HTTPAuth==3.1.2 +Flask-Migrate==1.8.0 +Flask-RESTful==0.3.5 +Flask-Script==2.0.5 +Flask-SQLAlchemy==2.1 +itsdangerous==0.24 +Jinja2==2.8 +Mako==1.0.4 +MarkupSafe==0.23 +pycrypto==2.6.1 +python-dateutil==2.5.3 +python-editor==1.0 +pytz==2016.4 +requests==2.10.0 +six==1.10.0 +SQLAlchemy==1.0.12 +Werkzeug==0.11.9 From 27b8fc294e0515b6b293aec26ba951212e206b14 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 21:16:34 +0100 Subject: [PATCH 22/29] [Chores] Modified travis file --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5273fdf..a9ef7bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,6 @@ python: install: - pip install -r requirements.txt script: - - nosetests --with-coverage + - coverage test.py after_success: - coveralls \ No newline at end of file From 32f2b27cbb09b5df17b42dfad52cc72d1496b62e Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 21:20:14 +0100 Subject: [PATCH 23/29] [Chores] Modified travis file --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a9ef7bc..05843d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: install: - pip install -r requirements.txt script: - - coverage test.py + - python run.py + - nosetests --with-coverage after_success: - coveralls \ No newline at end of file From 799c508e6dc47473f36a6d637c5801c9e5792584 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 21:31:06 +0100 Subject: [PATCH 24/29] [Chores] Modified travis file --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 05843d9..5273fdf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ python: install: - pip install -r requirements.txt script: - - python run.py - nosetests --with-coverage after_success: - coveralls \ No newline at end of file From 0e7b417645b42b9ba2c7d92077f3b23bdf84b850 Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Thu, 19 May 2016 21:34:08 +0100 Subject: [PATCH 25/29] [Chores] Modified gitignore --- .gitignore | 1 - app.db | Bin 0 -> 32768 bytes test.db | Bin 0 -> 32768 bytes 3 files changed, 1 deletion(-) create mode 100644 app.db create mode 100644 test.db diff --git a/.gitignore b/.gitignore index 903b579..64fb82a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ *.pyc */*.pyc -*.db diff --git a/app.db b/app.db new file mode 100644 index 0000000000000000000000000000000000000000..b5b93fbb9c79f9caff6f94f691382c545b15fe1f GIT binary patch literal 32768 zcmeI5Pi)&%7{H(7Hg2;eZ%|5yIP6W+Sfj41|HMw5ZK7Gpwz4i`SvzS+D05<`jn+62 zCyeg44Z&$3G-)@kNFYvJ5EB;=+7UQ12_b<5+64)T3n#?)+%~q`w4Fdg+w>)IVt?;_ z@B6-=pR4+NFE1C)&ow#@tGC)stHU(1hX?|hW{eP`!Zrlk`a^=9f%OCU6hix3?W$z* zx4~rUeIg|u5c)yl0k!GTq@0@Db$=@rlp_HofCP{L5IQ zcCk>NIbA4^E3%yC1y0jyEcc|UFSV_XQ>nEb_^{baU}JvvZ1FJj#m$!8s5c_=*DcSx z+G^XOR{W-4DbJoQlrOO}#Y=3wVf(U$((L(#A{+P3MkMPVrW5HK^O7`Xn!?*Ne!$gk z?TXW>EH}JP1^(f0R=@{mxxaKM0;PYQZ7_8#?@ADcuKOO^E!SZu&Yhbp7D^o17I@YQ z`T$A{uR5|CLGJ|dw)!zTd9GZXJyqh|#v_VpRxX|_mW!pC;sq9t%?HwBJaf7@bB2xk zZqJt3xO|u?X^!=|L1}DiO1Nol!n%p5FNkjmbIstE-7V79)HRN4J*rY&S?{9%lIBj( z0z(mEtCz@zj>tU)HidY6Y+*_u4cB(w^4?g6epa!%otA%G*~Ge{Y!XTI7WtH4rZ@Ee z=`!u#dmicvJ2VgSo;75-o-I|X=qGeK^;7CvYA+n%0|_7jB!C2v z01`j~NB{{S0VIF~9vuR&NfH@5plN3pyD%wbu{+J@pd6{SnyRcAld?Xkm`pMAS~joB zeMGXEs~gptoiS9!*0Ng7vYm{R(^NyvRx@?uavYjZL-QRzGVQ-@wvc72oY#!J99RfM zJ6Wg>f`!S|0Jc>1sx`STo2D&miUyx` zrDp4ytZ7~zkjUr+kHYafeA~i;k=Fs09=5P1+R4I_D3SeBV8QFUwzcAi;qPr}DFfkX zQA@rktLycgUQ@HGR;_BLmQ!k3Eo)kaoKXzf&Y7_|8QHICcFTj;v}Z?7w!#!859eDC zFa;v$wa<@Eg?zJMhQ_kR8%RJ|oZgDjAL%XnA-zXGqrX2oog6(u0!RP}AOR$R1dsp{ zKmter2_OL^@T3uVT1->%fDp>$NY99qa2!pH433Bs!iH4Fz^FJStR^5rX$LVS?iYeo zL5vVEAw*L-@dW(~?*H$?{C|3veoKF*KhSTUG_n{E5gG1t|pCuUJNq}H(f+&e2JSPCj05Zf=3(3^?L`>Z# z^lSQV>h{|6)yJ0#dX5B;01`j~NB{{S0VIF~kN^@u0uM=mCdmy`(6p-!Z;A267THp( z?XudE4*nV$qbY;OjYynrGQK@i#`xRl%H=>&V`k^+eRL~=dk`giPcEB2$ZjF zQqFEv*7Hi(GH;KIK>7G4Wn-gq1X#e3PwqbmG1uFzChIIYthH>%dL9Nd`TK~D330M- zxS%yxm<>xUE_$IlvYA)(yp}tXQ*zl%rcX(NeoRDqhtOZ>pY#qqm-zD``wCV=0!RP} zAOR$R1dsp{Kmter2_OL^fCM50=$GV3|CEXdMI-b(LW4w!#3$+pwc)nij6-W?M3fr@ z(F+k#mj8mlNJM0ad&Gl6B(1Pp+&A1y7V!1|Q@igI`W1bLmguvv2H;02H)SOMPTott zm%NzV3x)VV0!RP}AOR$R1dsp{Kmter2|UUKj#G(D3~5^EKFY3pb}%{$%j3a{Xg^SnYSDn->I^iu9sQ3T7r3hh4SpVTV^>pHF#ZoeN*Y>qFev#4jEu(I literal 0 HcmV?d00001 diff --git a/test.db b/test.db new file mode 100644 index 0000000000000000000000000000000000000000..45d62d131c1dddd659adf7c6aba495abbe2df15c GIT binary patch literal 32768 zcmeI*T~FFj7zglH5rj?ImN>GzlPrr6b@T;M^kO1bB_hrWOT20dElDE<2W6X=>TX@K zFR-t(>wSpra@i7--R_(URkUo0iOF>T#v|=>PVISqIfvL6WoNr!IVPz!Tej{HnR~$T zJhwy$$8o2$jMCCu#^_3*w?SX|3+sbcPq~?IBV*AME_Ck$7yUTC5;dZW_rH$wbQ22% zAOHafKmY;|fB*z;Odxs^*ojR{@W(G4y;3(jZL`J7a86aSC54o-%LRq72#H6+mO=8x zlCq|#q_|Zg#d4uAO(J1d-Oz25yvnM%^{g5f1tGyIOk20=UQ}|UhVGbJwPn&5gRIbF zl=7R(G;zar)39n*UwmJ0x8F98N}*6Jli}az++J2X)jiYE>Q>v)=x^z^Mt3yp{?XCC zO#IvYAvZ^F@118bJL~3UG#e&a-r6cC*&@qxKu+BXpR6L`vyPl);Hk2o^-|4mX4PG? zq3n{lWw^&%-BOkOT9L)#eLE(Is;nxiQp_nkYAE=LpwiEpV-;B?jo0q z`R%eo;;g6CCL0XK%1NHH8ix6<{kl%anWlG~rn{|OvDU;Zq0pf-6msX*#tqD^vx(6% z&IjkSx+BawoPsG<%WWdjOT23Uhx&%;rAjv0ySr(Pj_vu$Qo?&zizL`dJsc*RP~x+7?~71|ED+ox#*`mk|5H600bZa z0SG_<0uX=z1Rwwb2;4@2N^CPB%MN=!K*-G3(v_-_N=u?4&&gHYFjMBdETyHnN~)H2 z8!fSCk)BtZwj_w@8DVxt%n(Uf5at$yluszaOxXGV_uGhrK0^Qk5P$##AOHafKmY;| zfB*y_;4AQ`|0;oZ{{PFjHG&X;00bZa0SG_<0uX=z1Rwwb2;6FcuzMlEd;b6Ptxg7= zhX4d1009U<00Izz00bZa0SMf6fh934(;PETIr?`3X4@g`UxgCU=V5=Jkaz$8=Urz) zeh`2F1Rwwb2tWV=5P$##AOL~?MBp*KJU~;8Ubsr&-T(i>g=kffmJwRmM-dFuf&~H) wfB*y_009U<00Izz00bZafqz3_DLE?3^n*4Jh)(ufv` Date: Fri, 20 May 2016 12:42:59 +0100 Subject: [PATCH 26/29] [Chores] Improved test cpverage --- .coverage | 2 +- app/models.py | 12 +++---- app/resource.py | 2 +- test.db | Bin 32768 -> 32768 bytes tests/.coverage | 1 + tests/test_bucketlists.py | 10 +++++- tests/test_bucketlists_items.py | 40 +++++++++++++++++++++-- tests/test_edge_cases.py | 56 ++++++++++++++++++++++++++++++++ 8 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 tests/.coverage create mode 100644 tests/test_edge_cases.py diff --git a/.coverage b/.coverage index 13ad946..4afc19f 100644 --- a/.coverage +++ b/.coverage @@ -1 +1 @@ -!coverage.py: This is a private format, don't read it directly!{"lines": {"/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/views.py": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 15, 16, 21], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/test.py": [1, 3, 4], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/resource.py": [1, 2, 3, 4, 5, 7, 9, 16, 17, 20, 22, 30, 31, 42, 43, 44, 47, 49, 60, 61, 62, 65, 68, 71, 73, 86, 87, 88, 89, 92, 94, 97, 100, 102, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 129, 132, 134, 147, 148, 149, 150, 153, 154, 155, 157, 160, 162, 184, 195, 196, 197, 200, 201, 204, 207, 209, 223, 224, 225, 226, 229, 230, 233, 234, 235, 237, 240, 243, 245, 259, 260, 261, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 275, 276, 277, 280, 283, 285, 298, 299, 300, 301, 304, 305, 308, 312, 313, 324, 325, 327, 330, 333, 335, 348, 349, 350, 353], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/helper.py": [1, 2, 3, 4, 6, 8, 10, 11, 12, 13, 14, 15, 17, 19, 20, 21, 22, 27, 31, 32, 33, 37, 42, 43, 44, 45, 47, 49, 53, 55, 60, 61, 62, 66, 71, 72, 73], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/test_config.py": [1, 2, 4, 6], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/models.py": [1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 46, 49, 50, 51, 52, 53, 54, 55, 56, 60, 61, 62, 63, 64, 65, 66, 68, 69, 70, 71, 73, 76, 77, 78, 79, 80, 81, 82, 85, 86, 87], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/production_config.py": [1, 2, 4, 6]}} \ No newline at end of file +!coverage.py: This is a private format, don't read it directly!{"lines": {"/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/views.py": [1, 2, 3, 4, 6, 8, 9, 10, 11, 12, 13, 15, 16, 18, 21, 23], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/test.py": [1, 3, 4], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/resource.py": [1, 2, 3, 4, 5, 7, 9, 16, 17, 18, 20, 22, 28, 30, 31, 42, 43, 44, 45, 47, 49, 60, 61, 62, 63, 65, 66, 68, 71, 73, 86, 87, 88, 89, 90, 92, 94, 97, 100, 102, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 129, 132, 134, 147, 148, 149, 150, 153, 154, 155, 157, 160, 162, 173, 174, 175, 178, 179, 182, 184, 195, 196, 197, 198, 200, 201, 204, 207, 209, 223, 224, 225, 226, 227, 229, 230, 233, 234, 235, 237, 240, 243, 245, 259, 260, 261, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 275, 276, 277, 280, 283, 285, 298, 299, 300, 301, 302, 304, 305, 308, 312, 313, 324, 325, 327, 328, 330, 333, 335, 348, 349, 350, 351, 353], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/helper.py": [1, 2, 3, 4, 6, 8, 10, 11, 12, 13, 14, 15, 17, 19, 20, 21, 22, 23, 24, 27, 31, 32, 33, 34, 35, 37, 42, 43, 44, 45, 47, 49, 53, 55, 60, 61, 62, 63, 64, 66, 71, 72, 73, 74, 75], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/test_config.py": [1, 2, 4, 6], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/models.py": [1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 24, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 54, 55, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, 69, 71, 72, 74, 75, 76, 77, 78, 79, 80, 83, 84, 85], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/production_config.py": [1, 2, 4, 6]}} \ No newline at end of file diff --git a/app/models.py b/app/models.py index 93143db..ef8cac3 100644 --- a/app/models.py +++ b/app/models.py @@ -24,10 +24,8 @@ def get(self): 'date_modified':str(self.date_modified) } - def __repr__(): - return '' % self.name - - + def __repr__(self): + return '' % self.name class BucketListItemModel(db.Model): @@ -43,9 +41,6 @@ def __init__(self, task, bucketlist): self.task = task self.bucketlist = bucketlist - def __repr__(): - return '' % self.name - def get(self): return { 'id':self.id, @@ -56,6 +51,9 @@ def get(self): 'date_modified':str(self.date_modified) } + def __repr__(self): + return '' % self.task + class User(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/app/resource.py b/app/resource.py index a6ebd92..33d1240 100644 --- a/app/resource.py +++ b/app/resource.py @@ -325,7 +325,7 @@ def post(self): user = User.query.filter_by(username=args['username'],password=md5(args['password'])).first() if not user: - abort(403) + abort(403, message='Invalid user credentials') return {'token': user.generate_token(),'data': user.get()}, 200 diff --git a/test.db b/test.db index 45d62d131c1dddd659adf7c6aba495abbe2df15c..6fec5c41a218db304ec18600539ef7d26fa6156b 100644 GIT binary patch delta 122 zcmZo@U}|V!njpo*SUOS02}o{CXwsK6GEguyv@$WUGO;u=Ff`LOFx5p8+GOyEppt|~ I@&rr)02*8zJ^%m! delta 122 zcmZo@U}|V!njpo*@MWTm6Oi1P(4;SCXsKXiU}a!#Wnf}tU}&amV5*BGw8`KRK_v-~ I Date: Fri, 20 May 2016 12:55:17 +0100 Subject: [PATCH 27/29] [Chores] Added badges to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c5a9b03..52f6b6f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # bucketlist API +[![Build Status](https://travis-ci.org/andela-snwuguru/bucketlist-api.svg?branch=ch-ci-integration)](https://travis-ci.org/andela-snwuguru/bucketlist-api) [![Coverage Status](https://coveralls.io/repos/github/andela-snwuguru/bucketlist-api/badge.svg?branch=ch-ci-integration)](https://coveralls.io/github/andela-snwuguru/bucketlist-api?branch=ch-ci-integration) + According to Merriam-Webster Dictionary, a Bucket List is a list of things that one has not done before but wants to do before dying. This is a checkpoint 2 project used to evaluate Python beginner From cd044b6995121ebbb90224f099d3841555f7fbcb Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 24 May 2016 17:00:36 +0100 Subject: [PATCH 28/29] [Bug] Corrected a mis-spelt key word --- app.db | Bin 32768 -> 32768 bytes app/resource.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app.db b/app.db index b5b93fbb9c79f9caff6f94f691382c545b15fe1f..06ff1abd8a5287ac364eb0fad95eea8dc70f94c7 100644 GIT binary patch delta 1025 zcmaiy&ui0Q9LC=?V{f)K`KDvaoJxZ`iF71+6Vru_RJ^D~Wgb0D`^zrcT3HjuL`iQu z4U`v#7r}!Udy{~$9X)tcdhjTMJ9rlS2TW4iC0(7j_x-$lpZ9&9=k0V-olfe-S!U(s zxvR{|`}=RRvzRX;=t4pGE<6?H`F+0VXGER9)!;B%)CsxSb{|(u+jV!-Rgad1db4CV zmMX2LDl3x+d~AOgD>n@~A=aQns_=6eV}!j3=z3wMMJ-{X6MnHKK}J#_sF zLx@vrJCam(E01c9)Ubn1vsJYp*tIG*^60#&<&#L$Ok!9wlSml|>2akI0ZlPwc_85R z!-<$R9aGRwQjkr3BuXtr)>KS@{6|2Eo*)eS2^ADd*HdlEOiw*Qk1U&t zF|hQv?hqk_tlS`zShgdHkmHeth{@BPd(Y=EtRXmr-|zuE*nlxKdqxJMEJp}acDJRH^swe6v(yYz?bmhGR){!5Obk{^o5u!a UG6r$0fYGJ!#?XiV^VtIX2U2tl&;S4c delta 579 zcmZo@U}|V!njp<+G*QNx(P(4B5_thO-kl8mtN0!G?(tReN$~F6EGV#)ck@nNZzcgY z{$3{jJN&);bNP?)Z{mLolq%zA7Gl<%+-qMBl->k~D2xKi{pPLb=VfTJWHdG|&n(VR$jK}&QOL+I%2P6ZF<2@B4FYQ~F3-$MM`#ACGz4n4 zMAK}_3^a4{dymm*a(jHO5HtTY2L4CMO21*I@pJ(7l z1OPiQS{M!;MaY5`D5QoWWDW`qVH6=VW&vhZ#Fgp0Er@)<^TWy diff --git a/app/resource.py b/app/resource.py index 33d1240..1912b2f 100644 --- a/app/resource.py +++ b/app/resource.py @@ -232,7 +232,7 @@ def put(self, id, item_id): item.task = args.get('task',item.task) done = args.get('done',False) - item.done = True if done == 'True' else Fals + item.done = True if done == 'True' else False if not save(item): abort(409, message="Unable to update record") From b12bcf9f6c3df256dd8905048b3e615030c0636a Mon Sep 17 00:00:00 2001 From: sunday Nwuguru Date: Tue, 24 May 2016 17:32:59 +0100 Subject: [PATCH 29/29] [Bug] Modified logic for updating done column to true or false --- app.db | Bin 32768 -> 32768 bytes app/resource.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.db b/app.db index 06ff1abd8a5287ac364eb0fad95eea8dc70f94c7..dbaacc21e9571b7a6f643863d0a3c7101a2fcc65 100644 GIT binary patch delta 91 zcmZo@U}|V!njp<+H&Mo!(QaeH!hANK1q=c_DwEj@(l-kV{Na{x6kumC<}@~D vOe(EZNK`1#&r3-yN-NEoysk!>!_3Oq(8}0^V{&?pB?|)sgXQKWH8IQpdtezW delta 53 zcmZo@U}|V!njp<+Gf~Ew(Pm@9!hALc1_lA1^vUc6a?A^O(l-kV*z-(Iud!q?7hq@D Jyrd?E832gS4paaD diff --git a/app/resource.py b/app/resource.py index 1912b2f..624f124 100644 --- a/app/resource.py +++ b/app/resource.py @@ -231,8 +231,8 @@ def put(self, id, item_id): abort(400, message="Item does not exist") item.task = args.get('task',item.task) - done = args.get('done',False) - item.done = True if done == 'True' else False + done = args.get('done','false') + item.done = True if done.lower() == 'true' else False if not save(item): abort(409, message="Unable to update record")