From f9c09818f0ff8bf13082c2c0f792d0305e4b253e Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 00:22:21 +0300 Subject: [PATCH 01/25] [fx]separate logic from validations in auth --- app/auth/authentication.py | 80 +++++++++----------------------------- app/helpers/__init__.py | 0 app/helpers/decorators.py | 33 ++++++++++++++++ app/helpers/validators.py | 70 +++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 62 deletions(-) create mode 100644 app/helpers/__init__.py create mode 100644 app/helpers/decorators.py create mode 100644 app/helpers/validators.py diff --git a/app/auth/authentication.py b/app/auth/authentication.py index 8eea7c4..cd8af4e 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -1,11 +1,13 @@ """Class to deal with user authenticatication """ -from app.decorators import token_required +from app.helpers.decorators import token_required from app.models import User, Sessions import re from flask import request, jsonify, make_response from flask.views import MethodView from app import db +from app.helpers.validators import user_registration_validation, \ + user_login_validation, password_reset_validation class RegisterUser(MethodView): @@ -46,62 +48,25 @@ def post(self): 400: description: Bad Requests """ - regex_email = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z-.]+$)" - regex_username = "[a-zA-Z0-9- .]+$" + try: user_details = User.query.filter_by( email=request.data['email'].lower()).first() if not user_details: + email = str(request.data.get('email', '')) + password = str(request.data.get('password', '')) + username = str(request.data.get('username', '')) + secret = str(request.data.get('secret_word', '')) try: - email = str(request.data.get('email', '')) - password = str(request.data.get('password', '')) - username = str(request.data.get('username', '')) - secret = str(request.data.get('secret_word', '')) - - if email: - email = re.sub(r'\s+', ' ', email).strip() - email = None if email == " " else email.lower() - - if username: - username = re.sub(r'\s+', ' ', username).strip() - username = None if username == " " else username.title() - - if secret: - secret = re.sub(r'\s+', ' ', secret).strip() - secret = None if secret == " " else secret - - if not email or not password or not username: - response = { - 'message': "Kindly Provide all required details"} - return make_response(jsonify(response)), 422 - if not secret: - response = { - 'message': "Kindly Provide a secret word"} - return make_response(jsonify(response)), 422 - if not re.search(regex_email, email): - response = {'message': "Email pattern not valid"} - return make_response(jsonify(response)), 400 - - if not re.search(regex_username, username): - response = { - 'message': "No special characters allowed on username"} - return make_response(jsonify(response)), 400 - - if len(password) < 6: - response = {'message': "Password must be at least six characters"} - return make_response(jsonify(response)), 400 - - try: - user = User(email=email, password=password, - username=username, secret_word=secret) - user.save() - response = {'message': "Successfully registered"} - except Exception as e: - response = {'message': "User Exists, Kindly Login"} - return make_response(jsonify(response)), 409 + user_registration_validation(email, username, password, secret) + user = User(email=email, password=password, + username=username, secret_word=secret) + user.save() + response = {'message': "Successfully registered"} return make_response(jsonify(response)), 201 + except Exception as e: response = {'message': str(e)} return make_response(jsonify(response)), 400 @@ -151,11 +116,7 @@ def post(self): email = request.data['email'] password = request.data['password'] user_details = User.query.filter_by(email=email).first() - - if not email or not password: - response = {'message': "Kindly Provide email and password"} - return make_response(jsonify(response)), 422 - + user_login_validation(email, password) if user_details and user_details.password_check(password): access_token = user_details.user_token_generator(user_details.id) @@ -215,18 +176,13 @@ def put(self): """ try: + email = str(request.data.get('email', '')) user_details = User.query.filter_by( - email=request.data['email']).first() + email=email).first() reset_password = str(request.data.get('reset_password', '')) secret_word = str(request.data.get('secret_word', '')) + password_reset_validation(email, reset_password, secret_word) - if reset_password: - reset_password = re.sub(r'\s+', ' ', reset_password).strip() - reset_password = None if reset_password == " " else reset_password - - if not reset_password: - response = {"message": "No password provided"} - return make_response(jsonify(response)), 400 if user_details and user_details.secret_word_check(secret_word): res_password = User.password_hash(reset_password) user_details.password = res_password diff --git a/app/helpers/__init__.py b/app/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/helpers/decorators.py b/app/helpers/decorators.py new file mode 100644 index 0000000..008a7cd --- /dev/null +++ b/app/helpers/decorators.py @@ -0,0 +1,33 @@ +"""Decorator method to login and logout a user +""" +from functools import wraps +from flask import jsonify, make_response, request +from app.models import User, Sessions + + +def token_required(f): + """Decorator to check if user is logged in + """ + @wraps(f) + def decorated(*args, **kwargs): + """Decorator method to handle access token + issuance and blacklisting on logout + """ + access_token = None + authorization_header = request.headers.get('Authorization') + if authorization_header: + access_token = authorization_header + if not access_token: + response = {"message": "User is not authenticated"} + return make_response(jsonify(response)), 401 + try: + blacklisted_token = Sessions.check_logout_status(access_token) + if blacklisted_token: + return make_response( + jsonify({"message":"User is already logged out, Please login"}), 401) + else: + current_user = User.query.filter_by(id=User.decode_token(access_token)).first() + except Exception: + return {"message":"Token is expired"} + return f(current_user, *args, **kwargs) + return decorated diff --git a/app/helpers/validators.py b/app/helpers/validators.py new file mode 100644 index 0000000..af539a3 --- /dev/null +++ b/app/helpers/validators.py @@ -0,0 +1,70 @@ +"""Class to validate user input +""" +from marshmallow import Schema, fields, ValidationError +from validate_email import validate_email +import re + + +def user_registration_validation(email, username, password, secret): + """ + Method to check if email data is valid + """ + error = None + email = None if email == " " else email.lower() + valid_email = validate_email(email) + + if username: + username = re.sub(r'\s+', ' ', username).strip() + username = None if username == " " else username.title() + + if not email or not password or not username: + error = ValidationError('Kindly provide all details') + if not len(password) >= 6: + error = ValidationError('Password should be more than six characters') + if not secret: + error = ValidationError('Kindly provide a SECRET word') + if not username: + error = ValidationError('{} is not a valid username'.format(username)) + if not valid_email: + error = ValidationError('{} is not a valid email'.format(email)) + + if error: + raise error + + +def user_login_validation(email, password): + """ + Method to validate login credentials of a user + """ + error = None + if not email or not password: + error = ValidationError('Kindly Provide email and Password') + + if error: + raise error + + +def password_reset_validation(email, reset_password, secret): + """Validators to check password Reset + """ + error = None + if reset_password: + reset_password = re.sub(r'\s+', ' ', reset_password).strip() + reset_password = None if reset_password == " " else reset_password + + if not reset_password: + error = ValidationError('Kindly provide a reset Password') + + if not email: + error = ValidationError('Invalid user email ') + + if not secret: + error = ValidationError('Invalid Secret Word ') + + if error: + raise error + + + + + From 0c62f6214cae5f4d0deccde1442fc17e09003ceb Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 00:57:53 +0300 Subject: [PATCH 02/25] [fx] fix the auth tests --- tests/test_auth.py | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 9b4478a..a03e1cc 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -41,15 +41,16 @@ def test_register_user(self): self.assertEqual(result['message'], "Successfully registered") self.assertEqual(user_register.status_code, 201) - def test_empty_email_and_password_on_register(self): - """Method to check for empty email or password strings on signup + def test_password_on_register(self): + """Method to check for empty username or password strings on signup """ user_register = self.client().post(base_url + '/register', - data={'email': '', 'password': '', 'username': ''}) - self.assertEqual(user_register.status_code, 422) + data={'email': 'n@n.com', 'password': '4324', 'username': 'new', + 'secret_word': 'secret'}) + self.assertEqual(user_register.status_code, 400) user_details = json.loads(user_register.data.decode()) self.assertEqual(user_details['message'], - "Kindly Provide all required details") + "Password should be more than six characters") def test_empty_secret_word_on_register(self): """Method to test for empty secret word on user register @@ -58,11 +59,10 @@ def test_empty_secret_word_on_register(self): base_url + '/register', data={'email': 'test@test.com', 'password': '32eq5646436rw', 'username': 'New_User', 'secret_word': ''}) - self.assertEqual(user_register.status_code, 422) + self.assertEqual(user_register.status_code, 400) user_details = json.loads(user_register.data.decode()) self.assertEqual(user_details['message'], - "Kindly Provide a secret word") - + "Kindly provide a SECRET word") def test_minimum_required_password_on_register(self): """Methdod to test for the minimum required password length on registration @@ -74,7 +74,7 @@ def test_minimum_required_password_on_register(self): self.assertEqual(user_register.status_code, 400) user_details = json.loads(user_register.data.decode()) self.assertEqual(user_details['message'], - "Password must be at least six characters") + "Password should be more than six characters") def test_to_email_regex_pattern_on_register(self): """Method to check for a valid regex pattern on registration @@ -83,20 +83,6 @@ def test_to_email_regex_pattern_on_register(self): data={'email': 't.com', 'password': '2324dsfscdsf', 'username': 'User', 'secret_word': 'TOP SECRET'}) self.assertEqual(user_register.status_code, 400) - user_details = json.loads(user_register.data.decode()) - self.assertEqual(user_details['message'], "Email pattern not valid") - - def test_username_regex_pattern(self): - """Method to check if username matches provided regex pattern - """ - user_register = self.client().post( - base_url + '/register', - data={'email': 'test@test.com', - 'password': '2324dsfscdsf', 'username': '$%$^', 'secret_word': 'TOP SECRET'}) - self.assertEqual(user_register.status_code, 400) - user_details = json.loads(user_register.data.decode()) - self.assertEqual(user_details['message'], - "No special characters allowed on username") def test_error_exception_on_user_register(self): """Method to check for error handling in registration @@ -113,10 +99,10 @@ def test_empty_email_and_password_on_login(self): user_login = self.client().post(base_url + '/login', data={'email': '', 'password': ''}) - self.assertEqual(user_login.status_code, 422) + self.assertEqual(user_login.status_code, 400) user_details = json.loads(user_login.data.decode()) self.assertEqual(user_details['message'], - "Kindly Provide email and password") + "Error occurred on user login") def test_double_registration(self): """"Method to test a user who is already registered @@ -152,9 +138,9 @@ def test_to_check_empty_email_in_reset_password_in_auth(self): base_url + '/password-reset', data={'email': '', 'reset_password': 'testing_reset_p@ssword', 'secret_word': 'TOP SECRET'}) - self.assertEqual(password_reset.status_code, 404) + self.assertEqual(password_reset.status_code, 400) user_data = json.loads(password_reset.data.decode()) - self.assertIn(user_data['message'], "Kindly provide correct email and secret word") + self.assertIn(user_data['message'], "Invalid user email") def test_empty_reset_password(self): """Method to test for empty password while doing a password reset @@ -166,7 +152,7 @@ def test_empty_reset_password(self): 'reset_password': '', 'secret_word': 'TOP SECRET'}) self.assertEqual(password_reset.status_code, 400) user_data = json.loads(password_reset.data.decode()) - self.assertIn(user_data['message'], "No password provided") + self.assertIn(user_data['message'], "Kindly provide a reset Password") def test_to_check_success_in_reseting_password(self): """Method to check for successfully updated user password From 2d8be8240bdaa9795705bae412e56875c0f13390 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 02:13:58 +0300 Subject: [PATCH 03/25] [fx] add category validatios to helper module --- app/classes/categories.py | 120 ++++++++---------- .../{validators.py => auth_validators.py} | 4 +- app/helpers/category_validators.py | 46 +++++++ app/helpers/recipe_validators.py | 0 4 files changed, 99 insertions(+), 71 deletions(-) rename app/helpers/{validators.py => auth_validators.py} (94%) create mode 100644 app/helpers/category_validators.py create mode 100644 app/helpers/recipe_validators.py diff --git a/app/classes/categories.py b/app/classes/categories.py index b1629d9..a728677 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -1,10 +1,11 @@ """Class to handle category creation, viewing and manipulation """ import re -from app.decorators import token_required +from app.helpers.decorators import token_required from app.models import Categories from flask import request, jsonify, make_response from flask.views import MethodView +from app.helpers.category_validators import create_category_validation, update_category_validation class Category(MethodView): @@ -54,42 +55,32 @@ def post(self, current_user): 201: description: category created """ - regex_pattern = "[a-zA-Z0-9- .]+$" - category_name = str(request.data.get('category_name')) - if category_name: - category_name = re.sub(r'\s+', ' ', category_name).strip() - - category_name = None if category_name == " " else category_name.title() - - if not category_name: - response = {'message': 'category name not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, category_name): - response = {'message': 'Category name is not valid'} - return make_response(jsonify(response)), 400 - - category_details = Categories.query.filter_by( - category_name=category_name, created_by=current_user.id).first() - - if category_details: - response = {'message': 'Category name exists'} + try: + category_name = str(request.data.get('category_name', '')) + category_details = Categories.query.filter_by( + category_name=category_name, created_by=current_user.id).first() + create_category_validation(category_name) + if category_details: + response = {'message': 'Category name exists'} + return make_response(jsonify(response)), 400 + + category = Categories( + category_name=category_name, created_by=current_user.id) + category.save() + response = jsonify({ + 'id': category.id, + 'category_name': category.category_name, + 'created_by': category.created_by, + 'date_created': category.date_created, + 'date_modified': category.date_modified + }) + response.status_code = 201 + return response + except Exception as e: + response = {'message': str(e)} return make_response(jsonify(response)), 400 - category = Categories( - category_name=category_name, created_by=current_user.id) - category.save() - response = jsonify({ - 'id': category.id, - 'category_name': category.category_name, - 'created_by': category.created_by, - 'date_created': category.date_created, - 'date_modified': category.date_modified - }) - response.status_code = 201 - return response - def get(self, current_user): """Method to get all categories of a user in a paginated way --- @@ -282,47 +273,38 @@ def put(self, current_user, id): 200: description: OK """ - regex_pattern = "[a-zA-Z0-9- .]+$" - - category = Categories.query.filter_by( - id=id, created_by=current_user.id).first() - category_name = str(request.data.get('category_name', '')) - if category_name: - category_name = re.sub(r'\s+', ' ', category_name).strip() + try: + category = Categories.query.filter_by( + id=id, created_by=current_user.id).first() + category_name = str(request.data.get('category_name', '')) + update_category_validation(category_name) + if not category: + response = {'message': 'Category does not exist'} + return make_response(jsonify(response)), 404 - category_name = None if category_name == " " else category_name.title() - if not category_name: - response = {'message': 'category name not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, category_name): - response = {'message': 'Category name is not valid'} - return make_response(jsonify(response)), 400 - - if not category: - response = {'message': 'Category does not exist'} - return make_response(jsonify(response)), 404 - - category_details = Categories.query.filter_by( + category_details = Categories.query.filter_by( category_name=category_name, created_by=current_user.id).first() - if category_details: - response = {'message': 'Category name exists'} - return make_response(jsonify(response)), 400 + if category_details: + response = {'message': 'Category name exists'} + return make_response(jsonify(response)), 400 - category.category_name = category_name - category.save() - response = jsonify({ - 'id': category.id, - 'category_name': category.category_name, - 'created_by': category.created_by, - 'date_created': category.date_created, - 'date_modified': category.date_modified - }) - response.status_code = 200 - return response + category.category_name = category_name + category.save() + response = jsonify({ + 'id': category.id, + 'category_name': category.category_name, + 'created_by': category.created_by, + 'date_created': category.date_created, + 'date_modified': category.date_modified + }) + response.status_code = 200 + return response + except Exception as e: + response = {'message': str(e)} + return make_response(jsonify(response)), 400 def delete(self, current_user, id): """Method to delete a single category using its category id diff --git a/app/helpers/validators.py b/app/helpers/auth_validators.py similarity index 94% rename from app/helpers/validators.py rename to app/helpers/auth_validators.py index af539a3..61fec73 100644 --- a/app/helpers/validators.py +++ b/app/helpers/auth_validators.py @@ -56,10 +56,10 @@ def password_reset_validation(email, reset_password, secret): error = ValidationError('Kindly provide a reset Password') if not email: - error = ValidationError('Invalid user email ') + error = ValidationError('Invalid user email') if not secret: - error = ValidationError('Invalid Secret Word ') + error = ValidationError('Invalid Secret Word') if error: raise error diff --git a/app/helpers/category_validators.py b/app/helpers/category_validators.py new file mode 100644 index 0000000..156162f --- /dev/null +++ b/app/helpers/category_validators.py @@ -0,0 +1,46 @@ +"""Methods to validate a users category +""" +from marshmallow import ValidationError +import re + + +def create_category_validation(category_name): + """Validation method for creating a category + """ + error = None + regex_pattern = "[a-zA-Z0-9- .]+$" + + if category_name: + category_name = re.sub(r'\s+', ' ', category_name).strip() + category_name = None if category_name == " " else category_name.title() + + if not category_name: + error = ValidationError('category name not valid') + + if not re.search(regex_pattern, category_name): + error = ValidationError('{} Category name is not valid' .format(category_name)) + + if error: + raise error + + +def update_category_validation(category_name): + """ + Method to check for validation for an existing category + + """ + error = None + regex_pattern = "[a-zA-Z0-9- .]+$" + + if category_name: + category_name = re.sub(r'\s+', ' ', category_name).strip() + category_name = None if category_name == " " else category_name.title() + + if not category_name: + error = ValidationError('category name not provided') + + if not re.search(regex_pattern, category_name): + error = ValidationError('Category name is not valid') + + if error: + raise error diff --git a/app/helpers/recipe_validators.py b/app/helpers/recipe_validators.py new file mode 100644 index 0000000..e69de29 From 20c68a3e1a9bb3d800f991f2cc9171225054a21d Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 03:16:08 +0300 Subject: [PATCH 04/25] [fx] test code refactor and remove unused imports --- app/classes/categories.py | 9 ++++---- tests/test_categories.py | 46 +++------------------------------------ 2 files changed, 7 insertions(+), 48 deletions(-) diff --git a/app/classes/categories.py b/app/classes/categories.py index a728677..ad427b3 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -1,11 +1,10 @@ """Class to handle category creation, viewing and manipulation """ -import re from app.helpers.decorators import token_required from app.models import Categories from flask import request, jsonify, make_response from flask.views import MethodView -from app.helpers.category_validators import create_category_validation, update_category_validation +from app.helpers.category_validators import category_validation class Category(MethodView): @@ -60,7 +59,7 @@ def post(self, current_user): category_name = str(request.data.get('category_name', '')) category_details = Categories.query.filter_by( category_name=category_name, created_by=current_user.id).first() - create_category_validation(category_name) + category_validation(category_name) if category_details: response = {'message': 'Category name exists'} return make_response(jsonify(response)), 400 @@ -278,12 +277,12 @@ def put(self, current_user, id): category = Categories.query.filter_by( id=id, created_by=current_user.id).first() category_name = str(request.data.get('category_name', '')) - update_category_validation(category_name) + category_validation(category_name) + if not category: response = {'message': 'Category does not exist'} return make_response(jsonify(response)), 404 - category_details = Categories.query.filter_by( category_name=category_name, created_by=current_user.id).first() diff --git a/tests/test_categories.py b/tests/test_categories.py index e025fa0..0f2faac 100644 --- a/tests/test_categories.py +++ b/tests/test_categories.py @@ -65,7 +65,7 @@ def test_null_category_name(self): data={'category_name': ''}) categories_data = json.loads(create_categories.data.decode()) self.assertEqual(create_categories.status_code, 400) - self.assertIn(categories_data['message'], 'category name not provided') + self.assertIn(categories_data['message'], 'Category name is not valid') def test_invalid_category_name(self): """Method to test invalid category name @@ -101,26 +101,6 @@ def test_api_can_get_all_recipe_categories(self): Authorization=self.access_token)) self.assertEqual(get_categories.status_code, 200) - def test_to_check_for_invalid_page_number_on_category_fetch(self): - """Method to test invalid arguements of page on category fetch - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/?page=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - - def test_to_check_for_invalid_limit_parameter_on_category_fetch(self): - """Method to test invalid arguements of limit on category fetch - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/?limit=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - def test_to_check_for_paginated_recipe_categories(self): """Method to test pagination of category results """ @@ -174,7 +154,7 @@ def test_api_can_edit_a_recipe_category(self): self.assertEqual(create_category.status_code, 201) edit_category = self.client().put(base_url + 'categories/1', headers=dict( - Authorization=self.access_token), data={"category_name": "newly_edited_category"}) + Authorization=self.access_token), data={"category_name": "Newly_Edited"}) self.assertEqual(edit_category.status_code, 200) #test to check whether the edited category exists @@ -201,7 +181,7 @@ def test_edit_category_with_null_name(self): headers=dict(Authorization=self.access_token), data={"category_name": ""}) self.assertEqual(edit_category.status_code, 400) category_data = json.loads(edit_category.data.decode()) - self.assertIn(category_data['message'], 'category name not provided') + self.assertIn(category_data['message'], 'Category name is not valid') def test_edit_category_with_invalid_name(self): """test if API can edit a recipe category with an invalid name @@ -275,26 +255,6 @@ def test_to_check_null_search_response(self): Authorization=self.access_token)) self.assertEqual(get_categories.status_code, 200) - def test_to_check_for_invalid_page_number_on_search(self): - """Method to test invalid arguements on category search - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/search/?q=New&page=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - - def test_to_check_for_invalid_limit_parameter_on_search(self): - """Method to test pagination of searched category results - """ - get_categories = self.client().post(base_url + 'categories/', headers=dict( - Authorization=self.access_token), data=self.categories) - self.assertEqual(get_categories.status_code, 201) - get_categories = self.client().get(base_url + 'categories/search/?q=New&limit=fd', headers=dict( - Authorization=self.access_token)) - self.assertEqual(get_categories.status_code, 400) - def test_to_search_with_null_category_item(self): """Test to search with no category item provided """ From 86d118ffd0bd96c1925354e65dcce18c1edc8a6a Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 03:17:06 +0300 Subject: [PATCH 05/25] [fx] remove method repetiition --- app/helpers/category_validators.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/app/helpers/category_validators.py b/app/helpers/category_validators.py index 156162f..a348858 100644 --- a/app/helpers/category_validators.py +++ b/app/helpers/category_validators.py @@ -4,7 +4,7 @@ import re -def create_category_validation(category_name): +def category_validation(category_name): """Validation method for creating a category """ error = None @@ -17,30 +17,9 @@ def create_category_validation(category_name): if not category_name: error = ValidationError('category name not valid') - if not re.search(regex_pattern, category_name): - error = ValidationError('{} Category name is not valid' .format(category_name)) - - if error: - raise error - - -def update_category_validation(category_name): - """ - Method to check for validation for an existing category - - """ - error = None - regex_pattern = "[a-zA-Z0-9- .]+$" - - if category_name: - category_name = re.sub(r'\s+', ' ', category_name).strip() - category_name = None if category_name == " " else category_name.title() - - if not category_name: - error = ValidationError('category name not provided') - if not re.search(regex_pattern, category_name): error = ValidationError('Category name is not valid') if error: raise error + From 266936f357796c23d7edc062191423663794df2a Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 03:18:34 +0300 Subject: [PATCH 06/25] [fx] add login method check as a static method --- app/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index fc7050d..fa1e316 100644 --- a/app/models.py +++ b/app/models.py @@ -204,7 +204,8 @@ def __init__(self, auth_token): """ self.auth_token = auth_token - def check_logout_status(self, auth_token): + @staticmethod + def check_logout_status(auth_token): """Method to check if a user is logged out """ logout_state = Sessions.query.filter_by(auth_token=auth_token).first() From db22f3ea32eb97d7c2e4ee15de63af0010de16df Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 03:19:42 +0300 Subject: [PATCH 07/25] [fx] separate validations and programming language --- app/classes/recipes.py | 98 +++++++++++--------------------- app/helpers/recipe_validators.py | 34 +++++++++++ 2 files changed, 67 insertions(+), 65 deletions(-) diff --git a/app/classes/recipes.py b/app/classes/recipes.py index 391da52..71f4daf 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -2,10 +2,11 @@ """ import re -from app.decorators import token_required +from app.helpers.decorators import token_required from app.models import Recipes from flask import request, jsonify, make_response from flask.views import MethodView +from app.helpers.recipe_validators import recipe_validation class Recipe(MethodView): @@ -56,7 +57,6 @@ def post(self, current_user, id): 404: description: Category does not exist """ - regex_pattern = "[a-zA-Z0-9- .]+$" category_id = id if category_id: try: @@ -64,25 +64,7 @@ def post(self, current_user, id): recipe_ingredients = request.data.get('recipe_ingredients', '') recipe_methods = request.data.get('recipe_methods', '') - if recipe_name: - recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() - recipe_name = None if recipe_name == "" else recipe_name.title() - - if not recipe_name: - response = {'message': 'Recipe name not provided'} - return make_response(jsonify(response)), 400 - if not recipe_ingredients: - response = {'message': 'Recipe ingredients not provided'} - return make_response(jsonify(response)), 400 - if not recipe_methods: - response = { - 'message': 'Recipe preparation methods not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, recipe_name): - response = {'message': 'Recipe name is not valid'} - return make_response(jsonify(response)), 400 - + recipe_validation(recipe_name, recipe_methods, recipe_ingredients) recipe = Recipes(recipe_name=recipe_name, recipe_ingredients=recipe_ingredients, recipe_methods=recipe_methods, category_id=category_id) recipe_details = Recipes.query.filter_by( @@ -264,52 +246,38 @@ def put(self, current_user, id, recipe_id): 201: description: Successfully edited a recipe """ - regex_pattern = "[a-zA-Z0-9- .]+$" - category_id = id - recipe = Recipes.query.filter_by( - category_id=category_id, id=recipe_id).first() - recipe_name = str(request.data.get('recipe_name', '')) - recipe_ingredients = str(request.data.get('recipe_ingredients', '')) - recipe_methods = str(request.data.get('recipe_methods', '')) - - if recipe_name: - recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() - recipe_name = None if recipe_name == "" else recipe_name.title() - - if not recipe_name: - response = {'message': 'Recipe name not provided'} - return make_response(jsonify(response)), 400 - if not recipe_ingredients: - response = {'message': 'Recipe ingredients not provided'} - return make_response(jsonify(response)), 400 - if not recipe_methods: - response = {'message': 'Recipe preparation methods not provided'} - return make_response(jsonify(response)), 400 - - if not re.search(regex_pattern, recipe_name): - response = {'message': 'Recipe name is not valid'} + try: + category_id = id + recipe = Recipes.query.filter_by( + category_id=category_id, id=recipe_id).first() + recipe_name = str(request.data.get('recipe_name', '')) + recipe_ingredients = str(request.data.get('recipe_ingredients', '')) + recipe_methods = str(request.data.get('recipe_methods', '')) + recipe_validation(recipe_name,recipe_methods, recipe_ingredients) + + if not recipe: + response = {'message': 'No recipe found'} + response = make_response(jsonify(response)), 404 + return response + else: + recipe.recipe_name = recipe_name + recipe.recipe_ingredients = recipe_ingredients + recipe.recipe_methods = recipe_methods + recipe.save() + response = {'id': recipe.id, + 'recipe_name': recipe.recipe_name, + 'recipe_ingredients': recipe.recipe_ingredients, + 'recipe_methods': recipe.recipe_methods, + 'category_id': recipe.category_id, + 'date_created': recipe.date_created, + 'date_modified': recipe.date_modified + } + response = make_response(jsonify(response)), 201 + return response + except Exception as e: + response = {'message': str(e)} return make_response(jsonify(response)), 400 - if not recipe: - response = {'message': 'No recipe found'} - response = make_response(jsonify(response)), 404 - return response - else: - recipe.recipe_name = recipe_name - recipe.recipe_ingredients = recipe_ingredients - recipe.recipe_methods = recipe_methods - recipe.save() - response = {'id': recipe.id, - 'recipe_name': recipe.recipe_name, - 'recipe_ingredients': recipe.recipe_ingredients, - 'recipe_methods': recipe.recipe_methods, - 'category_id': recipe.category_id, - 'date_created': recipe.date_created, - 'date_modified': recipe.date_modified - } - response = make_response(jsonify(response)), 201 - return response - def delete(self, current_user, id, recipe_id): """Method to delete a recipe in a category --- diff --git a/app/helpers/recipe_validators.py b/app/helpers/recipe_validators.py index e69de29..912510b 100644 --- a/app/helpers/recipe_validators.py +++ b/app/helpers/recipe_validators.py @@ -0,0 +1,34 @@ +from marshmallow import ValidationError +import re + + +def recipe_validation(recipe_name, recipe_methods, recipe_ingredients): + error = None + regex_pattern = "[a-zA-Z0-9-]+$" + + if recipe_name: + recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() + recipe_name = None if recipe_name == " " else recipe_name.title() + + if recipe_ingredients: + recipe_ingredients = re.sub(r'\s+', ' ', recipe_ingredients).strip() + recipe_ingredients = None if recipe_ingredients == " " else recipe_ingredients + + if recipe_methods: + recipe_methods = re.sub(r'\s+', ' ', recipe_methods).strip() + recipe_methods = None if recipe_methods == " " else recipe_methods + + if not recipe_name: + error = ValidationError('Recipe name not provided') + + if not recipe_ingredients: + error = ValidationError('Recipe ingredients not provided') + + if not recipe_methods: + error = ValidationError('Recipe preparation methods not provided') + + if not re.search(regex_pattern, recipe_name): + error = ValidationError('Recipe name is not valid') + + if error: + raise error From 50d724d7c71fa748d720f5f860bd27693ced76ab Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 12:20:42 +0300 Subject: [PATCH 08/25] [fx] fix failling recipe tests --- app/classes/recipes.py | 8 +++++++- tests/test_recipes.py | 12 ++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/classes/recipes.py b/app/classes/recipes.py index 71f4daf..9d9e4ae 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -64,9 +64,15 @@ def post(self, current_user, id): recipe_ingredients = request.data.get('recipe_ingredients', '') recipe_methods = request.data.get('recipe_methods', '') - recipe_validation(recipe_name, recipe_methods, recipe_ingredients) + try: + recipe_validation(recipe_name, recipe_methods, recipe_ingredients) + except Exception as e: + response = {'message': str(e)} + return make_response(jsonify(response)), 400 + recipe = Recipes(recipe_name=recipe_name, recipe_ingredients=recipe_ingredients, recipe_methods=recipe_methods, category_id=category_id) + recipe_details = Recipes.query.filter_by( category_id=category_id, recipe_name=recipe_name).first() diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 8c53aac..d95aa7f 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -79,12 +79,12 @@ def test_null_recipe_name(self): """ create_recipe = self.client().post(base_url + '/categories/1/recipes/', headers=dict(Authorization=self.access_token), - data={"recipe_name": "", + data={"recipe_name": " ", "recipe_ingredients": "milk", "recipe_methods": "heat to boil"}) self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) - self.assertIn(recipe_data['message'], 'Recipe name not provided') + self.assertIn(recipe_data['message'], 'Recipe name is not valid') def test_null_recipe_methods(self): """test if recipe methods are null @@ -93,7 +93,7 @@ def test_null_recipe_methods(self): headers=dict(Authorization=self.access_token), data={"recipe_name": "New Recipe", "recipe_ingredients": "milk", - "recipe_methods": ""}) + "recipe_methods": " "}) self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) @@ -119,12 +119,12 @@ def test_invalid_recipe_name(self): """ create_recipe = self.client().post(base_url + '/categories/1/recipes/', headers=dict(Authorization=self.access_token), - data={"recipe_name": "@@@", + data={"recipe_name": "", "recipe_ingredients": "milk, water", "recipe_methods": "heat to boil"}) - self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) self.assertIn(recipe_data['message'], 'Recipe name is not valid') + self.assertEqual(create_recipe.status_code, 400) def test_duplicate_recipe(self): """test if recipe is duplicated @@ -201,7 +201,7 @@ def test_to_edit_recipe_with_null_name(self): 'recipe_methods': 'boil to heat'}) category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) - self.assertIn(category_data['message'], 'Recipe name not provided') + self.assertIn(category_data['message'], 'Recipe name is not valid') def test_to_edit_recipe_with_null_recipe_methods(self): """Test to edit a recipe name with null recipe methods From bcd785b7f4edd1c0568d33a00f90eaaf8619f763 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 12:25:26 +0300 Subject: [PATCH 09/25] [ft] add marshmallow package for validation --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index aa4d624..23294a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ flasgger==0.8.0 Flask==0.12.2 Flask-API==1.0 Flask-Bcrypt==0.7.1 +flask-marshmallow==0.8.0 Flask-Migrate==2.1.1 Flask-RESTful==0.3.6 Flask-Script==2.0.6 @@ -30,6 +31,7 @@ jsonschema==2.6.0 lazy-object-proxy==1.3.1 Mako==1.0.7 MarkupSafe==1.0 +marshmallow==2.15.0 mccabe==0.6.1 mistune==0.8.3 nose==1.3.7 @@ -50,6 +52,7 @@ requests==2.18.4 six==1.11.0 SQLAlchemy==1.1.14 urllib3==1.22 +validate-email==1.3 Werkzeug==0.12.2 wrapt==1.10.11 yapf==0.20.0 From 800aa5ea853889ac2ff7e66bb00be53da545f993 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 12:31:20 +0300 Subject: [PATCH 10/25] [fx] change validator names on import --- app/auth/authentication.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/auth/authentication.py b/app/auth/authentication.py index cd8af4e..1b5fe3b 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -6,7 +6,7 @@ from flask import request, jsonify, make_response from flask.views import MethodView from app import db -from app.helpers.validators import user_registration_validation, \ +from app.helpers.auth_validators import user_registration_validation, \ user_login_validation, password_reset_validation @@ -115,8 +115,9 @@ def post(self): try: email = request.data['email'] password = request.data['password'] - user_details = User.query.filter_by(email=email).first() + user_details = User.query.filter_by(email==email).first() user_login_validation(email, password) + if user_details and user_details.password_check(password): access_token = user_details.user_token_generator(user_details.id) From 4b4538f7e0472b407c014c472da92910062ad67f Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 12:35:05 +0300 Subject: [PATCH 11/25] [fx] changed fetch of username on login --- app/auth/authentication.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/auth/authentication.py b/app/auth/authentication.py index 1b5fe3b..2ad3af8 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -2,7 +2,6 @@ """ from app.helpers.decorators import token_required from app.models import User, Sessions -import re from flask import request, jsonify, make_response from flask.views import MethodView from app import db @@ -115,7 +114,7 @@ def post(self): try: email = request.data['email'] password = request.data['password'] - user_details = User.query.filter_by(email==email).first() + user_details = User.query.filter(email=email).first() user_login_validation(email, password) if user_details and user_details.password_check(password): From 284024b03c6ad4b3e848867034934ca7221a821f Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 12:35:48 +0300 Subject: [PATCH 12/25] [fx] changed location of decorators --- app/decorators.py | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 app/decorators.py diff --git a/app/decorators.py b/app/decorators.py deleted file mode 100644 index 008a7cd..0000000 --- a/app/decorators.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Decorator method to login and logout a user -""" -from functools import wraps -from flask import jsonify, make_response, request -from app.models import User, Sessions - - -def token_required(f): - """Decorator to check if user is logged in - """ - @wraps(f) - def decorated(*args, **kwargs): - """Decorator method to handle access token - issuance and blacklisting on logout - """ - access_token = None - authorization_header = request.headers.get('Authorization') - if authorization_header: - access_token = authorization_header - if not access_token: - response = {"message": "User is not authenticated"} - return make_response(jsonify(response)), 401 - try: - blacklisted_token = Sessions.check_logout_status(access_token) - if blacklisted_token: - return make_response( - jsonify({"message":"User is already logged out, Please login"}), 401) - else: - current_user = User.query.filter_by(id=User.decode_token(access_token)).first() - except Exception: - return {"message":"Token is expired"} - return f(current_user, *args, **kwargs) - return decorated From a1f847fe3cd8a4e9d21a4ce81024a849fb03cb77 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 12:42:59 +0300 Subject: [PATCH 13/25] [fx] replaced filter with filter_by on user fetch on login --- app/auth/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/auth/authentication.py b/app/auth/authentication.py index 2ad3af8..bbb1eba 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -114,7 +114,7 @@ def post(self): try: email = request.data['email'] password = request.data['password'] - user_details = User.query.filter(email=email).first() + user_details = User.query.filter_by(email=email).first() user_login_validation(email, password) if user_details and user_details.password_check(password): From abfc298515de217bb804daa306734dcb5d53f456 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 13:47:49 +0300 Subject: [PATCH 14/25] [fx] remodel API diaplay tables on README.md --- README.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1db58a6..51cc942 100644 --- a/README.md +++ b/README.md @@ -76,30 +76,31 @@ Interact with the API: -), send http requests using Postman ## User API Endpoints - URL Endpoint | HTTP requests | access | status | - ---------------- | ----------------- | ------------- | ------------------ - / flask_api / v1 / auth / register | POST | Register a new user | publc - / flask_api / v1 / auth / login | POST | Login and retrieve token | public +|URL Endpoint |HTTP requests |access |status| +---------------- | ----------------- | ------------- | ------------------ +|/flask_api/v1/auth/register | POST | Register a new user | public| +|/flask_api/v1/auth/login | POST | Login and retrieve token | public| + ## Categories API Endpoints - URL Endpoint | HTTP requests | access | status | - ---------------- | ----------------- | ------------- | ------------------ - / flask_api / v1 / categories / | POST | Create a new recipe category | private - / flask_api / v1 / categories / | GET | Retrieve all categories for user | private - / flask_api / v1 / categories / search /?q= & limit & page | GET | Retrieve all categories for a given search | private - / flask_api / v1 / categories / / | GET | Retrieve a category by ID | private - / flask_api / v1 / categories / / | PUT | Update a category | private - / flask_api / v1 / categories / / | DELETE | Delete a category | private +URL Endpoint | HTTP requests | access | status | +---------------- | ----------------- | ------------- | ------------------ +/flask_api/v1/categories/ | POST | Create a new recipe category | private +/flask_api/v1/categories/ | GET | Retrieve all categories for user | private +/flask_api/v1/categories/search/?q= & limit & page | GET | Retrieve all categories for a given search | private +/flask_api/v1/categories// | GET | Retrieve a category by ID | private +/flask_api/v1/categories// | PUT | Update a category | private +/flask_api/v1/categories// | DELETE | Delete a category | private ## Recipes API Endpoints - URL Endpoint | HTTP requests | access | status | - ---------------- | ----------------- | ------------- | ------------------ - / flask_api / v1 / categories / /recipes / | GET | Retrive recipes in a given category | private - / flask_api / v1 / categories / /recipes / | POST | Create recipes in a category | private - / flask_api / v1 / categories / /recipes / search /?q= & limit & page | GET | Retrieve all recipes for a given search | private - / flask_api / v1 / categories / /recipes / / | DELETE | Delete a recipe in a category | private - / flask_api / v1 / categories / /recipes / / | PUT | update recipe details | private +URL Endpoint | HTTP requests | access | status | +---------------- | ----------------- | ------------- | ------------------ +/flask_api/v1/categories//recipes/ | GET | Retrive recipes in a given category | private +/flask_api/v1/categories//recipes/ | POST | Create recipes in a category | private +/flask_api/v1/categories//recipes/search/?q=&limit&page | GET | Retrieve all recipes for a given search | private +/flask_api/v1/categories//recipes//| DELETE | Delete a recipe in a category | private +/flask_api/v1/categories//recipes// | PUT | update recipe details | private Run the APIs on postman to ensure they are fully functioning. From 8c66ec0ad4a705cd7970ac68123a04b7e24f5ed6 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sat, 6 Jan 2018 13:48:55 +0300 Subject: [PATCH 15/25] [fx] fix the response status --- app/auth/authentication.py | 1 + app/classes/categories.py | 16 ++++++++++------ app/classes/recipes.py | 25 +++++++++++++++++-------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/auth/authentication.py b/app/auth/authentication.py index bbb1eba..fc9bf40 100644 --- a/app/auth/authentication.py +++ b/app/auth/authentication.py @@ -123,6 +123,7 @@ def post(self): if access_token: response = { 'message': 'Successful Login', + 'status': 'success', 'access_token': access_token.decode() } return make_response(jsonify(response)), 200 diff --git a/app/classes/categories.py b/app/classes/categories.py index ad427b3..7697842 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -287,7 +287,8 @@ def put(self, current_user, id): category_name=category_name, created_by=current_user.id).first() if category_details: - response = {'message': 'Category name exists'} + response = {'message': 'Category name exists', + 'status': 'fail'} return make_response(jsonify(response)), 400 category.category_name = category_name @@ -337,13 +338,15 @@ def delete(self, current_user, id): category = Categories.query.filter_by( id=id, created_by=current_user.id).first() if not category: - response = {'message': 'Category does not exist'} + response = {'message': 'Category does not exist', + 'status': 'error'} return make_response(jsonify(response)), 404 else: category.delete_categories() - return { - "message": "successfully deleted category" .format(category.id) - }, 200 + response = {'message': 'successfully deleted category', + 'status': 'success', + 'category_id': 'category id {} Deleted '.format(category.id)} + return make_response(jsonify(response)), 200 class SearchCategory(MethodView): @@ -433,7 +436,8 @@ def get(self, current_user): results.append(category_object) return make_response(jsonify(results)), 200 else: - response = {'message': 'No search item provided'} + response = {'message': 'No search item provided', + 'status': 'error'} return make_response(jsonify(response)), 200 diff --git a/app/classes/recipes.py b/app/classes/recipes.py index 9d9e4ae..5a62b79 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -77,7 +77,8 @@ def post(self, current_user, id): category_id=category_id, recipe_name=recipe_name).first() if recipe_details: - response = {'message': 'Recipe name exists'} + response = {'message': 'Recipe name exists', + 'status': 'fail'} return make_response(jsonify(response)), 400 recipe.save() response = {'id': recipe.id, @@ -92,7 +93,8 @@ def post(self, current_user, id): return response except Exception: - response = {'message': 'Category does not exist'} + response = {'message': 'Category does not exist', + 'status': 'error'} return make_response(jsonify(response)), 404 def get(self, current_user, id): @@ -151,7 +153,8 @@ def get(self, current_user, id): results.append(recipe_obj) if len(results) <= 0: - response = {'message': 'No recipe found '} + response = {'message': 'No recipe found ', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response response = jsonify(results) @@ -198,7 +201,8 @@ def get(self, current_user, id, recipe_id): """ recipe = Recipes.query.filter_by(category_id=id, id=recipe_id).first() if not recipe: - response = {'message': 'No recipe found'} + response = {'message': 'No recipe found', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response else: @@ -262,7 +266,8 @@ def put(self, current_user, id, recipe_id): recipe_validation(recipe_name,recipe_methods, recipe_ingredients) if not recipe: - response = {'message': 'No recipe found'} + response = {'message': 'No recipe found', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response else: @@ -314,13 +319,16 @@ def delete(self, current_user, id, recipe_id): """ recipe = Recipes.query.filter_by(category_id=id, id=recipe_id).first() if not recipe: - response = {'message': 'No recipe found'} + response = {'message': 'No recipe found', + 'status': 'error'} response = make_response(jsonify(response)), 404 return response else: recipe.delete_recipes() response = { - "message": "successfully deleted category".format(recipe.id)} + "message": "successfully deleted recipe", + 'status': 'success', + "recipe_id": 'Deleted recipe {} ' .format(recipe.id)} response = make_response(jsonify(response)), 200 return response @@ -396,7 +404,8 @@ def get(self, current_user, id): response.status_code = 200 return response else: - response = {'message': 'No search item provided'} + response = {'message': 'No search item provided', + 'status': 'error'} return make_response(jsonify(response)), 200 From d685b833877409fccd503d8364e36846a10f79d3 Mon Sep 17 00:00:00 2001 From: mwaz Date: Sun, 7 Jan 2018 21:27:05 +0300 Subject: [PATCH 16/25] [fx] add test methods to handle errors on bad request --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 02a86bf..5e6d4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ instance/__pycache__ tests/__pycache__ .vscode/settings.json migrations/ - +.DS_Store +.cache/ +.coverage +cover/ From 1ed6a040639a2e17ee3060a773f89e53718d3cac Mon Sep 17 00:00:00 2001 From: mwaz Date: Sun, 7 Jan 2018 21:28:02 +0300 Subject: [PATCH 17/25] [fx] add method to capture errors on bad requests or invalid routes --- app/__init__.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/__init__.py b/app/__init__.py index 7464c61..6376f6a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,7 @@ from flask_api import FlaskAPI from flask_sqlalchemy import SQLAlchemy from flasgger import Swagger +from flask import make_response, jsonify, abort # local import @@ -70,4 +71,23 @@ def make_app(config_name): view_func=user_password_reset_view, ) app.add_url_rule(base_url + '/auth/logout', view_func=user_logout_view) + @app.errorhandler(404) + def not_found(error): + """handles error when users enters inappropriate endpoint + """ + response = { + 'message': 'Page not found' + } + return make_response(jsonify(response)), 404 + + @app.errorhandler(405) + def method_not_allowed(error): + """ handles errors if users uses method that is not allowed in an endpoint + """ + response = { + 'message': 'Method not allowed' + } + return make_response(jsonify(response)), 405 + return app + From 3a063cf40d73da673c5ee1f69521626cc39cdeac Mon Sep 17 00:00:00 2001 From: mwaz Date: Sun, 7 Jan 2018 21:29:03 +0300 Subject: [PATCH 18/25] [fx] add test methods to handle errors on bad requests and invalid pages --- tests/test_auth.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_auth.py b/tests/test_auth.py index a03e1cc..39cfcb3 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -233,3 +233,19 @@ def test_to_check_inexistent_user_email_on_password_reset(self): password_reset_data = json.loads(password_reset.data.decode()) self.assertIn( password_reset_data['message'], '"Kindly provide correct email and secret word"') + + def test_to_check_invalid_route(self): + """test to check message returned after an invalid route is provided on register + """ + user_register = self.client().post(base_url + '/register/', data=self.user_details) + self.assertEqual(user_register.status_code, 404) + register_data = json.loads(user_register.data.decode()) + self.assertIn(register_data['message'], 'Page not found') + + def test_invalid_method_on_route(self): + """Method to check invalid route provided on register + """ + user_register = self.client().get(base_url + '/register', data=self.user_details) + self.assertEqual(user_register.status_code, 405) + register_data = json.loads(user_register.data.decode()) + self.assertIn(register_data['message'], 'Method not allowed') From 4a8787b5cc43dfc54f621d6e2cba981d66b0c6ce Mon Sep 17 00:00:00 2001 From: mwaz Date: Sun, 7 Jan 2018 21:30:06 +0300 Subject: [PATCH 19/25] [fx] response delete message of recipes and categories --- app/classes/categories.py | 2 +- app/classes/recipes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/classes/categories.py b/app/classes/categories.py index 7697842..3a17cd2 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -345,7 +345,7 @@ def delete(self, current_user, id): category.delete_categories() response = {'message': 'successfully deleted category', 'status': 'success', - 'category_id': 'category id {} Deleted '.format(category.id)} + 'category_id': 'category id {} '.format(category.id)} return make_response(jsonify(response)), 200 diff --git a/app/classes/recipes.py b/app/classes/recipes.py index 5a62b79..df19d01 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -328,7 +328,7 @@ def delete(self, current_user, id, recipe_id): response = { "message": "successfully deleted recipe", 'status': 'success', - "recipe_id": 'Deleted recipe {} ' .format(recipe.id)} + "recipe_id": 'recipe id {} ' .format(recipe.id)} response = make_response(jsonify(response)), 200 return response From 96c32b027b507a4323d64885dc676a36b21996ae Mon Sep 17 00:00:00 2001 From: mwaz Date: Mon, 8 Jan 2018 07:26:21 +0300 Subject: [PATCH 20/25] [fx] users should not be able to edit recipes of other users --- app/classes/recipes.py | 6 +++--- app/models.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/classes/recipes.py b/app/classes/recipes.py index df19d01..55fb36b 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -71,10 +71,10 @@ def post(self, current_user, id): return make_response(jsonify(response)), 400 recipe = Recipes(recipe_name=recipe_name, recipe_ingredients=recipe_ingredients, - recipe_methods=recipe_methods, category_id=category_id) + recipe_methods=recipe_methods, category_id=category_id, created_by=current_user.id) recipe_details = Recipes.query.filter_by( - category_id=category_id, recipe_name=recipe_name).first() + category_id=category_id, recipe_name=recipe_name, created_by=current_user.id).first() if recipe_details: response = {'message': 'Recipe name exists', @@ -259,7 +259,7 @@ def put(self, current_user, id, recipe_id): try: category_id = id recipe = Recipes.query.filter_by( - category_id=category_id, id=recipe_id).first() + category_id=category_id, id=recipe_id, created_by=current_user.id).first() recipe_name = str(request.data.get('recipe_name', '')) recipe_ingredients = str(request.data.get('recipe_ingredients', '')) recipe_methods = str(request.data.get('recipe_methods', '')) diff --git a/app/models.py b/app/models.py index fa1e316..a5c00a0 100644 --- a/app/models.py +++ b/app/models.py @@ -154,9 +154,10 @@ class Recipes(db.Model): date_modified = db.Column( db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) + created_by = db.Column(db.Integer, db.ForeignKey(User.id)) category_id = db.Column(db.Integer, db.ForeignKey(Categories.id)) - def __init__(self, recipe_name, recipe_ingredients, recipe_methods, category_id): + def __init__(self, recipe_name, recipe_ingredients, recipe_methods, category_id, created_by): """ Constructor to initialize the class variables, category name and the owner @@ -165,6 +166,7 @@ def __init__(self, recipe_name, recipe_ingredients, recipe_methods, category_id) self.recipe_ingredients = recipe_ingredients self.recipe_methods = recipe_methods self.category_id = category_id + self.created_by = created_by def save(self): """ From 866e8a8a6e21cf59c269ff0eaa039bc903a5198a Mon Sep 17 00:00:00 2001 From: mwaz Date: Mon, 8 Jan 2018 12:53:15 +0300 Subject: [PATCH 21/25] [fx] add body in schema for category creation --- app/classes/categories.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/classes/categories.py b/app/classes/categories.py index 3a17cd2..d9844f1 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -35,6 +35,7 @@ def post(self, current_user): type: string default: Breakfast + responses: 200: schema: @@ -43,6 +44,18 @@ def post(self, current_user): category_name: type: string default: Breakfast + created_by: + type: integer + default: 2 + date_created: + type: string + default: Wed 20 Dec + date_modified: + type: string + default: Wed 20 Dec + id: + type: integer + default: 1 400: description: category name not valid 400: From 25c35f5d3f235331b277e0de89f81774d484c297 Mon Sep 17 00:00:00 2001 From: mwaz Date: Mon, 8 Jan 2018 13:37:51 +0300 Subject: [PATCH 22/25] [fx] add body in schema for category creation --- app/classes/categories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/classes/categories.py b/app/classes/categories.py index d9844f1..952b265 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -29,7 +29,7 @@ def post(self, current_user): required: true type: string schema: - id: categories + id: categories create properties: category_name: type: string From 859b7135b8d4e5e81e89bfa8f1fc7143fa45647d Mon Sep 17 00:00:00 2001 From: mwaz Date: Mon, 8 Jan 2018 19:59:59 +0300 Subject: [PATCH 23/25] [fx] validation errors on recipes and auth --- app/helpers/auth_validators.py | 10 ++++---- app/helpers/category_validators.py | 1 - app/helpers/recipe_validators.py | 38 ++++++++++++------------------ 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/app/helpers/auth_validators.py b/app/helpers/auth_validators.py index 61fec73..33fb7ec 100644 --- a/app/helpers/auth_validators.py +++ b/app/helpers/auth_validators.py @@ -1,6 +1,6 @@ """Class to validate user input """ -from marshmallow import Schema, fields, ValidationError +from marshmallow import ValidationError from validate_email import validate_email import re @@ -14,17 +14,17 @@ def user_registration_validation(email, username, password, secret): valid_email = validate_email(email) if username: - username = re.sub(r'\s+', ' ', username).strip() + username = re.sub(r'\s+', '', username).strip() username = None if username == " " else username.title() if not email or not password or not username: - error = ValidationError('Kindly provide all details') + error = ValidationError('Email, password, username missing') if not len(password) >= 6: error = ValidationError('Password should be more than six characters') if not secret: error = ValidationError('Kindly provide a SECRET word') if not username: - error = ValidationError('{} is not a valid username'.format(username)) + error = ValidationError('{} Kindly provide a username ') if not valid_email: error = ValidationError('{} is not a valid email'.format(email)) @@ -49,7 +49,7 @@ def password_reset_validation(email, reset_password, secret): """ error = None if reset_password: - reset_password = re.sub(r'\s+', ' ', reset_password).strip() + reset_password = re.sub(r'\s+', '', reset_password).strip() reset_password = None if reset_password == " " else reset_password if not reset_password: diff --git a/app/helpers/category_validators.py b/app/helpers/category_validators.py index a348858..653d292 100644 --- a/app/helpers/category_validators.py +++ b/app/helpers/category_validators.py @@ -22,4 +22,3 @@ def category_validation(category_name): if error: raise error - diff --git a/app/helpers/recipe_validators.py b/app/helpers/recipe_validators.py index 912510b..36fe571 100644 --- a/app/helpers/recipe_validators.py +++ b/app/helpers/recipe_validators.py @@ -1,34 +1,26 @@ -from marshmallow import ValidationError +from marshmallow import ValidationError import re -def recipe_validation(recipe_name, recipe_methods, recipe_ingredients): +def recipe_validation(recipe, *argv): error = None regex_pattern = "[a-zA-Z0-9-]+$" - if recipe_name: - recipe_name = re.sub(r'\s+', ' ', recipe_name).strip() - recipe_name = None if recipe_name == " " else recipe_name.title() + if recipe: + recipe = re.sub(r'\s+', '', recipe).strip() + recipe = None if recipe == " " else recipe.title() - if recipe_ingredients: - recipe_ingredients = re.sub(r'\s+', ' ', recipe_ingredients).strip() - recipe_ingredients = None if recipe_ingredients == " " else recipe_ingredients + if not re.search(regex_pattern, recipe): + error = ValidationError('recipe name cannot be empty or with invalid characters') - if recipe_methods: - recipe_methods = re.sub(r'\s+', ' ', recipe_methods).strip() - recipe_methods = None if recipe_methods == " " else recipe_methods + for arg in argv: + if arg: + arg = re.sub(r'\s+', '', arg).strip() + arg = None if arg == " " else arg - if not recipe_name: - error = ValidationError('Recipe name not provided') + if not arg: + error = ValidationError('Kindly provide ingredients and methods') - if not recipe_ingredients: - error = ValidationError('Recipe ingredients not provided') + if error: + raise error - if not recipe_methods: - error = ValidationError('Recipe preparation methods not provided') - - if not re.search(regex_pattern, recipe_name): - error = ValidationError('Recipe name is not valid') - - if error: - raise error From 6c1c6a9bdca0b62ec40f1cad46f51af760e48cd3 Mon Sep 17 00:00:00 2001 From: mwaz Date: Mon, 8 Jan 2018 20:01:05 +0300 Subject: [PATCH 24/25] [fx] fix failing asserts on recipe tests --- tests/test_recipes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index d95aa7f..3356b7c 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -84,7 +84,7 @@ def test_null_recipe_name(self): "recipe_methods": "heat to boil"}) self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) - self.assertIn(recipe_data['message'], 'Recipe name is not valid') + self.assertIn(recipe_data['message'], 'recipe name cannot be empty or with invalid characters') def test_null_recipe_methods(self): """test if recipe methods are null @@ -98,7 +98,7 @@ def test_null_recipe_methods(self): self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) self.assertIn(recipe_data['message'], - 'Recipe preparation methods not provided') + 'Kindly provide ingredients and methods') def test_null_recipe_ingredients(self): """test if recipe ingredients are null @@ -112,7 +112,7 @@ def test_null_recipe_ingredients(self): self.assertEqual(create_recipe.status_code, 400) recipe_data = json.loads(create_recipe.data.decode()) self.assertIn(recipe_data['message'], - 'Recipe ingredients not provided') + 'Kindly provide ingredients and methods') def test_invalid_recipe_name(self): """test if recipe name is valid @@ -123,7 +123,7 @@ def test_invalid_recipe_name(self): "recipe_ingredients": "milk, water", "recipe_methods": "heat to boil"}) recipe_data = json.loads(create_recipe.data.decode()) - self.assertIn(recipe_data['message'], 'Recipe name is not valid') + self.assertIn(recipe_data['message'], 'recipe name cannot be empty or with invalid characters') self.assertEqual(create_recipe.status_code, 400) def test_duplicate_recipe(self): @@ -201,7 +201,7 @@ def test_to_edit_recipe_with_null_name(self): 'recipe_methods': 'boil to heat'}) category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) - self.assertIn(category_data['message'], 'Recipe name is not valid') + self.assertIn(category_data['message'], 'recipe name cannot be empty or with invalid characters') def test_to_edit_recipe_with_null_recipe_methods(self): """Test to edit a recipe name with null recipe methods @@ -219,7 +219,7 @@ def test_to_edit_recipe_with_null_recipe_methods(self): category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) self.assertIn(category_data['message'], - 'Recipe preparation methods not provided') + 'Kindly provide ingredients and methods') def test_to_edit_recipe_with_null_recipe_ingredients(self): """Test to edit a recipe name with null recipe ingredients @@ -237,7 +237,7 @@ def test_to_edit_recipe_with_null_recipe_ingredients(self): category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) self.assertIn(category_data['message'], - 'Recipe ingredients not provided') + 'Kindly provide ingredients and methods') def test_edit_invalid_recipe_name(self): """Test to edit a recipe name with invalid recipe name @@ -254,7 +254,7 @@ def test_edit_invalid_recipe_name(self): 'recipe_methods': 'heat to boil'}) category_data = json.loads(edit_recipe.data.decode()) self.assertEqual(edit_recipe.status_code, 400) - self.assertIn(category_data['message'], 'Recipe name is not valid') + self.assertIn(category_data['message'], 'recipe name cannot be empty or with invalid characters') def test_edit_recipe_with_no_recipe_id(self): """Test to edit a recipe name with invalid recipe name From 17b0bb5e22501f6cc6861d74d5b33fed74bb7733 Mon Sep 17 00:00:00 2001 From: mwaz Date: Mon, 8 Jan 2018 20:02:58 +0300 Subject: [PATCH 25/25] [fx] formating of recipe and category id after deletion --- app/classes/categories.py | 2 +- app/classes/recipes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/classes/categories.py b/app/classes/categories.py index 952b265..378e603 100644 --- a/app/classes/categories.py +++ b/app/classes/categories.py @@ -358,7 +358,7 @@ def delete(self, current_user, id): category.delete_categories() response = {'message': 'successfully deleted category', 'status': 'success', - 'category_id': 'category id {} '.format(category.id)} + 'id': '{}'.format(category.id)} return make_response(jsonify(response)), 200 diff --git a/app/classes/recipes.py b/app/classes/recipes.py index 55fb36b..dddaaf9 100644 --- a/app/classes/recipes.py +++ b/app/classes/recipes.py @@ -328,7 +328,7 @@ def delete(self, current_user, id, recipe_id): response = { "message": "successfully deleted recipe", 'status': 'success', - "recipe_id": 'recipe id {} ' .format(recipe.id)} + "id": '{}' .format(recipe.id)} response = make_response(jsonify(response)), 200 return response