diff --git a/.coverage b/.coverage
new file mode 100644
index 0000000..4afc19f
--- /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, 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/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..0a3814c
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[report]
+omit =
+ */python?.?/*
+ tests/*
+ */site-packages/nose/*
+ *__init__*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..64fb82a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pyc
+*/*.pyc
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
diff --git a/README.md b/README.md
index 6cde440..52f6b6f 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,146 @@
-# bucketlist-api
+# 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
+
+## Features, Endpoints and Accessiblity
+
+
+ Features |
+ Endpoint |
+ Public |
+
+
+ Register |
+ POST /auth/register |
+ True |
+
+
+Authentication |
+POST /auth/login |
+True |
+
+
+
+Create Bucketlist |
+POST /bucketlists/ |
+False |
+
+
+
+Fetch Bucketlists |
+GET /bucketlists/ |
+False |
+
+
+
+Fetch Single Bucketlists |
+GET /bucketlists/:id |
+False |
+
+
+
+Update bucketlist record |
+PUT /bucketlists/:id |
+False |
+
+
+
+Delete bucketlist record |
+DELETE /bucketlists/:id |
+False |
+
+
+
+Create Bucketlist Item |
+POST /bucketlists/:id/items |
+False |
+
+
+
+Fetch Bucketlists Items |
+GET /bucketlists/:id/items |
+False |
+
+
+
+Fetch Single Bucketlists item |
+GET /bucketlists/:id/items/:itemId |
+False |
+
+
+
+Update bucketlist item record |
+PUT /bucketlists/:id/items/:itemId |
+False |
+
+
+
+Delete bucketlist item record |
+DELETE /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 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
diff --git a/app.db b/app.db
new file mode 100644
index 0000000..dbaacc2
Binary files /dev/null and b/app.db differ
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e815af4
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,11 @@
+from flask import Flask
+from flask_sqlalchemy import SQLAlchemy
+from flask import request
+
+app = Flask(__name__)
+
+#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
new file mode 100644
index 0000000..d10d548
--- /dev/null
+++ b/app/helper.py
@@ -0,0 +1,75 @@
+from flask_restful import reqparse
+import hashlib
+from itsdangerous import TimestampSigner
+from app import db
+
+encrypt_key = 'bucketlists api'
+
+def save(model):
+ """ Save a row in the database """
+ try:
+ db.session.add(model)
+ db.session.commit()
+ return True
+ except:
+ return False
+
+def delete(model):
+ """ Deletes a row from the database """
+ try:
+ db.session.delete(model)
+ db.session.commit()
+ return True
+ except:
+ return False
+
+
+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_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
+ """
+ return hashlib.md5(string.encode("utf")).hexdigest()
+
+def encrypt(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=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)
+ except:
+ return False
diff --git a/app/models.py b/app/models.py
new file mode 100644
index 0000000..ef8cac3
--- /dev/null
+++ b/app/models.py
@@ -0,0 +1,85 @@
+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)
+ name = db.Column(db.String(255), unique=True)
+ 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())
+ user = db.relationship('User', backref=db.backref('bucketlists', lazy='dynamic'))
+
+ 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__(self):
+ 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=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, bucketlist):
+ self.task = task
+ self.bucketlist = bucketlist
+
+ 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)
+ }
+
+ def __repr__(self):
+ return '' % self.task
+
+
+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, password):
+ self.username = username
+ self.email = email
+ self.password = password
+
+ def __repr__(self):
+ 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),
+ }
+
+ def generate_token(self):
+ string = str(self.password) + '|' + str(self.id) + '|'
+ return encrypt(string)
diff --git a/app/resource.py b/app/resource.py
new file mode 100644
index 0000000..624f124
--- /dev/null
+++ b/app/resource.py
@@ -0,0 +1,357 @@
+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()
+
+@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):
+ """
+ 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:
+ abort(403, message="Unauthorized access")
+
+ return {'data':bucketlist.get()}
+
+ @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:
+ 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")
+
+ return {}, 204
+
+ @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()
+ if not bucketlist:
+ abort(403, message="Unauthorized access")
+
+ bucketlist.name = args['name']
+
+ if not save(bucketlist):
+ abort(409, message="Item already exists")
+
+ return {'data':bucketlist.get()}, 201
+
+
+class BucketLists(Resource):
+
+ @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))
+ 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(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):
+ 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):
+ """
+ This endpoint returns created bucketlist details.
+ Method: POST
+ Parameters:
+ name (required)
+ Header:
+ AccessToken (required)
+
+ Response: JSON
+ """
+
+ 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(args['name'], user)
+ if not save(bucketlist):
+ abort(409, message="Item already exists")
+
+ return {'data': bucketlist.get()}, 201
+
+
+class BucketListItem(Resource):
+
+ @auth.login_required
+ def get(self, 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):
+ """
+ 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):
+ """
+ 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.lower() == 'true' else False
+
+ 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):
+ """
+ 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))
+ 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(
+ 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):
+ 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):
+ """
+ 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
+
+
+
+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
+ """
+
+ 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, message='Invalid user credentials')
+
+ 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
+ """
+
+ args = validate_args({'username':True, 'password':True, 'email':True})
+ user = User(args['username'], args['email'], md5(args['password']))
+ if not save(user):
+ abort(401, message="User already exists")
+
+ return {'data': user.get()}, 201
+
+
+
+
diff --git a/app/views.py b/app/views.py
new file mode 100644
index 0000000..13b526e
--- /dev/null
+++ b/app/views.py
@@ -0,0 +1,23 @@
+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, 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')
+api.add_resource(BucketListItem, '/bucketlists//items/')
+
+@app.route('/')
+@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)
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/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/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/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/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 ###
diff --git a/production_config.py b/production_config.py
new file mode 100644
index 0000000..ca33db6
--- /dev/null
+++ b/production_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/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
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
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
diff --git a/test.db b/test.db
new file mode 100644
index 0000000..6fec5c4
Binary files /dev/null and b/test.db differ
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
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/.coverage b/tests/.coverage
new file mode 100644
index 0000000..c2d1203
--- /dev/null
+++ b/tests/.coverage
@@ -0,0 +1 @@
+!coverage.py: This is a private format, don't read it directly!{"lines": {"/Library/Python/2.7/site-packages/nose/importer.py": [143, 144, 146, 147, 148, 149, 151, 152, 153, 154, 155, 156, 157, 158, 32, 161, 40, 41, 42, 44, 45, 47, 30, 54, 59, 62, 53, 65, 66, 67, 68, 70, 71, 72, 75, 76, 77, 78, 79, 80, 81, 94, 96, 97, 98, 63], "/Library/Python/2.7/site-packages/nose/loader.py": [128, 131, 179, 134, 135, 151, 143, 144, 145, 146, 147, 404, 149, 150, 89, 409, 154, 156, 90, 158, 159, 160, 161, 418, 91, 420, 421, 406, 424, 92, 170, 455, 178, 157, 177, 434, 435, 436, 181, 182, 183, 180, 186, 447, 416, 196, 197, 454, 481, 427, 201, 417, 79, 81, 82, 83, 84, 85, 86, 473, 474, 475, 476, 93, 94, 95, 97, 99, 422, 425, 209, 210, 369, 371, 410, 374, 375, 212, 378, 379, 200, 426, 405], "/Library/Python/2.7/site-packages/nose/plugins/isolate.py": [61, 62], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/tests/test_bucketlists.py": [1, 2, 3, 4, 5], "/Library/Python/2.7/site-packages/nose/pyversion.py": [70, 175], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/tests/test_bucketlists_items.py": [1, 2, 3, 4, 5], "/Library/Python/2.7/site-packages/nose/plugins/debug.py": [40, 41, 42, 43], "/Library/Python/2.7/site-packages/nose/suite.py": [539, 540, 543, 544, 545, 546, 547, 548, 551, 552, 554, 52, 53, 68, 72, 73, 75, 76, 79, 80, 81, 94, 95, 96, 97, 98, 99, 100, 103, 104, 105, 106, 107, 113, 114, 148, 149, 150, 151, 153, 154, 155, 156, 157, 158, 173, 177, 201, 204, 205, 208, 209, 216, 217, 218, 224, 226, 227, 228, 269, 270, 274, 277, 278, 279, 282, 283, 285, 286, 287, 288, 289, 290, 291, 292, 293, 297, 298, 301, 302, 303, 304, 308, 309, 310, 312, 313, 314, 315, 323, 324, 325, 326, 327, 328, 329, 330, 331, 337, 338, 339, 340, 341, 342, 345, 346, 347, 348, 349, 350, 351, 356, 357, 358, 360, 361, 362, 364, 365, 366, 367, 368, 372, 373, 374, 394, 396, 397, 401, 402, 403, 404, 405, 406, 407, 418, 419, 420, 421, 422, 423, 424, 427, 435, 436, 441, 443, 445, 446, 451, 452, 453, 454, 457, 459, 460, 462, 463, 464, 465, 466, 471, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486], "/Library/Python/2.7/site-packages/nose/plugins/doctests.py": [192, 193, 194, 195, 188, 189, 190, 191], "/Library/Python/2.7/site-packages/nose/plugins/failuredetail.py": [33, 35, 36], "/Library/Python/2.7/site-packages/nose/plugins/logcapture.py": [34, 38, 39, 40, 41, 44, 178, 179, 193, 194, 195, 196, 198, 199, 76, 204, 77, 78, 207, 80, 209, 86, 217, 79, 222, 208, 233, 234, 235, 240], "/Library/Python/2.7/site-packages/nose/result.py": [158, 159, 38, 39, 40, 41, 43, 44, 182, 183, 186, 187, 60, 61, 65, 69, 75, 76, 77, 81, 82, 88, 89, 90, 97, 98, 59, 103, 104, 105, 106, 109, 110], "/Library/Python/2.7/site-packages/nose/selector.py": [129, 130, 131, 134, 135, 35, 37, 40, 41, 42, 43, 44, 45, 53, 57, 244, 222, 224, 225, 226, 227, 228, 229, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 116, 117, 118, 119, 123, 126, 127], "/Library/Python/2.7/site-packages/nose/plugins/deprecated.py": [40, 42, 43, 44], "/Library/Python/2.7/site-packages/nose/util.py": [320, 278, 263, 520, 521, 522, 267, 524, 526, 527, 272, 529, 530, 323, 660, 662, 663, 446, 312, 163, 164, 470, 321, 264, 306, 307, 308, 309, 310, 265, 184, 313, 266, 187, 188, 189, 190, 319, 192, 279, 322, 195, 471, 140, 311, 337, 338, 339, 340, 270, 342, 343, 271, 479, 481, 483, 484, 485, 486, 273, 449, 448, 318, 502, 503, 504, 276, 506, 447], "/Library/Python/2.7/site-packages/nose/plugins/prof.py": [71, 74, 75, 76, 80, 81, 82, 83, 84, 57], "/Library/Python/2.7/site-packages/nose/proxy.py": [128, 129, 130, 131, 132, 133, 30, 168, 169, 170, 43, 45, 46, 47, 176, 177, 178, 57, 58, 59, 60, 62, 64, 78, 80, 81, 82, 83, 89, 96, 102, 103, 104, 110, 111, 112, 116, 117, 118, 122, 123, 124, 125], "/Library/Python/2.7/site-packages/nose/plugins/testid.py": [137, 138, 142, 143, 144, 145, 148, 149, 150, 151, 154, 155], "/Library/Python/2.7/site-packages/nose/plugins/cover.py": [164, 263, 173, 271, 182, 183], "/Library/Python/2.7/site-packages/nose/plugins/skip.py": [57, 59, 60, 61], "/Library/Python/2.7/site-packages/nose/plugins/capture.py": [64, 96, 98, 69, 102, 97, 80, 74, 75, 76, 111, 112, 58, 59, 101], "/Library/Python/2.7/site-packages/nose/plugins/base.py": [98, 100, 101, 102], "/Library/Python/2.7/site-packages/nose/core.py": [65, 34, 36, 37, 41, 42, 43, 44, 50, 51, 55, 56, 59, 188, 61, 62, 193, 66, 199, 200, 201, 202, 203, 204, 205, 207, 187, 60], "/Library/Python/2.7/site-packages/nose/failure.py": [37, 38, 39, 22, 23, 24, 25, 26, 27, 30, 31], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/tests/test_edge_cases.py": [1, 2, 3, 4, 5], "/Library/Python/2.7/site-packages/nose/plugins/manager.py": [128, 262, 263, 264, 265, 138, 139, 140, 141, 142, 143, 272, 273, 274, 149, 167, 134, 166, 295, 168, 301, 302, 125, 177, 178, 137, 184, 249, 88, 89, 93, 94, 95, 96, 144, 99, 252, 106, 107, 111, 114, 123, 118, 105, 120, 121, 250, 251, 124, 253, 254], "/Users/andela/Documents/projects/flaskapps/bucketlist-api/app/__init__.py": [1], "/Library/Python/2.7/site-packages/nose/plugins/xunit.py": [192, 193, 191], "/Library/Python/2.7/site-packages/nose/plugins/errorclass.py": [140, 141, 143, 144, 148, 150, 151, 152, 153, 154], "/Library/Python/2.7/site-packages/nose/plugins/multiprocess.py": [224, 225, 226, 227, 231, 233, 234, 235, 238, 223], "/Library/Python/2.7/site-packages/nose/case.py": [128, 129, 130, 131, 132, 133, 51, 140, 173, 147, 148, 149, 151, 154, 155, 29, 159, 160, 33, 34, 163, 36, 37, 38, 39, 40, 41, 42, 172, 45, 48, 49, 178, 179, 185, 59, 60, 64, 69, 70, 161, 74, 162, 99, 100, 101, 102, 103, 104, 167, 168, 166]}}
\ No newline at end of file
diff --git a/tests/test_bucketlists.py b/tests/test_bucketlists.py
new file mode 100644
index 0000000..9939b88
--- /dev/null
+++ b/tests/test_bucketlists.py
@@ -0,0 +1,140 @@
+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 TestBucketlistResources(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)
+ 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 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)
+ 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 new_bucketlist(self):
+ """ 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)
+ 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)
+
+ 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 get_bucketlists(self):
+ """ 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)
+
+ def get_single_bucketlist(self):
+ """ Get bucketlists single item """
+
+ get_bucketlists = self.app.get('/api/v1.0/bucketlists/0',
+ headers={'AccessToken':self.token})
+ self.assertEqual(get_bucketlists.status_code, 403)
+
+ 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)
+
+ def update_bucketist(self):
+ """ Update bucketlists """
+
+ put_bucketlists = self.app.put('/api/v1.0/bucketlists/0',
+ data={'name':'test item modified'}, headers={'AccessToken':self.token})
+ self.assertEqual(put_bucketlists.status_code, 403)
+
+ 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')
+ self.assertEqual(str(data['name']), 'test item modified')
+
+ 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..aeaf671
--- /dev/null
+++ b/tests/test_bucketlists_items.py
@@ -0,0 +1,180 @@
+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 TestBucketlistItemResources(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)
+
+ duplicate_response = self.app.post('/api/v1.0/auth/register',data=user)
+ self.assertEqual(duplicate_response.status_code, 401)
+
+ 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':'tuserfake', 'password':'test'}
+ response = self.app.post('/api/v1.0/auth/login',data=user)
+ self.assertEqual(response.status_code, 403)
+
+ 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/0' + '/items',
+ data={'task':'buy a private jet'},
+ headers={'AccessToken':self.token})
+ self.assertEqual(new_bucketlist_item.status_code, 403)
+
+ 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/0' + '/items/' + str(self.bucketlist_item_id),
+ data={'done':True}, headers={'AccessToken':self.token})
+ self.assertEqual(put_item.status_code, 403)
+
+ 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_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, 400)
+
+ del_bucketlists = self.app.delete('/api/v1.0/bucketlists/0',
+ headers={'AccessToken':self.token})
+ self.assertEqual(del_bucketlists.status_code, 403)
+
+ def delete_item(self):
+ """ Delete bucketlist item """
+
+ del_item = self.app.delete('/api/v1.0/bucketlists/0'+ '/items/' + str(self.bucketlist_item_id),
+ headers={'AccessToken':self.token})
+ self.assertEqual(del_item.status_code, 403)
+
+ 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.get_single_item()
+ self.delete_bucketlist()
+ self.delete_item()
+
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
new file mode 100644
index 0000000..026016e
--- /dev/null
+++ b/tests/test_edge_cases.py
@@ -0,0 +1,56 @@
+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 *
+
+
+class TestResources(unittest.TestCase):
+ """Test cases for Bucketlist Items"""
+
+ 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 test_helper_delete_exception(self):
+ bucketlist = BucketListModel('unknown', None)
+ self.assertEqual(delete(bucketlist), False)
+
+ def test_helper_get_user_id_from_token_exception(self):
+ self.assertEqual(get_user_id_from_token('bucketlist'), 0)
+
+ def test_helper_encrypt_exception(self):
+ self.assertEqual(encrypt(False), False)
+
+ def test_helper_decrypt_exception(self):
+ self.assertEqual(decrypt(False), False)
+
+ def test_model_repr(self):
+ user = User('guru','','')
+ bucketlist = BucketListModel('test', user)
+ bucketlist_item = BucketListItemModel('test', bucketlist)
+ print user, bucketlist, bucketlist_item
+
+ def test_view_index(self):
+ response = self.app.get('/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_view_not_found(self):
+ response = self.app.get('/unknown')
+ self.assertEqual(response.status_code, 404)
+
+ def test_resource_no_token(self):
+ response = self.app.get('/api/v1.0/bucketlists')
+ self.assertEqual(response.status_code, 403)
+
+
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file