From 05e4aafa85b1e1425b5ae557b5c240325d207978 Mon Sep 17 00:00:00 2001 From: WaitheraMbugua Date: Wed, 22 Aug 2018 15:29:00 +0300 Subject: [PATCH 1/5] [Chore #159955297] database endpoints for login and signup --- API/app/Questions/views.py | 2 +- API/run.py | 20 ++++++++++- API/tests/test_Questions.py | 6 +++- UI/PostQuestion.html | 45 ++++-------------------- UI/main.js | 5 +++ v2/app/questions/model.py | 69 +++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 42 deletions(-) create mode 100644 v2/app/questions/model.py diff --git a/API/app/Questions/views.py b/API/app/Questions/views.py index 8c14cf3..f445131 100644 --- a/API/app/Questions/views.py +++ b/API/app/Questions/views.py @@ -36,7 +36,7 @@ def question(id, **kwargs): 'id': question.id, 'name': question.name, 'date_created': question.date_created, - 'date_modified': bucketlist.date_modified + 'date_modified': question.date_modified }) response.status_code = 200 return response diff --git a/API/run.py b/API/run.py index 22e3794..2bee778 100644 --- a/API/run.py +++ b/API/run.py @@ -1,5 +1,23 @@ +from flask import Flask + +from flask_restful import Api + + + + +def create_app(env="dev"): + app = Flask(__name__) + api = Api(app) + + + ) + + + + return app +from app import run + -from app import app if __name__ == '__main__': diff --git a/API/tests/test_Questions.py b/API/tests/test_Questions.py index 7c4c6d4..204e6f5 100644 --- a/API/tests/test_Questions.py +++ b/API/tests/test_Questions.py @@ -4,12 +4,16 @@ import os import json + + + + class BaseTestCase(TestCase): def setUp(self): """Configure test enviroment.""" - + os.environ['APP_SETTINGS'] = 'Testing' self.app = create_app("Testing") self.app_context = self.app.app_context() diff --git a/UI/PostQuestion.html b/UI/PostQuestion.html index 63c00a0..333668f 100644 --- a/UI/PostQuestion.html +++ b/UI/PostQuestion.html @@ -1,4 +1,3 @@ -<<<<<<< HEAD @@ -36,44 +35,12 @@

StackOverFlow - Lite

- -======= - - - - - -
-
- - - - Post a Question - - - - -
-
-

StackOverFlow - Lite

- -
-
-
-
-
- -
-
-
+
+
+ -
-
+ + ->>>>>>> 0bfb9e46bf63fafcd0a0d919363fb4198787b71f diff --git a/UI/main.js b/UI/main.js index e69de29..3145755 100644 --- a/UI/main.js +++ b/UI/main.js @@ -0,0 +1,5 @@ +for (const btn of document.querySelectorAll('.vote')) { + btn.addEventListener('click', event => { + event.target.classList.toggle('on'); + }); +} \ No newline at end of file diff --git a/v2/app/questions/model.py b/v2/app/questions/model.py new file mode 100644 index 0000000..974227b --- /dev/null +++ b/v2/app/questions/model.py @@ -0,0 +1,69 @@ +import os +import re +import jwt +import datetime + +from app import db +from flask import jsonify +from flask_bcrypt import Bcrypt + + +class User(db.Model): + """This class represents the user table.""" + + __tablename__ = 'User' + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), nullable=False, unique=True) + password = db.Column(db.String(80), nullable=False) + questions = db.relationship( + 'question', + backref='users', + lazy='dynamic' + ) + + def __init__(self, email): + """initialization""" + self.email = email + self.password = '' + + @staticmethod + def validate_email(email): + """Method validates an email""" + address_matcher = re.compile(r'^[\w-]+@([\w-]+\.)+[\w]+$') + return True if address_matcher.match(email) else False + + def create_password(self, password): + """Method generates a hashed password""" + self.password = Bcrypt().generate_password_hash(password).decode() + + def validate_password(self, password): + """Method confirms that password is correct""" + return Bcrypt().check_password_hash(self.password, password) + + def gen_token(self): + """Generates a token""" + token = jwt.encode({ + 'id': self.id, + 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=30) + }, os.getenv('SECRET')) + return jsonify({'token': token.decode('UTF-8')}) + + def save(self): + """Adds a new user to the database """ + db.session.add(self) + db.session.commit() + + @staticmethod + def get_all(user_id): + """Gets all users in a single query """ + return User.query.all() + + def delete(self): + """Deletes an existing user from the database """ + db.session.delete(self) + db.session.commit() + + def __repr__(self): + """Represents the object instance of the model whenever it queries""" + return "".format(self.email) From e5d186cd9ddadc71a0a9860fbfeb17cfe26f7180 Mon Sep 17 00:00:00 2001 From: WaitheraMbugua Date: Thu, 23 Aug 2018 00:33:39 +0300 Subject: [PATCH 2/5] [Chore #159955297] Create question post get endpoints --- v2/app/questions/models.py | 79 ++++++++++++++++++++++++++++ v2/app/{questions => users}/model.py | 0 v2/tests/test_Users.py | 2 + 3 files changed, 81 insertions(+) create mode 100644 v2/app/questions/models.py rename v2/app/{questions => users}/model.py (100%) diff --git a/v2/app/questions/models.py b/v2/app/questions/models.py new file mode 100644 index 0000000..929e4fe --- /dev/null +++ b/v2/app/questions/models.py @@ -0,0 +1,79 @@ + + +class questions(db.Model): + """This class represents the questionlist table.""" + + __tablename__ = 'questions' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(500)) + user_id = 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()) + items = db.relationship( + 'Item', + backref='questions', + lazy='dynamic', + cascade="all,delete", + ) + + def __init__(self, title, user_id): + """initialization.""" + self.title = title + self.user_id = user_id + + def new_item(self): + """Adds a new question to the database """ + db.session.add(self) + db.session.commit() + + @staticmethod + def get_all(user_id): + """Gets all questions in a single query """ + return questionlist.query.all(user_id=user_id) + + def delete(self): + """Deletes an existing question from the database """ + db.session.delete(self) + db.session.commit() + + def __repr__(self): + """Represents the object instance of the model whenever it queries""" + return "".format(self.title) + + +class Item(db.Model): + """This class represents the questions table.""" + + + __tablename__ = 'items' + __table_args__ = {'extend_existing': True} + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(500)) + question_id = db.Column(db.Integer, db.ForeignKey('questions.id')) + + def __init__(self, name, question_id): + """initialization.""" + self.name = name + self.question_id = question_id + + def save(self): + """Adds a new question to the database """ + db.session.add(self) + db.session.commit() + + @staticmethod + def get_all(question_id): + """Gets all questions in a single query """ + return Item.query.all(question_id=question_id) + + def delete(self): + """Deletes an existing questions from the database """ + db.session.delete(self) + db.session.commit() + + def __repr__(self): + """Represents the object instance of the model whenever it queries""" + return "".format(self.name) \ No newline at end of file diff --git a/v2/app/questions/model.py b/v2/app/users/model.py similarity index 100% rename from v2/app/questions/model.py rename to v2/app/users/model.py diff --git a/v2/tests/test_Users.py b/v2/tests/test_Users.py index effcdfd..f0f86ae 100644 --- a/v2/tests/test_Users.py +++ b/v2/tests/test_Users.py @@ -6,6 +6,8 @@ import os import json from app import db, create_app +from app.users,model import user + class UserTestCase(unittest.TestCase): From 7d30e3a566f61e09f2ebe9955c24bff3de3b2921 Mon Sep 17 00:00:00 2001 From: WaitheraMbugua Date: Fri, 24 Aug 2018 00:37:21 +0300 Subject: [PATCH 3/5] Changes to folder structure --- API/app/Questions/views.py | 33 ++++++++- API/app/run.py | 2 +- API/app/views.py | 28 ------- v2/api/__init__.py | 11 +++ v2/api/database/__init__.py | 0 v2/api/database/helpers.py | 55 ++++++++++++++ v2/api/database/models.py | 17 +++++ v2/api/endpoints/__init__.py | 0 v2/api/endpoints/questions.py | 128 ++++++++++++++++++++++++++++++++ v2/api/endpoints/users.py | 41 +++++++++++ v2/api/tests/test_Questions.py | 131 +++++++++++++++++++++++++++++++++ v2/api/tests/test_Users.py | 129 ++++++++++++++++++++++++++++++++ v2/app/questions/models.py | 79 -------------------- v2/app/users/model.py | 69 ----------------- v2/run.py | 4 + v2/tests/test_Users.py | 3 +- 16 files changed, 550 insertions(+), 180 deletions(-) create mode 100644 v2/api/__init__.py create mode 100644 v2/api/database/__init__.py create mode 100644 v2/api/database/helpers.py create mode 100644 v2/api/database/models.py create mode 100644 v2/api/endpoints/__init__.py create mode 100644 v2/api/endpoints/questions.py create mode 100644 v2/api/endpoints/users.py create mode 100644 v2/api/tests/test_Questions.py create mode 100644 v2/api/tests/test_Users.py delete mode 100644 v2/app/questions/models.py delete mode 100644 v2/app/users/model.py create mode 100644 v2/run.py diff --git a/API/app/Questions/views.py b/API/app/Questions/views.py index f445131..70b8fe1 100644 --- a/API/app/Questions/views.py +++ b/API/app/Questions/views.py @@ -4,9 +4,38 @@ from flask import request, jsonify, abort from . import Questions -@app.route('/questions/', methods=['GET', 'PUT', 'DELETE']) + +@questions.route('/api/v1/questions/', methods=['POST', 'GET']) +def get_questions(): + """This function handles request to the questions resource""" + if request.method == 'GET': + # return all questions in the db + response = jsonify(data['questions']) + response.status_code = 200 + else: + # POST request: Returns a new question with its id + question_id = int(data["questions"][-1]['id']) + 1 + req_data = json.loads(request.data.decode('utf-8').replace("'", '"')) + question = req_data['question'] + posted_by = req_data['namr'] + date = '{:%B %d, %Y}'.format(datetime.now()) + new_question = { + "id": question_id, + "text": question_text, + "posted_by": posted_by, + "date": date, + "answers": [] + } + data['questions'].append(new_question) + response = jsonify(new_question) + response.status_code = 201 + + return response + + +@app.route('api/v1/questions/', methods=['GET', 'PUT', 'DELETE']) def question(id, **kwargs): - # GET request: Retrieve a buckelist using it's ID + # GET request: Retrieve a question using it's ID question = Questions.query.filter_by(id=id).first() if not : # Raise an HTTPException with a 404 not found status code diff --git a/API/app/run.py b/API/app/run.py index b51533d..c58fb5b 100644 --- a/API/app/run.py +++ b/API/app/run.py @@ -13,7 +13,7 @@ def Login(): error = None if request.method =="POST": - if request.form['username'] != 'admin' or request.form["password"] != 'admin': + if request.form['username'] != 'flaskuser' or request.form["password"] != 'flaskuser2018': error = "Invalid credentials. Please try again." else: return redirect(url_for("UserAccount")) diff --git a/API/app/views.py b/API/app/views.py index 58a3018..ce7c6a2 100644 --- a/API/app/views.py +++ b/API/app/views.py @@ -25,34 +25,6 @@ def locate(id, items): return None -@questions.route('/api/v1/questions/', methods=['POST', 'GET']) -def get_questions(): - """This function handles request to the questions resource""" - if request.method == 'GET': - # return all questions in the db - response = jsonify(data['questions']) - response.status_code = 200 - else: - # handles a POST request - # return the new question with the question id - question_id = int(data["questions"][-1]['id']) + 1 - req_data = json.loads( - request.data.decode('utf-8').replace("'", '"')) - question_text = req_data['text'] - asked_by = req_data['asked_by'] - date = '{:%B %d, %Y}'.format(datetime.now()) - new_question = { - "id": question_id, - "text": question_text, - "asked_by": asked_by, - "date": date, - "answers": [] - } - data['questions'].append(new_question) - response = jsonify(new_question) - response.status_code = 201 - - return response @questions.route('/api/v1/questions/', methods=['GET', 'PUT', 'DELETE']) diff --git a/v2/api/__init__.py b/v2/api/__init__.py new file mode 100644 index 0000000..4291b4e --- /dev/null +++ b/v2/api/__init__.py @@ -0,0 +1,11 @@ +from flask import flask +from flask_restful import Api + +from api.endpoints.users import (signup, login) +from api.endpoints.questions import (signup, login) + +app = Flask(__name__) +api = Api(app) + +api.add_resource(UserRegister, '/api/v2/auth/signup') +api.add_resource(UserLogin, '/api/v2/auth/login') diff --git a/v2/api/database/__init__.py b/v2/api/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v2/api/database/helpers.py b/v2/api/database/helpers.py new file mode 100644 index 0000000..5d010d9 --- /dev/null +++ b/v2/api/database/helpers.py @@ -0,0 +1,55 @@ +""" +This is the helpers module + +This module contains functions that abstract the common DB usage +""" +import psycopg2 +from db_conn import DbConn + + +def run_query(query, inputs): + """Run queries with inputs""" + try: + db_instance = DbConn() + db_instance.cur.execute(query, inputs) + db_instance.conn.commit() + db_instance.close() + return True + + except psycopg2.Error: + return False + +def run_just_query(query): + """Run queries without inputs""" + try: + db_instance = DbConn() + db_instance.cur.execute(query) + db_instance.conn.commit() + db_instance.close() + return True + + except psycopg2.Error: + return False + + +def get_query(query, inputs): + """Get results with inputs""" + try: + db_instance = DbConn() + db_instance.cur.execute(query, (inputs,)) + result = db_instance.cur.fetchall() + db_instance.close() + return result + except psycopg2.Error: + return False + +def get_just_query(query): + """Get results without inputs""" + try: + db_instance = DbConn() + db_instance.cur.execute(query) + result = db_instance.cur.fetchall() + db_instance.close() + return result + except psycopg2.Error: + return False \ No newline at end of file diff --git a/v2/api/database/models.py b/v2/api/database/models.py new file mode 100644 index 0000000..6727e29 --- /dev/null +++ b/v2/api/database/models.py @@ -0,0 +1,17 @@ +from api import app +from api.database.helpers import run_query, get_query, get_just_query, run_just_query + +def view_questions(): + """Get all questions""" + # query and the user inputs + query = ("SELECT * FROM tbl_questions;") + # run query + return run_just_query(query) +def view_question(id): + """Get one question""" + query = ("SELECT * FROM tbl_questions;") + # run query + return run_query(query, id) + + + diff --git a/v2/api/endpoints/__init__.py b/v2/api/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/v2/api/endpoints/questions.py b/v2/api/endpoints/questions.py new file mode 100644 index 0000000..6565eca --- /dev/null +++ b/v2/api/endpoints/questions.py @@ -0,0 +1,128 @@ +from api.database.models import ( view_questions ) +from flask import jsonify + + +class Questions(MethodView): + """ + Questions + """ + + def get(self): + """ + Fetch all questions + """ + question = Questions.query.filter_by(id=id).first() + response = {'questions': view_questions()} + return response, 200 + + def post(self): + """ + Post a question + """ + question = Questions.query.filter_by(id=id).first() + question_id = int(data["questions"][-1]['id']) + 1 + req_data = json.loads(request.data.decode('utf-8').replace("'", '"')) + question = req_data['question'] + posted_by = req_data['name'] + date = '{:%B %d, %Y}'.format(datetime.now()) + new_question = { + "id": question_id, + "text": question_text, + "posted_by": posted_by, + "date": date + + } + data['questions'].append(new_question) + response = jsonify(new_question) + response.status_code = 201 + + return response + + + + +class Question(MethodView): + """ Get single question """ + + def get(self, id): + """ + Fetch a specific question + """ + question = Questions.query.filter_by(id=id).first() + response = jsonify({ + 'id': question.id, + 'name': question.name, + 'date_created': question.date_created, + 'date_modified': question.date_modified + }) + response.status_code = 200 + return response + + + + def delete(self, id): + """ + Delete a question + """ + + question = Questions.query.filter_by(id=id).first() + question.delete() + response = { + "message": "Question deleted successfully" + } + response.status_code = 200 + return response + + + +class addAnswer(MethodView): + """ + Add answer + """ + + def post(self, id): + """add an answer""" + answer = Answers.query.filter_by(id=id).first() + answer_id = int(data["answers"][-1]['id']) + 1 + req_data = json.loads(request.data.decode('utf-8').replace("'", '"')) + answer = req_data['answer'] + posted_by = req_data['name'] + date = '{:%B %d, %Y}'.format(datetime.now()) + new_answer = { + "id": answer_id, + "text": answer_text, + "posted_by": posted_by, + "date": date + + } + data['answer'].append(new_answer) + response = jsonify(new_answer) + response.status_code = 201 + return response + + + +class updateAnswer(MethodView): + """ + Update answer + """ + + + def put(self, id): + """add an answer""" + response = UserModel().decode_auth_token(auth_token) + if int(user_id) == int(response): + # edit answer + new_text = json.loads(request.data.decode().replace("'", '"'))['text'] + text = answers.update_answer(new_text, answer_id) + else: + # it is not the same user who asked the answer + raise Forbidden + resp = { + "message": "success", + "description": "answer updated succesfully", + "text": text + } + return jsonify(resp), 200 + pass + diff --git a/v2/api/endpoints/users.py b/v2/api/endpoints/users.py new file mode 100644 index 0000000..cd2e658 --- /dev/null +++ b/v2/api/endpoints/users.py @@ -0,0 +1,41 @@ +""" +Users endpoint +""" +from flask import jsonify +from flask_bcrypt import Bcrypt +from api.api.database.models import () +import re + + +class signUp(Resource): + def signUp(user_name, email, password): + # encrypt password + hidden_password = Bcrypt().generate_password_hash(password).decode() + query = (INSERT * INTO tbl_users('user_name', 'email', 'hidden_password')) + + return run_query(query, inputs=[user_name, email, password]) + + + + +class login(Resource): + def validate_email(email): + """Method confirms email is correct""" + check_email = re.compile(r'^[\w-]+@([\w-]+\.)+[\w]+$') + return True if check_email.match(email) else False + + def validate_password(password): + """Method confirms that password is correct""" + return Bcrypt().check_password_hash(self.password, password) + + def user_login(email, password): + + if validate_email(email) ==True & validate_password(password): + query = "SELECT * FROM tbl_users WHERE email = %s;" + all_members = get_query(query, email) + + for user_email in all_members: + if user_email['email'] == email: + return user_email + + diff --git a/v2/api/tests/test_Questions.py b/v2/api/tests/test_Questions.py new file mode 100644 index 0000000..4cba1c0 --- /dev/null +++ b/v2/api/tests/test_Questions.py @@ -0,0 +1,131 @@ +""" +Tests for Questions +""" + +from api.endpoints import users + +class questionTestCase(unittest.TestCase): + """This class represents the question test case""" + def setUp(self): + """Define test variables and initialize app.""" + self.app = create_app(config_name="testing") + self.client = self.app.test_client + self.user = {'email': 'test@test.com', 'password': 'testPass'} + self.question = {'question': 'How do you create a list in python'} + + # binds the app to the current context + with self.app.app_context(): + # create all tables + db.create_all() + + def registration(self): + """Test for User registeration""" + return self.client().post('/signup/', data=self.user) + + def login(self): + """Logins a user""" + self.registration() + resp = self.client().post('/login/', data=self.user) + return {'Authorization': json.loads(resp.data.decode())['token']} + + def test_create_question(self): + """Test API can create a question""" + resp = self.client().post( + '/question/', headers=self.login(), data=self.question) + self.assertEqual(resp.status_code, 201) + self.assertIn('How do you create a list in python?', str(resp.data)) + + def test_confirm_forum_creation(self): + """Test user cannot post the same questions """ + self.client().post( + '/question', headers=self.login(), data=self.question) + resp = self.client().post( + '/question/', headers=self.login(), data=self.question) + self.assertEqual(resp.status_code, 403) + self.assertIn('That question has been posted!', str(resp.data)) + + def test_blank_title(self): + """Test that question is not blank""" + self.question['title'] = '' + resp = self.client().post( + '/question/', headers=self.login(), data=self.question) + self.assertEqual(resp.status_code, 401) + self.assertIn('Please write your question', str(resp.data)) + + def test_get_all_questions_when_blank(self): + """Tests if a question exists""" + resp = self.client().get('/question/', headers=self.login()) + self.assertEqual(resp.status_code, 404) + self.assertIn('questions not found', str(resp.data)) + + def test_api_can_get_question_by_id(self): + """Test API can get a question using it's id""" + resp = self.client().post( + '/question/', headers=self.login(), data=self.question) + self.assertEqual(resp.status_code, 201) + id = json.loads(resp.data.decode())['id'] + result = self.client().get( + '/question/{}'.format(id), headers=self.login()) + self.assertEqual(result.status_code, 200) + self.assertIn('How do you create a list in python?', str(result.data)) + + + + def test_delete_question(self): + """Test API can delete a question""" + self.client().post( + '/question/', headers=self.login(), data=self.question) + resp = self.client().delete( + '/question/1/', headers=self.login(), data=self.question) + self.assertEqual(resp.status_code, 200) + self.assertIn('question deleted', str(resp.data)) + result = self.client().get( + '/question/1/', headers=self.login()) + self.assertEqual(result.status_code, 403) + self.assertIn('question Not found', str(result.data)) + + def test_question_search(self): + """Test API can serach for a question""" + self.question['question'] = 'How do you create a list in python?' + self.client().post( + '/question/', headers=self.login(), data=self.question) + resp = self.client().get( + '/question?q=forum', headers=self.login()) + self.assertIn('How do you create a list in python?', str(resp.data)) + resp = self.client().get('/question?q=forum', headers=self.login()) + self.assertEqual(len(json.loads(resp.data.decode())['question']), 1) + + def test_question_search_for_non_existent(self): + """Test API can search for non existing data""" + resp = self.client().get( + '/question?q=question20', headers=self.login(), data=self.question) + self.assertIn('questions not found', str(resp.data)) + + def test_get_questions_with_limit(self): + """Test API can search content limit""" + self.client().post( + '/question', headers=self.login(), data=self.question) + self.question['question'] = 'question written' + self.client().post( + '/question', headers=self.login(), data=self.question) + resp = self.client().get( + '/question?limit=1', headers=self.login(), data=self.question) + self.assertEqual(len(json.loads(resp.data.decode())['question']), 1) + + def test_input_is_alphanumerhic(self): + """Test API can search content limit with aplhabets""" + resp = self.client().get( + '/question?test', headers=self.login(), data=self.question) + self.assertIn( + 'Error, pass a number', json.loads(resp.data.decode()).values()) + + def tearDown(self): + """teardown all initialized variables.""" + with self.app.app_context(): + # drop all tables + db.session.remove() + db.drop_all() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/v2/api/tests/test_Users.py b/v2/api/tests/test_Users.py new file mode 100644 index 0000000..d8e2573 --- /dev/null +++ b/v2/api/tests/test_Users.py @@ -0,0 +1,129 @@ +""" +Test for the user class with authentication + +""" +import unittest +import os +import json +from app import db, create_app +from app.users,model import user +from api.endpoints import users +import unittest +import + + + +class UserTestCase(unittest.TestCase): + """This class runs checks for user test case""" + def setUp(self): + """Define test variables and initialize app.""" + self.app = create_app(config_name="testing") + self.client = self.app.test_client + self.user = {'email': 'flaskuser@email.com', 'password': 'flask2018'} + + # binds the app to the current context + with self.app.app_context(): + # create all tables + db.create_all() + + def registration(self): + """Test API can register a user""" + resp = self.client().post('/auth/signup/', data=self.user) + + def login(self): + """Login a user""" + self.registration() + resp = self.client().post('/auth/login/', data=self.user) + return {'Authorization': json.loads(resp.data.decode())['token']} + + def test_user_registration(self): + """Test API can create a user""" + resp = self.client().post('/signup/', data=self.user) + self.assertEqual(resp.status_code, 201) + self.assertIn('Sign Up has been successful', str(resp.data)) + + def test_user_existence(self): + """Test API can't register a user twice""" + user_data = {'email': 'flaskuser.email@com', 'password': 'flask2018'} + resp = self.client().post('/auth/register/', data=user_data) + self.assertEqual(resp.status_code, 403) + + def test_email_validation(self): + """Test correct email format""" + user_data = {'email': 'flaskuser,email@com', 'password': 'flask2018'} + resp = self.client().post('/auth/register/', data=user_data) + self.assertEqual(resp.status_code, 403) + self.assertIn('Invalid email address!', str(resp.data)) + + def test_password_length(self): + """ Test confirms if password it the same'""" + user_data = {'email': 'flaskuser@email.com', 'password': 'flask'} + resp = self.client().post('/auth/register/', data=user_data) + self.assertEqual(resp.status_code, 411) + self.assertIn('Password length is too short!', str(resp.data)) + + def test_registration_data(self): + """Test confirms that slots required have been filled""" + user_data = {'email': '', 'password': ''} + resp = self.client().post('/auth/register/', data=user_data) + self.assertEqual(resp.status_code, 400) + self.assertIn('Email and password required!', str(resp.data)) + + def test_login(self): + """Test API can login a user""" + self.registration() + resp = self.client().post('/auth/login/', data=self.user) + self.assertEqual(resp.status_code, 200) + self.assertIn('token', str(resp.data)) + + def test_login_with_wrong_password(self): + """Test for wrong password""" + self.registration() + self.user['password'] = 'django2018' + resp = self.client().post('/auth/login/', data=self.user) + self.assertEqual(resp.status_code, 401) + self.assertIn('Wrong password!', str(resp.data)) + + def test_login_with_missing_data(self): + """Test for wrong password""" + self.registration() + self.user = {'email': '', 'password': ''} + resp = self.client().post('/auth/login/', data=self.user) + self.assertEqual(resp.status_code, 401) + self.assertIn('User not found', str(resp.data)) + + def test_logout(self): + """Test API can logout a user""" + resp = self.client().post('/auth/logout', data=self.user) + + def test_reset_password(self): + """Test API can reset password for a user""" + self.registration() + self.user['password'] = 'python2018' + resp = self.client().post('/auth/reset_password/', data=self.user) + self.assertEqual(resp.status_code, 200) + self.assertIn('Password was changed successfully', str(resp.data)) + + resp = self.client().post('/auth/login', data=self.user) + self.assertEqual(resp.status_code, 200) + self.assertIn('token', str(resp.data)) + + def test_delete_user(self): + """ Test API can delete a user""" + self.client().post( + '/auth/delete/', headers=self.login(), data=self.user) + resp = self.client().delete( + '/auth/delete/', headers=self.login(), data=self.user) + self.assertEqual(resp.status_code, 200) + self.assertIn('User has been deleted', str(resp.data)) + + def tearDown(self): + """teardown all initialized variables.""" + with self.app.app_context(): + # drop all tables + db.session.remove() + db.drop_all() + +# Make the tests easily executable +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/v2/app/questions/models.py b/v2/app/questions/models.py deleted file mode 100644 index 929e4fe..0000000 --- a/v2/app/questions/models.py +++ /dev/null @@ -1,79 +0,0 @@ - - -class questions(db.Model): - """This class represents the questionlist table.""" - - __tablename__ = 'questions' - - id = db.Column(db.Integer, primary_key=True) - title = db.Column(db.String(500)) - user_id = 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()) - items = db.relationship( - 'Item', - backref='questions', - lazy='dynamic', - cascade="all,delete", - ) - - def __init__(self, title, user_id): - """initialization.""" - self.title = title - self.user_id = user_id - - def new_item(self): - """Adds a new question to the database """ - db.session.add(self) - db.session.commit() - - @staticmethod - def get_all(user_id): - """Gets all questions in a single query """ - return questionlist.query.all(user_id=user_id) - - def delete(self): - """Deletes an existing question from the database """ - db.session.delete(self) - db.session.commit() - - def __repr__(self): - """Represents the object instance of the model whenever it queries""" - return "".format(self.title) - - -class Item(db.Model): - """This class represents the questions table.""" - - - __tablename__ = 'items' - __table_args__ = {'extend_existing': True} - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(500)) - question_id = db.Column(db.Integer, db.ForeignKey('questions.id')) - - def __init__(self, name, question_id): - """initialization.""" - self.name = name - self.question_id = question_id - - def save(self): - """Adds a new question to the database """ - db.session.add(self) - db.session.commit() - - @staticmethod - def get_all(question_id): - """Gets all questions in a single query """ - return Item.query.all(question_id=question_id) - - def delete(self): - """Deletes an existing questions from the database """ - db.session.delete(self) - db.session.commit() - - def __repr__(self): - """Represents the object instance of the model whenever it queries""" - return "".format(self.name) \ No newline at end of file diff --git a/v2/app/users/model.py b/v2/app/users/model.py deleted file mode 100644 index 974227b..0000000 --- a/v2/app/users/model.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import re -import jwt -import datetime - -from app import db -from flask import jsonify -from flask_bcrypt import Bcrypt - - -class User(db.Model): - """This class represents the user table.""" - - __tablename__ = 'User' - - id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), nullable=False, unique=True) - password = db.Column(db.String(80), nullable=False) - questions = db.relationship( - 'question', - backref='users', - lazy='dynamic' - ) - - def __init__(self, email): - """initialization""" - self.email = email - self.password = '' - - @staticmethod - def validate_email(email): - """Method validates an email""" - address_matcher = re.compile(r'^[\w-]+@([\w-]+\.)+[\w]+$') - return True if address_matcher.match(email) else False - - def create_password(self, password): - """Method generates a hashed password""" - self.password = Bcrypt().generate_password_hash(password).decode() - - def validate_password(self, password): - """Method confirms that password is correct""" - return Bcrypt().check_password_hash(self.password, password) - - def gen_token(self): - """Generates a token""" - token = jwt.encode({ - 'id': self.id, - 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=30) - }, os.getenv('SECRET')) - return jsonify({'token': token.decode('UTF-8')}) - - def save(self): - """Adds a new user to the database """ - db.session.add(self) - db.session.commit() - - @staticmethod - def get_all(user_id): - """Gets all users in a single query """ - return User.query.all() - - def delete(self): - """Deletes an existing user from the database """ - db.session.delete(self) - db.session.commit() - - def __repr__(self): - """Represents the object instance of the model whenever it queries""" - return "".format(self.email) diff --git a/v2/run.py b/v2/run.py new file mode 100644 index 0000000..46b1fb5 --- /dev/null +++ b/v2/run.py @@ -0,0 +1,4 @@ +from api import app + +if __name__ = "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/v2/tests/test_Users.py b/v2/tests/test_Users.py index f0f86ae..06a9286 100644 --- a/v2/tests/test_Users.py +++ b/v2/tests/test_Users.py @@ -3,10 +3,11 @@ """ import unittest +import sh import os import json from app import db, create_app -from app.users,model import user +from app.users.model import user From 230f93d3134ec979b3bfa1536820f68c9dc1aec8 Mon Sep 17 00:00:00 2001 From: WaitheraMbugua <42140262+Waitherambugua@users.noreply.github.com> Date: Fri, 24 Aug 2018 05:31:26 +0300 Subject: [PATCH 4/5] Add badges --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 41b474d..4b2981f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # StackOverflow-Lite +[![Coverage Status](https://coveralls.io/repos/github/Waitherambugua/StackOverflow-Lite/badge.svg?branch=master)](https://coveralls.io/github/Waitherambugua/StackOverflow-Lite?branch=master) + StackOverflow is a web aplication that allows usere to view and upload questions. Gh-pages: https://waitherambugua.github.io From 0f7b8c56004e7dc5dd99975117c4c421a0318fee Mon Sep 17 00:00:00 2001 From: WaitheraMbugua <42140262+Waitherambugua@users.noreply.github.com> Date: Fri, 24 Aug 2018 08:17:01 +0300 Subject: [PATCH 5/5] Update README.md --- README.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b2981f..8ac60ac 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,138 @@ # StackOverflow-Lite -[![Coverage Status](https://coveralls.io/repos/github/Waitherambugua/StackOverflow-Lite/badge.svg?branch=master)](https://coveralls.io/github/Waitherambugua/StackOverflow-Lite?branch=master) + +[![Build Status](https://travis-ci.org/Waitherambugua/StackOverflow-Lite.svg?branch=database_tests)](https://travis-ci.org/Waitherambugua/StackOverflow-Lite) StackOverflow is a web aplication that allows usere to view and upload questions. +Project Overview +StackOverflowLite is a platform where people can ask questions and provide answers. + +Required Features +A user can create an account and log in +A user can post questions +A user can delete the questions they post +A user can post answers +A user can view the answers to the questions +A user can accept answer out of all answers to his/her questions as the preferred answer. + +**UI Templates** +Home Page +User Registration +User Login +User Profile Page +Post a question page +View a single questions and answers page +View questions and answers page +StackOverFlowLite Complete UI template on gh-pages +[a link](https://waitherambugua@github.io) link +Getting started +The following instructions will get a copy of StackOverFlowLite up and running on your machine for development and testing purposes + +*Requirements* +StackOverFlowLite will require the following: + +A computer running on any distribution of Unix or Mac or Windows OS +Installation +Clone the repository to your local machine +$ git clone https://github.com/waitherambugua/StackOverflow-Lite.git +Navigate to the directory +$ cd StackOverflowLite/UI +Open the file +$ pen index.html file with a browser of your choice +Create API endpoints +Getting started +The following instructions will get a copy of StackOverFlowLite up and running on your machine for development and testing purposes + +**The API** +**Requirements** +StackOverFlowLite will require the following: + +A computer running on any distribution of Windows OS or get some help from your administrator on how to install the application if on another OS +Python 3.5 or higher +Pip +Git +Virtualenv +Installation + +To clone and run this application, you will need Git installed on your computer. From your command line: + +# Clone this repository to your local machine +$ git clone https://github.com/waitherambugua/StackOverflow-Lite.git + +# Navigate to the folder that contains the app +$ cd StackOverflow-Lite + +# Create a virtual environment and activate it +$ virtualenv -p python3 venv + +# Activate the virtual environment +$ source venv/bin/activate + +# Install the requirements +$ pip install -r requirements.txt + +# Launch the application +$ python3 run.py + +### Run the tests +$ nosetests --with-coverage +API endpoints +Users Endpoints +Method | Endpoint | Functionality +---------------------------------------------------------------------------------- +POST | /StackOverFlowLite/api/v1/auth/register | Creates a user account + +POST | /StackOverFlowLite/api/v1/auth/login | Logs in a user + +POST | /StackOverFlowLite/api/v1/auth/logout | Logs out a user + +PUT | /StackOverFlowLite/api/v1/auth/reset-password | Reset a password for a logged user + +DELETE | /StackOverFlowLite/api/v1/questions/question-ID | Delete a request of a logged in user + +Questions Endpoints + +Method | Endpoint | Functionality +---------------------------------------------------------------------------------- +POST |/StackOverFlowLite/api/v2/questions | Add a question + +POST |/StackOverFlowLite/api/v2/questions/question-ID/answers | Add an answer + +GET |/StackOverFlowLite/api/v2/questions | Lists all questions + +GET | /StackOverFlowLite/api/v2/questions/questionID | List a question + +PUT | /StackOverFlowLite/api/v2/questions/questionID | Edit a question + +DELETE | /StackOverFlowLite/api/v2/questions/questionID | Delete a question + +Answers Endpoints +Method| Endpoint | Functionality +---------------------------------------------------------------------------------- +POST |/StackOverFlowLite/api/v2/questions/question-ID/answers | Add an answer + +GET |/StackOverFlowLite/api/v2/questions/questionID/answers | Lists all answers + +PUT |/StackOverFlowLite/api/v2/questions/questionID/answer/answerID | Edit an answer + + +StackOverFlowLite hosted on Heroku + +stackoverflowlite on heroku + +**Build with** +HTML, CSS, Javascript +Flask RESTful API + +**How to Contribute** +Fork repository to your github account +Create a branch +Make changes +Create a pull request + + +Contributors Linda Mbugua + Gh-pages: https://waitherambugua.github.io -Heroku API link: https://lstackoverflowlite-api-heroku.herokuapp.com/ +Heroku API link: https://mstackoverflowlite.herokuapp.com/