From c63307e33ea44b78a0f72f7b6257426bbcd1035f Mon Sep 17 00:00:00 2001 From: "DESKTOP-RNK2HL9\\DELL" Date: Sun, 8 Sep 2024 18:07:02 +0700 Subject: [PATCH 1/2] feature/1-init-project --- backend/app.py | 10 ++++++ backend/flaskr/__init__.py | 41 +++++++++++++++++++++ backend/models.py | 5 +-- frontend/src/components/QuestionView.js | 48 ++++++++++++------------- 4 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 backend/app.py diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 000000000..3877c6494 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,10 @@ +import os +from flaskr import create_app + +os.environ['FLASK_APP'] = 'flaskr:create_app' +os.environ['FLASK_ENV'] = 'development' + +app = create_app() + +if __name__ == '__main__': + app.run() \ No newline at end of file diff --git a/backend/flaskr/__init__.py b/backend/flaskr/__init__.py index eef4a19d8..ba18cebed 100644 --- a/backend/flaskr/__init__.py +++ b/backend/flaskr/__init__.py @@ -21,17 +21,39 @@ def create_app(test_config=None): """ @TODO: Set up CORS. Allow '*' for origins. Delete the sample route after completing the TODOs """ + #CORS(app, resources={r"*/api/*" : {origins: '*'}}) + + CORS(app) """ @TODO: Use the after_request decorator to set Access-Control-Allow """ + @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(): + # Implement pagination + page = request.args.get('page', 1, type=int) + start = (page - 1) * 10 + end = start + 10 + + categories = Category.query.all() + formatted_categories = [category.format() for category in categories] + return jsonify({ + 'success': True, + 'categories': formatted_categories[start:end], + 'total_categories': len(formatted_categories) + }) """ @TODO: @@ -45,6 +67,25 @@ def create_app(test_config=None): 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(): + # Implement pagination + page = request.args.get('page', 1, type=int) + start = (page - 1) * 10 + end = start + 10 + + categories = Category.query.all() + formatted_categories = [category.format() for category in categories] + + questions = Question.query.all() + formatted_questions = [question.format() for question in questions] + + return jsonify({ + 'success': True, + 'questions': formatted_questions[start:end], + 'total_questions': len(formatted_questions), + 'categories': formatted_categories, + }) """ @TODO: diff --git a/backend/models.py b/backend/models.py index 3c5f56ed1..fd3176f0d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -4,7 +4,7 @@ import json database_name = 'trivia' -database_path = 'postgresql://{}/{}'.format('localhost:5432', database_name) +database_path = 'postgresql://postgres:12345@{}/{}'.format('localhost:5432', database_name) db = SQLAlchemy() @@ -17,7 +17,8 @@ def setup_db(app, database_path=database_path): app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False db.app = app db.init_app(app) - db.create_all() + with app.app_context(): + db.create_all() """ Question diff --git a/frontend/src/components/QuestionView.js b/frontend/src/components/QuestionView.js index a86b1f8db..ff296f6b8 100755 --- a/frontend/src/components/QuestionView.js +++ b/frontend/src/components/QuestionView.js @@ -1,8 +1,8 @@ -import React, { Component } from 'react'; -import '../stylesheets/App.css'; -import Question from './Question'; -import Search from './Search'; -import $ from 'jquery'; +import React, { Component } from "react"; +import "../stylesheets/App.css"; +import Question from "./Question"; +import Search from "./Search"; +import $ from "jquery"; class QuestionView extends Component { constructor() { @@ -23,18 +23,18 @@ class QuestionView extends Component { getQuestions = () => { $.ajax({ url: `/questions?page=${this.state.page}`, //TODO: update request URL - type: 'GET', + type: "GET", success: (result) => { this.setState({ questions: result.questions, totalQuestions: result.total_questions, categories: result.categories, - currentCategory: result.current_category, + // currentCategory: result.current_category, }); return; }, error: (error) => { - alert('Unable to load questions. Please try your request again'); + alert("Unable to load questions. Please try your request again"); return; }, }); @@ -51,7 +51,7 @@ class QuestionView extends Component { pageNumbers.push( { this.selectPage(i); }} @@ -66,7 +66,7 @@ class QuestionView extends Component { getByCategory = (id) => { $.ajax({ url: `/categories/${id}/questions`, //TODO: update request URL - type: 'GET', + type: "GET", success: (result) => { this.setState({ questions: result.questions, @@ -76,7 +76,7 @@ class QuestionView extends Component { return; }, error: (error) => { - alert('Unable to load questions. Please try your request again'); + alert("Unable to load questions. Please try your request again"); return; }, }); @@ -85,9 +85,9 @@ class QuestionView extends Component { submitSearch = (searchTerm) => { $.ajax({ url: `/questions`, //TODO: update request URL - type: 'POST', - dataType: 'json', - contentType: 'application/json', + type: "POST", + dataType: "json", + contentType: "application/json", data: JSON.stringify({ searchTerm: searchTerm }), xhrFields: { withCredentials: true, @@ -102,23 +102,23 @@ class QuestionView extends Component { return; }, error: (error) => { - alert('Unable to load questions. Please try your request again'); + alert("Unable to load questions. Please try your request again"); return; }, }); }; questionAction = (id) => (action) => { - if (action === 'DELETE') { - if (window.confirm('are you sure you want to delete the question?')) { + if (action === "DELETE") { + if (window.confirm("are you sure you want to delete the question?")) { $.ajax({ url: `/questions/${id}`, //TODO: update request URL - type: 'DELETE', + type: "DELETE", success: (result) => { this.getQuestions(); }, error: (error) => { - alert('Unable to load questions. Please try your request again'); + alert("Unable to load questions. Please try your request again"); return; }, }); @@ -128,8 +128,8 @@ class QuestionView extends Component { render() { return ( -
-
+
+

{ this.getQuestions(); @@ -147,7 +147,7 @@ class QuestionView extends Component { > {this.state.categories[id]} {`${this.state.categories[id].toLowerCase()}`} @@ -156,7 +156,7 @@ class QuestionView extends Component {

-
+

Questions

{this.state.questions.map((q, ind) => ( ))} -
{this.createPagination()}
+
{this.createPagination()}
); From 3fbaf1ee93349a555130f50482b091e005ec059a Mon Sep 17 00:00:00 2001 From: "DESKTOP-RNK2HL9\\DELL" Date: Sat, 21 Sep 2024 23:08:45 +0700 Subject: [PATCH 2/2] feature/2-implement-endpoint --- backend/flaskr/__init__.py | 132 ++++++++++++++++++++++-- frontend/src/components/FormView.js | 71 +++++++------ frontend/src/components/Question.js | 34 +++--- frontend/src/components/QuestionView.js | 22 ++-- frontend/src/components/QuizView.js | 87 ++++++++-------- 5 files changed, 235 insertions(+), 111 deletions(-) diff --git a/backend/flaskr/__init__.py b/backend/flaskr/__init__.py index ba18cebed..c33b7bb77 100644 --- a/backend/flaskr/__init__.py +++ b/backend/flaskr/__init__.py @@ -1,10 +1,11 @@ import os -from flask import Flask, request, abort, jsonify +from flask import Flask, request, abort, jsonify, Response from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS import random +from sqlalchemy import delete -from models import setup_db, Question, Category +from models import db, setup_db, Question, Category QUESTIONS_PER_PAGE = 10 @@ -46,13 +47,12 @@ def get_categories(): start = (page - 1) * 10 end = start + 10 - categories = Category.query.all() - formatted_categories = [category.format() for category in categories] + categories = get_all_formatted_category() return jsonify({ 'success': True, - 'categories': formatted_categories[start:end], - 'total_categories': len(formatted_categories) + 'categories': categories[start:end], + 'total_categories': len(categories) }) """ @@ -67,15 +67,14 @@ def get_categories(): 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') + @app.route('/questions', methods=['GET']) def get_questions(): # Implement pagination page = request.args.get('page', 1, type=int) start = (page - 1) * 10 end = start + 10 - categories = Category.query.all() - formatted_categories = [category.format() for category in categories] + categories = get_all_formatted_category() questions = Question.query.all() formatted_questions = [question.format() for question in questions] @@ -84,7 +83,7 @@ def get_questions(): 'success': True, 'questions': formatted_questions[start:end], 'total_questions': len(formatted_questions), - 'categories': formatted_categories, + 'categories': categories }) """ @@ -94,6 +93,32 @@ def get_questions(): 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(id): + id_question = int(id) + + try: + question = Question.query.filter(Question.id == id_question).one_or_none() + + if question is None: + handle_error(404, 'Error: question not found') + + db.session.delete(question) + db.session.commit() + except: + handle_error(422, 'An error occurred!') + + return jsonify({ + 'success': True, + 'message': "deleted successfully" + }) + + # return jsonify({ + # 'success': True, + # 'deleted': id_question, + # 'question': current_questions, + # 'total_question': len(current_questions) + # }) """ @TODO: @@ -105,6 +130,24 @@ def get_questions(): 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_difficulty = body.get('difficulty', None) + new_category = body.get('category', None) + + try: + question = Question(question=new_question, answer=new_answer, category=int(new_category), difficulty=int(new_difficulty)) + question.insert() + except: + handle_error(422, 'An error occurred!') + + return({ + 'success': True, + 'message': 'Create successfully!' + }) """ @TODO: @@ -116,6 +159,22 @@ def get_questions(): 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(): + page = request.args.get('page', 1, type=int) + start = (page - 1) * 10 + end = start + 10 + + body = request.get_json() + key_word = body['searchTerm'] + questions = db.session.query(Question).filter(Question.question.ilike(f'%{key_word}%')).all() + formatted_questions = [question.format() for question in questions] + + return jsonify({ + 'success': True, + 'questions': formatted_questions[start:end], + 'total_questions': len(formatted_questions), + }) """ @TODO: @@ -125,6 +184,25 @@ def get_questions(): categories in the left column will cause only questions of that category to be shown. """ + @app.route("/categories//questions") + def get_all_question(id): + # Implement pagination + page = request.args.get('page', 1, type=int) + start = (page - 1) * 10 + end = start + 10 + + id_category = int(id) + categories = Category.query.filter_by(id=id_category).all() + formatted_categories = [category.format() for category in categories] + questions = Question.query.filter_by(category=id_category).all() + formatted_questions = [question.format() for question in questions] + + return jsonify({ + 'success': True, + 'questions': formatted_questions[start:end], + 'total_questions': len(formatted_questions), + 'currentCategory': formatted_categories[0] + }) """ @TODO: @@ -137,12 +215,46 @@ def get_questions(): 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_question_to_play(): + data = request.get_json() + previous_questions = data.get('previous_questions') + quiz_category = data.get('quiz_category') + result = None + questions = [] + + # get all questions + if quiz_category['id'] is 0: + questions = Question.query.all() + else: + questions = Question.query.filter_by(category=quiz_category['id']).all() + + format_questions = [question.format() for question in questions] + if len(format_questions) != 0: + if len(previous_questions) is 0: + result = format_questions[0] + else: + data = [question for question in format_questions if question['id'] not in previous_questions] + if len(data) != 0: + result = data[0] + + return jsonify({ + 'question': result + }) """ @TODO: Create error handlers for all expected errors including 404 and 422. """ + def handle_error(code, message): + error_message = ({'message': message}) + abort(Response(error_message, code)) + + def get_all_formatted_category(): + categories = Category.query.all() + formatted_categories = [category.format() for category in categories] + return formatted_categories return app diff --git a/frontend/src/components/FormView.js b/frontend/src/components/FormView.js index fce7b4e4a..165fe1504 100755 --- a/frontend/src/components/FormView.js +++ b/frontend/src/components/FormView.js @@ -1,41 +1,52 @@ -import React, { Component } from 'react'; -import $ from 'jquery'; -import '../stylesheets/FormView.css'; +import React, { Component } from "react"; +import $ from "jquery"; +import "../stylesheets/FormView.css"; class FormView extends Component { constructor(props) { super(); this.state = { - question: '', - answer: '', + question: "", + answer: "", difficulty: 1, category: 1, - categories: {}, + categories: [], }; } componentDidMount() { $.ajax({ url: `/categories`, //TODO: update request URL - type: 'GET', + type: "GET", success: (result) => { + console.log(result); this.setState({ categories: result.categories }); return; }, error: (error) => { - alert('Unable to load categories. Please try your request again'); + alert("Unable to load categories. Please try your request again"); return; }, }); } submitQuestion = (event) => { + if ( + !this.state.question || + !this.state.answer || + !this.state.question.trim() || + !this.state.answer.trim() + ) { + alert("The question or answer is invalid!"); + return; + } + event.preventDefault(); $.ajax({ - url: '/questions', //TODO: update request URL - type: 'POST', - dataType: 'json', - contentType: 'application/json', + url: "/questions", //TODO: update request URL + type: "POST", + dataType: "json", + contentType: "application/json", data: JSON.stringify({ question: this.state.question, answer: this.state.answer, @@ -47,11 +58,11 @@ class FormView extends Component { }, crossDomain: true, success: (result) => { - document.getElementById('add-question-form').reset(); + document.getElementById("add-question-form").reset(); return; }, error: (error) => { - alert('Unable to add question. Please try your request again'); + alert("Unable to add question. Please try your request again"); return; }, }); @@ -63,44 +74,44 @@ class FormView extends Component { render() { return ( -
+

Add a New Trivia Question

- +
); diff --git a/frontend/src/components/Question.js b/frontend/src/components/Question.js index fba79b48b..bf547b6f8 100755 --- a/frontend/src/components/Question.js +++ b/frontend/src/components/Question.js @@ -1,5 +1,5 @@ -import React, { Component } from 'react'; -import '../stylesheets/Question.css'; +import React, { Component } from "react"; +import "../stylesheets/Question.css"; class Question extends Component { constructor() { @@ -16,32 +16,32 @@ class Question extends Component { render() { const { question, answer, category, difficulty } = this.props; return ( -
-
{question}
-
+
+
{question}
+
{`${category.toLowerCase()}`} -
Difficulty: {difficulty}
+
{difficulty}
delete this.props.questionAction('DELETE')} + src="delete.png" + alt="delete" + className="delete" + onClick={() => this.props.questionAction("DELETE")} />
this.flipVisibility()} > - {this.state.visibleAnswer ? 'Hide' : 'Show'} Answer + {this.state.visibleAnswer ? "Hide" : "Show"} Answer
-
+
Answer: {answer} diff --git a/frontend/src/components/QuestionView.js b/frontend/src/components/QuestionView.js index ff296f6b8..ec4309728 100755 --- a/frontend/src/components/QuestionView.js +++ b/frontend/src/components/QuestionView.js @@ -11,7 +11,7 @@ class QuestionView extends Component { questions: [], page: 1, totalQuestions: 0, - categories: {}, + categories: [], currentCategory: null, }; } @@ -29,7 +29,7 @@ class QuestionView extends Component { questions: result.questions, totalQuestions: result.total_questions, categories: result.categories, - // currentCategory: result.current_category, + currentCategory: null, }); return; }, @@ -84,7 +84,7 @@ class QuestionView extends Component { submitSearch = (searchTerm) => { $.ajax({ - url: `/questions`, //TODO: update request URL + url: `/questions/search`, //TODO: update request URL type: "POST", dataType: "json", contentType: "application/json", @@ -138,18 +138,18 @@ class QuestionView extends Component { Categories
    - {Object.keys(this.state.categories).map((id) => ( + {this.state.categories.map((category) => (
  • { - this.getByCategory(id); + this.getByCategory(category.id); }} > - {this.state.categories[id]} + {category.type} {`${this.state.categories[id].toLowerCase()}`}
  • ))} @@ -163,7 +163,9 @@ class QuestionView extends Component { key={q.id} question={q.question} answer={q.answer} - category={this.state.categories[q.category]} + category={this.state.categories.find( + (category) => category.id === q.category + )} difficulty={q.difficulty} questionAction={this.questionAction(q.id)} /> diff --git a/frontend/src/components/QuizView.js b/frontend/src/components/QuizView.js index 3b458b2a2..ce4a0a916 100644 --- a/frontend/src/components/QuizView.js +++ b/frontend/src/components/QuizView.js @@ -1,6 +1,6 @@ -import React, { Component } from 'react'; -import $ from 'jquery'; -import '../stylesheets/QuizView.css'; +import React, { Component } from "react"; +import $ from "jquery"; +import "../stylesheets/QuizView.css"; const questionsPerPlay = 5; @@ -11,10 +11,10 @@ class QuizView extends Component { quizCategory: null, previousQuestions: [], showAnswer: false, - categories: {}, + categories: [], numCorrect: 0, currentQuestion: {}, - guess: '', + guess: "", forceEnd: false, }; } @@ -22,13 +22,13 @@ class QuizView extends Component { componentDidMount() { $.ajax({ url: `/categories`, //TODO: update request URL - type: 'GET', + type: "GET", success: (result) => { this.setState({ categories: result.categories }); return; }, error: (error) => { - alert('Unable to load categories. Please try your request again'); + alert("Unable to load categories. Please try your request again"); return; }, }); @@ -47,12 +47,11 @@ class QuizView extends Component { if (this.state.currentQuestion.id) { previousQuestions.push(this.state.currentQuestion.id); } - $.ajax({ - url: '/quizzes', //TODO: update request URL - type: 'POST', - dataType: 'json', - contentType: 'application/json', + url: "/quizzes", //TODO: update request URL + type: "POST", + dataType: "json", + contentType: "application/json", data: JSON.stringify({ previous_questions: previousQuestions, quiz_category: this.state.quizCategory, @@ -66,13 +65,13 @@ class QuizView extends Component { showAnswer: false, previousQuestions: previousQuestions, currentQuestion: result.question, - guess: '', + guess: "", forceEnd: result.question ? false : true, }); return; }, error: (error) => { - alert('Unable to load question. Please try your request again'); + alert("Unable to load question. Please try your request again"); return; }, }); @@ -94,30 +93,30 @@ class QuizView extends Component { showAnswer: false, numCorrect: 0, currentQuestion: {}, - guess: '', + guess: "", forceEnd: false, }); }; renderPrePlay() { return ( -
    -
    Choose Category
    -
    -
    +
    +
    Choose Category
    +
    +
    ALL
    - {Object.keys(this.state.categories).map((id) => { + {this.state.categories.map((category, index) => { return (
    - this.selectCategory({ type: this.state.categories[id], id }) + this.selectCategory({ type: category.type, id: category.id }) } > - {this.state.categories[id]} + {category.type}
    ); })} @@ -128,11 +127,11 @@ class QuizView extends Component { renderFinalScore() { return ( -
    -
    +
    +
    Your Final Score is {this.state.numCorrect}
    -
    +
    Play Again?
    @@ -142,28 +141,28 @@ class QuizView extends Component { evaluateAnswer = () => { const formatGuess = this.state.guess // eslint-disable-next-line - .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, '') + .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "") .toLowerCase(); const answerArray = this.state.currentQuestion.answer .toLowerCase() - .split(' '); + .split(" "); return answerArray.every((el) => formatGuess.includes(el)); }; renderCorrectAnswer() { let evaluate = this.evaluateAnswer(); return ( -
    -
    +
    +
    {this.state.currentQuestion.question}
    -
    - {evaluate ? 'You were correct!' : 'You were incorrect'} +
    + {evaluate ? "You were correct!" : "You were incorrect"}
    -
    {this.state.currentQuestion.answer}
    -
    - {' '} - Next Question{' '} +
    {this.state.currentQuestion.answer}
    +
    + {" "} + Next Question{" "}
    ); @@ -176,16 +175,16 @@ class QuizView extends Component { ) : this.state.showAnswer ? ( this.renderCorrectAnswer() ) : ( -
    -
    +
    +
    {this.state.currentQuestion.question}
    - +