From bdc0b600991ff29cc1b6b95fabce16f5523f6c8a Mon Sep 17 00:00:00 2001 From: "nate.dalian@gmail.com" Date: Tue, 4 Apr 2023 13:53:40 +1200 Subject: [PATCH] Update --- backend/flaskr/__init__.py | 292 ++++++++++++++++++++++++++----------- backend/models.py | 11 +- backend/test_flaskr.py | 22 ++- 3 files changed, 234 insertions(+), 91 deletions(-) diff --git a/backend/flaskr/__init__.py b/backend/flaskr/__init__.py index 531034738..de25d1266 100644 --- a/backend/flaskr/__init__.py +++ b/backend/flaskr/__init__.py @@ -11,92 +11,212 @@ def create_app(test_config=None): # create and configure the app app = Flask(__name__) - setup_db(app) - - """ - @TODO: Set up CORS. Allow '*' for origins. Delete the sample route after completing the TODOs - """ - - """ - @TODO: Use the after_request decorator to set Access-Control-Allow - """ - - """ - @TODO: - Create an endpoint to handle GET requests - for all available categories. - """ - - - """ - @TODO: - Create an endpoint to handle GET requests for questions, - including pagination (every 10 questions). - This endpoint should return a list of questions, - number of total questions, current category, categories. - - TEST: At this point, when you start the application - you should see questions and categories generated, - ten questions per page and pagination at the bottom of the screen for three pages. - Clicking on the page numbers should update the questions. - """ - - """ - @TODO: - Create an endpoint to DELETE question using a question ID. - - TEST: When you click the trash icon next to a question, the question will be removed. - This removal will persist in the database and when you refresh the page. - """ - - """ - @TODO: - Create an endpoint to POST a new question, - which will require the question and answer text, - category, and difficulty score. - - TEST: When you submit a question on the "Add" tab, - the form will clear and the question will appear at the end of the last page - of the questions list in the "List" tab. - """ - - """ - @TODO: - Create a POST endpoint to get questions based on a search term. - It should return any questions for whom the search term - is a substring of the question. - - TEST: Search by any phrase. The questions list will update to include - only question that include that string within their question. - Try using the word "title" to start. - """ - - """ - @TODO: - Create a GET endpoint to get questions based on category. - - TEST: In the "List" tab / main screen, clicking on one of the - categories in the left column will cause only questions of that - category to be shown. - """ - - """ - @TODO: - Create a POST endpoint to get questions to play the quiz. - This endpoint should take category and previous question parameters - and return a random questions within the given category, - if provided, and that is not one of the previous questions. - - TEST: In the "Play" tab, after a user selects "All" or a category, - one question at a time is displayed, the user is allowed to answer - and shown whether they were correct or not. - """ - - """ - @TODO: - Create error handlers for all expected errors - including 404 and 422. - """ + with app.app_context(): + setup_db(app) + + """ + @TODO: Set up CORS. Allow '*' for origins. Delete the sample route after completing the TODOs + Use Flask-CORS to enable cross-domain requests and set response headers. + """ + CORS(app, resources={r"/*": {"origins": "*"}}) + + + """ + @TODO: Use the after_request decorator to set Access-Control-Allow + Create an endpoint to handle GET requests for questions, including pagination (every 10 questions). + This endpoint should return a list of questions, number of total questions, current category, categories. + """ + @app.after_request + def after_request(response): + response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization') + response.headers.add('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS') + return response + + + """ + @TODO: + Create an endpoint to handle GET requests + for all available categories. + """ + @app.route('/categories') + def get_categories(): + # get all category objects + categories = Category.query.all() + # create a dictionary with the category id as the key and the category type as the value + formatted_categories = {category.id: category.type for category in categories} + # return the dictionary as a json response + return jsonify({ + 'success': True, + 'categories': formatted_categories + }) + + + """ + @TODO: + Create an endpoint to handle GET requests for questions, + including pagination (every 10 questions). + This endpoint should return a list of questions, + number of total questions, current category, categories. + + TEST: At this point, when you start the application + you should see questions and categories generated, + ten questions per page and pagination at the bottom of the screen for three pages. + Clicking on the page numbers should update the questions. + """ + @app.route('/questions') + def get_questions(): + page = request.args.get('page', 1, type=int) + start = (page - 1) * QUESTIONS_PER_PAGE + end = start + QUESTIONS_PER_PAGE + questions = Question.query.all() + formatted_questions = [question.format() for question in questions] + categories = Category.query.all() + formatted_categories = {category.id: category.type for category in categories} + return jsonify({ + 'success': True, + 'questions': formatted_questions[start:end], + 'total_questions': len(formatted_questions), + 'categories': formatted_categories, + 'current_category': None + }) + + + """ + @TODO: + Create an endpoint to DELETE question using a question ID. + + TEST: When you click the trash icon next to a question, the question will be removed. + This removal will persist in the database and when you refresh the page. + """ + @app.route('/questions/', methods=['DELETE']) + def delete_question(question_id): + try: + question = Question.query.get(question_id) + question.delete() + return jsonify({ + 'success': True, + 'deleted': question_id + }) + except: + abort(422) + + """ + @TODO: + Create an endpoint to POST a new question, + which will require the question and answer text, + category, and difficulty score. + + TEST: When you submit a question on the "Add" tab, + the form will clear and the question will appear at the end of the last page + of the questions list in the "List" tab. + """ + @app.route('/questions', methods=['POST']) + def create_question(): + body = request.get_json() + new_question = body.get('question', None) + new_answer = body.get('answer', None) + new_category = body.get('category', None) + new_difficulty = body.get('difficulty', None) + try: + question = Question(question=new_question, answer=new_answer, category=new_category, difficulty=new_difficulty) + question.insert() + return jsonify({ + 'success': True, + 'created': question.id + }) + except: + abort(422) + + + """ + @TODO: + Create a POST endpoint to get questions based on a search term. + It should return any questions for whom the search term + is a substring of the question. + + TEST: Search by any phrase. The questions list will update to include + only question that include that string within their question. + Try using the word "title" to start. + """ + @app.route('/questions/search', methods=['POST']) + def search_questions(): + body = request.get_json() + search_term = body.get('searchTerm', None) + questions = Question.query.filter(Question.question.ilike(f'%{search_term}%')).all() + formatted_questions = [question.format() for question in questions] + return jsonify({ + 'success': True, + 'questions': formatted_questions, + 'total_questions': len(formatted_questions), + 'current_category': None + }) + + + """ + @TODO: + Create a GET endpoint to get questions based on category. + + TEST: In the "List" tab / main screen, clicking on one of the + categories in the left column will cause only questions of that + category to be shown. + """ + @app.route('/categories//questions') + def get_questions_by_category(category_id): + questions = Question.query.filter(Question.category == category_id).all() + formatted_questions = [question.format() for question in questions] + return jsonify({ + 'success': True, + 'questions': formatted_questions, + 'total_questions': len(formatted_questions), + 'current_category': category_id + }) + + """ + @TODO: + Create a POST endpoint to get questions to play the quiz. + This endpoint should take category and previous question parameters + and return a random questions within the given category, + if provided, and that is not one of the previous questions. + + TEST: In the "Play" tab, after a user selects "All" or a category, + one question at a time is displayed, the user is allowed to answer + and shown whether they were correct or not. + """ + @app.route('/quizzes', methods=['POST']) + def get_quiz_question(): + body = request.get_json() + previous_questions = body.get('previous_questions', None) + quiz_category = body.get('quiz_category', None) + if quiz_category['id'] == 0: + questions = Question.query.all() + else: + questions = Question.query.filter(Question.category == quiz_category['id']).all() + formatted_questions = [question.format() for question in questions] + def get_random_question(): + return formatted_questions[random.randint(0, len(formatted_questions) - 1)] + question = get_random_question() + while question['id'] in previous_questions: + question = get_random_question() + return jsonify({ + 'success': True, + 'question': question + }) + + """ + @TODO: + Create error handlers for all expected errors + including 404 and 422. + """ + @app.errorhandler(404) + def not_found(error): + return jsonify({ + 'success': False, + 'error': 404, + 'message': 'Not found' + }), 404 return app + + + diff --git a/backend/models.py b/backend/models.py index 3c5f56ed1..b7b1cfd87 100644 --- a/backend/models.py +++ b/backend/models.py @@ -3,8 +3,15 @@ from flask_sqlalchemy import SQLAlchemy import json -database_name = 'trivia' -database_path = 'postgresql://{}/{}'.format('localhost:5432', database_name) +db_username = 'student' +db_password = '123456' +db_name = 'trivia' + +# Create the connection string +database_path = f"postgresql://{db_username}:{db_password}@localhost:5432/{db_name}" + +# Old Query +#database_path = 'postgresql://{db_username}/{db_password}'.format('localhost:5432', database_name) db = SQLAlchemy() diff --git a/backend/test_flaskr.py b/backend/test_flaskr.py index 16f9c5dd6..e767d2e87 100644 --- a/backend/test_flaskr.py +++ b/backend/test_flaskr.py @@ -6,6 +6,9 @@ from flaskr import create_app from models import setup_db, Question, Category +db_username = 'student' +db_password = '123456' +db_name = 'trivia' class TriviaTestCase(unittest.TestCase): """This class represents the trivia test case""" @@ -14,8 +17,9 @@ def setUp(self): """Define test variables and initialize app.""" self.app = create_app() self.client = self.app.test_client - self.database_name = "trivia_test" - self.database_path = "postgres://{}/{}".format('localhost:5432', self.database_name) + #self.database_name = "trivia_test" + #self.database_path = "postgres://{}/{}".format('localhost:5432', self.database_name) + self.database_path = f"postgresql://{db_username}:{db_password}@localhost:5432/{db_name}" setup_db(self.app, self.database_path) # binds the app to the current context @@ -26,13 +30,25 @@ def setUp(self): self.db.create_all() def tearDown(self): + """Executed after reach test""" pass - + """ TODO Write at least one test for each test for successful operation and for expected errors. """ + def test_get_paginated_questions(self): + res = self.client().get('/questions') + data = json.loads(res.data) + + self.assertEqual(res.status_code, 200) + self.assertEqual(data['success'], True) + self.assertTrue(data['total_questions']) + self.assertTrue(len(data['questions'])) + self.assertTrue(len(data['categories'])) + + # Make the tests conveniently executable