Build a Simple LMS-Integrated Quiz App Using Python, React.js, and LTI 1.3 – Part 5

Create a default questions module

In our Flask LTI project, let’s create a separate module for our questions in JSON format. Alongside the app.py, create a questions.py file and add as many questions as you want, in this format which is essentially a python list of dictionaries:

questions = [
{
    "questionText": "Where is the Taj Mahal?",
    "answerOptions": [
        { 'answerText': 'Agra', 'isCorrect': True },
        { 'answerText': 'New Delhi', 'isCorrect': False },
        { 'answerText': 'Mumbai', 'isCorrect:': False },
        { 'answerText': 'Chennai', 'isCorrect': False }
    ]
},
{
    "questionText": 'Where is the Great Wall of China?',
    "answerOptions": [
        {'answerText': 'China', 'isCorrect': True },
        {'answerText': 'Beijing', 'isCorrect': False },
        {'answerText': 'Shanghai', 'isCorrect': False },
        {'answerText': 'Tianjin', 'isCorrect': False }
    ]
},
{
    "questionText": "How many countries in the world?",
    "answerOptions": [
        { "answerText": "120", 'isCorrect': False },
        { "answerText": "183", 'isCorrect': False },
        { "answerText": "170", 'isCorrect': False },
        { "answerText": "195", 'isCorrect': True },
   ]
 },
{
    "questionText": "How many continents are there?",
    "answerOptions": [
        { "answerText": "5", 'isCorrect': False },
        { "answerText": "6", 'isCorrect': False },
        { "answerText": "7", 'isCorrect': True },
        { "answerText": "8", 'isCorrect': False },
   ]
 }
]

Create API routes and HTTP request methods

In the Flask app.py, add your new API routes/endpoints after the LTI-related routes. We’re not currently using anything special, such as Flask-RESTful — but as our app grows, it might be advisable to use a nice REST extension and break the API out into it’s own module. Here’s our simple CRUD configuration for now.

###############################################################
# End LTI-related routes
###############################################################

###############################################################
# API routes
###############################################################

@app.route('/api/questions', methods=['GET'])
def api_get_questions():
    return jsonify(get_questions())

@app.route('/api/questions/<question_id>', methods=['GET'])
def api_get_question(question_id):
    return jsonify(get_question_by_id(question_id))

@app.route('/api/questions/add',  methods = ['POST'])
def api_add_question():
    questionText = request.get_json()
    return jsonify(insert_quiz_question(questionText))

@app.route('/api/questions/update',  methods = ['PUT'])
def api_update_question():
    questionText = request.get_json()
    return jsonify(update_question(questionText))

@app.route('/api/questions/delete/<question_id>',  methods = ['DELETE'])
def api_delete_question(question_id):
    return jsonify(delete_question(question_id))

###############################################################
# End API routes
###############################################################

The HTTP GET on /api/questions will get us what we need in our current quiz frontend. The other endpoints are available for use as your app grows. They are here to cover all the bases and scale the project on the back-end getting ahead of necessary features — one idea would be a new component that allows instructors to add new questions!

One last file that we’ll need to create alongside our app.py is a dbmethods.py. module. Place your methods (called by our API handlers) for performing database queries in this file:

import sqlite3

###############################################################
# Database functions
###############################################################

def connect_to_db():
    conn = sqlite3.connect('database.db')
    return conn

def drop_db_table():
    try:
        conn = connect_to_db()
        conn.execute('''DROP TABLE questions''')
        conn.commit()
        print("Questions table dropped successfully")
    except:
        print("Questions table drop failed")
    finally:
        conn.close()

def create_db_table():
    try:
        conn = connect_to_db()
        conn.execute('''
            CREATE TABLE questions (
                question_id INTEGER PRIMARY KEY,
                questionText TEXT NOT NULL,
                answerOptions TEXT NOT NULL
            );
        ''')
        conn.commit()
        print("Questions table created successfully")
    except:
        print("Questions table creation failed")
    finally:
        conn.close()


def insert_quiz_question(questionText):
    inserted_question = {}
    try:
        conn = connect_to_db()
        cur = conn.cursor()
        cur.execute("INSERT INTO questions (questionText, answerOptions) VALUES (?, ?)", (questionText['questionText'], repr(questionText['answerOptions'])) )
        conn.commit()
        inserted_question = get_question_by_id(cur.lastrowid)
    except:
        conn().rollback()

    finally:
        conn.close()

    return inserted_question


def get_questions():
    questions = []
    try:
        conn = connect_to_db()
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        cur.execute("SELECT * FROM questions")
        rows = cur.fetchall()

        # convert row objects to dictionary
        for i in rows:
            questionText = {}
            questionText["question_id"] = i["question_id"]
            questionText["questionText"] = i["questionText"]
            questionText["answerOptions"] = eval(i["answerOptions"])
            questions.append(questionText)

    except:
        questions = []

    return questions


def get_question_by_id(question_id):
    questionText = {}
    try:
        conn = connect_to_db()
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        cur.execute("SELECT * FROM questions WHERE question_id = ?", (question_id,))
        row = cur.fetchone()

        # convert row object to dictionary
        questionText["question_id"] = row["question_id"]
        questionText["questionText"] = row["questionText"]
        questionText["answerOptions"] = eval(row["answerOptions"])
    except:
        questionText = {}

    return questionText


def update_question(questionText):
    updated_question = {}
    try:
        conn = connect_to_db()
        cur = conn.cursor()
        cur.execute("UPDATE questions SET questionText = ?, answerOptions = ? WHERE question_id =?", (questionText["questionText"], repr(questionText["answerOptions"]), questionText["question_id"]))
        conn.commit()
        #return the questionText
        updated_question = get_question_by_id(questionText["question_id"])
    except:
        conn.rollback()
        updated_question = {}
    finally:
        conn.close()

    return updated_question


def delete_question(question_id):
    message = {}
    try:
        conn = connect_to_db()
        conn.execute("DELETE from questions WHERE question_id = ?", (question_id,))
        conn.commit()
        message["status"] = "questionText deleted successfully"
    except:
        conn.rollback()
        message["status"] = "Cannot delete questionText"
    finally:
        conn.close()

    return message

These methods are essentially aligned with our API endpoints when they are called. They are operating on a local sqlite database which will get created once a connection is made. In our Flask app – whenever we launch the new server, we will drop the questions table, recreate it, and load questions from the questions.py module.

SOOO, keep in mind — if you used a tool to create new questions; 1) DB Browser for SQLite OR 2) the Python REPL to connect to the database, running manual queries OR 3) Postman (or curl from the command line) to add new test questions calling the /api/questions/add endpoint that we just created — make sure that you’ve removed or commented out the logic that drops the table to load up our default questions or you’ll lose your work!

There are other approaches that could be taken here, such as using an ORM like SQLAlchemy for Flask. Again, since this is a basic, simple project, we’re just relying on the one questions table for now. I also chose to do a couple of funky things… In order to store an array, I’m finding that I can easily apply repr() when storing — and eval() when retrieving, in order to store and query the exact string and retain the complete syntax of the array. It just seems to make things easier when working with Javascript notation in the end.

Next steps, import the 2 new modules.

In step 1, there were 2 lines commented out at the top of the app.py file that can now be added. The result should look like this, with all our other imports:

from questions import questions
from dbmethods import drop_db_table, create_db_table, get_questions, get_question_by_id, insert_quiz_question, update_question, delete_question

After our API routes, place a few lines that will drop and create the database table (we’re assuming “questions” for all of these). Again for scalability, add the acceptance of arguments (for any table name perhaps) if you want to make the database methods more general:

###############################################################
# End API routes
###############################################################

drop_db_table()
create_db_table()

for i in questions:
    print(insert_quiz_question(i))

After dropping and recreating a fresh table (while we’re developing), the code loops through the imported questions array and inserts them. I’m having all this print to the console for now.

Make sure you are in your Flask project folder and you have activated your virtual environment. Run the Flask server, which should start up and make itself available on localhost and TCP port 5000:

python app.py

My console looks like this:

Run a quick test to ensure the questions JSON is being served up from the backend:

http://localhost:5000/api/questions

Here’s just a portion of what we should be seeing:

Update frontend (react.js component) to use the Flask API

Our last piece before testing the full stack tool with a provider is to make sure we have removed the hard-coded quiz questions from our App.js. We want our React app to load the questions into memory from the get_questions API endpoint. The code below shows the hard-coded JSON commented out and replaced with a new version of the useEffect() hook:

  useEffect(() => {
    fetch("/api/questions")
      .then((response) => response.json())
      .then((json) => setQuestions(json))
      .catch((error) => console.error(error));
  }, []);

  // //useEffect(() => {
  // //  setQuestions([
  // //    {
  // //      questionText: "Where is the Taj Mahal?",
  // //      answerOptions: [
  // //        { answerText: "Agra", isCorrect: true },
  //         { answerText: "New Delhi", isCorrect: false },
  //         { answerText: "Mumbai", isCorrect: false },
  //         { answerText: "Chennai", isCorrect: false },
  //       ],
  //     },
  //     {
  //       questionText: "Where is the Great Wall of China?",
  //       answerOptions: [
  //         { answerText: "China", isCorrect: true },
  //         { answerText: "Beijing", isCorrect: false },
  //         { answerText: "Shanghai", isCorrect: false },
  //         { answerText: "Tianjin", isCorrect: false },
  //       ],
  //     },
  //   ]);
  // }, []);

Attempting to run the React development build will fail because of the relative path looking to load questions from the backend API. You’ll need to build the frontend again (like the end of Part 4) and place the resulting build files in the static/js and static/css directories in the Flask project. At that point we should be all set to test. Shoot me a comment on the related post if you run into any issues or have questions.

In the next part, we’ll test things out. Feel free to reference the following for getting that started on your own:

Ad-hoc platforms for LTI tool integrations testing
https://pypi.org/project/PyLTI1p3/
https://github.com/dmitry-viskov/pylti1.3-flask-example

I hope to also address some or all of the following in future posts (this is mostly a reminder for me):

  • Leverage the LTI Advantage Deeplinking Message spec to give folks with certain roles (teacher, instructor, TA) the ability to create new quiz questions.
  • Implement the use of libraries and extensions that equate to a better, more scaleable app:
    • Maybe Axios HTTP in the React.js project, placing the use of the fetch API with some other method.
    • Consider Flask-RESTful
    • Consider GraphQL and noSQL
    • Consider an ORM that more easily supports databases (Postgres, MySQL) that aren’t SQLite.
    • Convert to a new tutorial using Django, DRF, and Vue.js as front-end.
  • Address the unrealistic scoring logic currently in the LTI-related route.
  • A Troubleshooting post that addresses:
    • Launching the app as an instructor or admin and why the app fails to post unless you’re a student.
    • Potential CORS-related pitfalls??
  • Easier environment for development and automation / tooling.
  • Adding questions with Postman
  • Add toast notifications on correct/incorrect answers
  • Implement on Moodle
  • Implement on Canvas

Leave a Reply

Your email address will not be published. Required fields are marked *

+ 29 = 35

This site uses Akismet to reduce spam. Learn how your comment data is processed.