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

Related Posts

Part 1

  • Goals and Assumptions.
  • Initial Setup of the Flask backend.

Part 2

  • Implement LTI-related routes in Flask.
  • Do my best to explain the abstraction that’s happening with PyLTI1p3

Part 3

  • Question data – JSON structure.
  • Revisit project directories.
  • Use create-react-app to start a new React.js front-end project.
  • Install dependencies: Tailwind.
  • Start building out the initial App.js with the proper hooks and event handler. A single page app (SPA), very simple, so we’re not venturing outside this one file for our single quiz component.

Part 4

  • Create the Flask template and arguments to be passed to the frontend.
  • Build the React.js frontend and stage static files.

Part 5

  • Create a Python module to contain the initial set of questions.
  • Create Flask API routes with HTTP request definitions.
  • Create a separate Python module for the database methods. Creation of sqlite database and initial loading of questions.
  • Final update to the frontend to use the Flask API.

Github Repos:

Frontend (React, javascript): https://github.com/eharvey71/react-simple-quiz

Backend (Flask, Python): https://github.com/eharvey71/lti_example_quiz_react_flask

Resources:

Reference Project: https://github.com/dmitry-viskov/pylti1.3-flask-example

PyLTI1p3 for implementing LTI 1.3/Advantage in Python: https://pypi.org/project/PyLTI1p3/

LTI 1.3 Core Specification: https://www.imsglobal.org/spec/lti/v1p3

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

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

In parts 4 and 5 of this series, we’ll continue to build out some major backend components of our Quiz app. In Part 3, we created a simple React.js component but it currently attempts to post to grades to an invalid URL. This is because our approach to the project is to build the react (js/css) static files, but first we will need to update our Flask template to take care of this.

Create the Flask template

Back in Part 1, we set up our Flask app to look for template and static files in ./frontend/templates and ./frontend/static, respectively.

In Part 2, we then created an LTI Launch route to do the following:

We’re expected to return a template. I chose index.html for the name, but feel free to use a naming convention that works for your project. We also pass a number of arguments to the rendered page. We’re currently only using the highlighted properties (below) in our project. The others are examples of keys that could come in handy if you want to display the user’s name or confirm a deep link launch. We’ll cover this in a separate, future post.

 'page_title': 'LTI 1.3 Flask-React Quiz',
 'is_deep_link_launch': message_launch.is_deep_link_launch(),
 'launch_data': message_launch.get_launch_data(),
 'launch_id': message_launch.get_launch_id(),
 'curr_user_name': message_launch_data.get('name', ''),

Back in the Flask backend of our project, my layout looks like this. Yours may vary.

You will want to create a frontend directory within your root project directory. While this isn’t necessary to structure it this way (creating static and template directories in your Flask project root is fine), I like to denote that the frontend is in fact a separate react project, not your typical Flask project layout.

In your frontend directory, create static and templates directories at the same branch in your directory tree.

Create your empty template file in the templates directory. In my case, index.html. Enter or paste the following html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>{{ page_title }}</title>
    <script>
      var launchId = "{{ launch_id }}";
    </script>
    <link
      href="{{ url_for('static', filename='css/<Put Your React Static CSS File Here>) }}"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="root"></div>
    <script
      type="text/javascript"
      src="{{ url_for('static', filename='js/<Put Your React Static JS File Here') }}"
      charset="utf-8"
    ></script>
  </body>
</html>

This simple template does the following:

  • Grabs page_title and launch_id from the kwargs passed to the template when rendered.
  • References the static react CSS/JS files that we have yet to create.
  • The body of our Flask template loads a div with “root”. Always make sure the order is following this convention, at the top of the body of your HTML template / document, prior to loading the javascript <script>.

Build and Implement the React.js frontend

Next, you’ll want to build your React project (as you would for production) using the proper command at your terminal, the root of your frontend/react project, wherever you chose to create that. In my case, I’m using a separate project altogether. Another approach would’ve been to build everything in the frontend directory within your backend/Flask project. I will cover a more “elegant” (questionable) method by using webpack, in a later post. I’m also open to suggestions for better tooling of this method. I aspire to be a solid Vue.js and Django developer, so some of this is outside of my … area of concentration, perhaps? Anywho:

npm run build

Finally copy your react project’s static directory (including all sub-directories) over to the Flask project, so that the structure is similar to this:

React.js project: /build/static.. to Flask project: /frontend/static

The file names that are generated are unique to your project, so they’ll be different from mine in the image above.

Finally, update the index.html template with names that match your main.xxxxx.js and main.xxxxx.css files. I’ve highlighted the 2 lines of code that need to be changed in the original screenshot of the template above.

In Part 5, we’ll make one last update to our React component, so that we’re dynamically retrieving data via API and a database. But first, we’ll create a default questions python module, API routes and method handlers, and some backend database handlers.

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

Requirements for this part

  • Install the lastest Node.js for your OS: https://nodejs.org
  • A directory to contain a new ‘frontend’ project.

Question Data Structure and Initial Testing

In the next part, we will be building out the front-end. The super-simple quiz that we’ll build in React takes a JSON-formatted set of questions and a list of answers. Each answer will have an ‘isCorrect’ boolean property that will help us determine how to process the final score. The layout will resemble the following, with ‘questionText’ and an array of 4 ‘answerOptions’ objects with ‘answerText’ and ‘isCorrect’ properties.

   [
      {
        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 },
        ],
      },
    ]

The goal is to load up the data into a database, converting row objects into dictionary format, and serving up the entire payload through an API using our Flask server. Since we’re not quite there yet, and we want to focus on just the front-end at the moment, we’ll simply load this JSON into a React useEffect() hook for testing purposes.

Project Directory Structure

Feel free to change or add directories into a structure that’s more beneficial for your project, keeping everything more organized than what I have, and perhaps defining both a backend and frontend directory structure. The current layout looks like the following for our backend project — so far:

├── app.py
├── configs
│   ├── private.key
│   ├── public.key
│   └── reactquiz.json
├── env

I personally chose to start the frontend in a completely new project, where I then build the static files from the react.js end of things and copy over the final static files into a frontend directory, sitting at the root of the backend project. Like this:

├── app.py
├── configs
│   ├── private.key
│   ├── public.key
│   └── reactquiz.json
├── env
├── frontend
│   ├── static
│   └── templates

This is not scalable in the long term for large projects, unless you choose to automate the deployment of the static builds from your react application in some way or leverage updates to webpack. Since tooling for production builds is out of the scope of Part 3, we’ll start a second (create-react-app) project and perform a manual copy of built files to the Flask server for the final demonstration and testing.

Alright, let’s continue!

Wherever you choose to start your frontend piece (either in the current project root directory or separate project altogether), run the following at the terminal and keep in mind that this will create a new directory for the project.

npx create-react-app frontend

More information about create-react-app can be found here: https://create-react-app.dev/docs/getting-started/

After creating the app, follow the directions that are given to change to the new directory and test that everything is working:

cd frontend
npm start

Stop the development server (Ctrl-C ). Open your editor in the frontend directory and remove everything from the src subdirectory. We’re going to create a few new files as we go along.

Tailwind – Note: Tailwind is a rapidly changing project. Make sure you reference the current install docs if you run into any issues with the steps below.

Back to the terminal and the frontend directory, let’s install the Tailwind library:

npm install -D tailwindcss
npx tailwindcss init

Add the highlighted line below to the new tailwind.config.js file, created in the last step:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}

Finally, create a new index.css file in the frontend/src directory and add the following Tailwind directives:

@tailwind base;
@tailwind components;
@tailwind utilities;

Create another file (index.js) in the frontend/src directory and add the following code. This is straightforward, common code for launching a new react App component in a virtual DOM:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Now, let’s start work on the App.js component, which will contain the core code of our new quiz app. Create a third file in the frontend/src directory and name it App.js (following the proper naming convention of capitalizing the first letter of the new component file). Place the following code at the top:

import React, { useState, useEffect } from "react";

function App() {
  const [questions, setQuestions] = useState(null);
  const [currentQuestion, setCurrentQuestion] = useState(0);
  const [showScore, setShowScore] = useState(false);
  const [score, setScore] = useState(0);

  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 },
        ],
      },
    ]);
  }, []);

In the above code:

  • Lines 1-7: Initializing the App and adding our pieces of state:
    • Questions (eventually loaded from our source API) will be managed by questions and currentQuestion.
    • showScore and score will be used for managing the points accumulated from correct answers and displaying the result at the end of the quiz.
  • Lines 9-30: In this initial testing phase, we’ll load the quiz questions and answers through the use of a useEffect() hook. In a later phase, this will contain a method for fetching the data through the backend API.

The next bit looks like this:

useEffect(() => {
    if (!showScore) return;
    const launchId = window.launchId;
    fetch("/api/score/" + launchId + "/" + score + "/", {
      method: "POST",
    })
      .then((response) => response.json())
      .catch((error) => console.error(error));
  }, [showScore, score]);
  • Line 32: The declaration of another useEffect() hook. The intention of this one will be to POST the final grade to the platform once the state of showScore is true. This will happen after the final question has been answered.
  • Line 33: Since our useEffect may fire multiple times based on the last line and the array of dependencies containing ‘[showScore, score]’, we don’t want to do anything until showScore’s state is true.
  • Line 34: The unique launch ID (more on this later) that’s contained in the Flask template (window) is retrieved for inclusion in the POST to the platform.
  • Lines 35-39: The Fetch API is used here to post the final score through our flask implementation, taking a JSON response and any error output. This could be implemented in a number of ways, including using the Axios library for making promise-based requests. This final code assumes that we are using ‘npm run build’ instead of running in dev mode, so the path to /api/score doesn’t include the full path to our local flask dev server. In other words, this assumes that we are including the static product (.js/.css files) in the backend. More clarification on this later.

Our quiz app will invoke a separate button for each answer. Here’s an event handler that fires once a button is clicked:

const handleAnswerButtonClick = (isCorrect) => {
    if (isCorrect === true) {
      setScore((score) => score + 1);
    }
    const nextQuestion = currentQuestion + 1;
    if (nextQuestion < questions.length) {
      setCurrentQuestion(nextQuestion);
    } else {
      setShowScore(true);
    }
  };

Lines 42-45: The handler receives the current correct answer property and adds to the score for a correct answer.
Lines 46-50: Set the next question and check if we have are finished with all the questions. If so, we will set the state for rendering the final score.

Our simple quiz app uses a conditional (ternary) operators to render the questions and final score. We’re using Tailwind CSS for styling.

return (
    <>
      <div className="bg-white pt-10 pb-8 shadow-xl sm:mx-auto sm:rounded-lg sm:px-10 w-128">
        <h1 className="mb-4 text-4xl font-extrabold">Quiz</h1>
        {showScore ? (
          <div>
            You scored {score} out of {questions.length}
          </div>
        ) : (
          <>
            <p>
              Question {currentQuestion + 1} of {questions && questions.length}
            </p>
            <p>----</p>
            <p className="mb-4">
              {questions && questions[currentQuestion].questionText}
            </p>

            <div className="flex space-x-4">
              {questions &&
                questions[currentQuestion].answerOptions.map(
                  (answerOptions, index) => (
                    <button
                      key={index}
                      className="text-white sm:px-2 sm:py-2 bg-sky-700 hover:bg-sky-800 rounded"
                      onClick={() =>
                        handleAnswerButtonClick(answerOptions.isCorrect)
                      }
                    >
                      {answerOptions.answerText}
                    </button>
                  )
                )}
            </div>
          </>
        )}
      </div>
    </>
  );
}

export default App;

Once all the code comes together, run the app to ensure that all is working. The score submission will throw an error until the backend pieces are completed in the next section.

npm run

Connect to http://127.0.0.1:3000 to give the quiz frontend a test

Stay tuned for several new parts in this series, where we’ll complete the following:

  • Implement the backend APIs and routes
  • Set up a database (SQLite) for storing and retrieving quiz questions through a set of database methods for running queries.
  • Complete the Flask template for final rendering of our react component.
  • Test using the saltire platform emulator.
  • Create a diagram of all of the moving parts (LTI (Platform <-> Tool), Flask (LTI, API <-> Database), frontend.

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

Check out part one first — where we walk through the setup of the Flask backend: Build a Simple LMS-Integrated Quiz App Using Python, React.js, and LTI 1.3 – Part 1

Part 2 – Flask and implementing LTI-related routes

Picking up where we left off in the app.py file in our project root directory

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

@app.route('/login/', methods=['GET', 'POST'])
def login():
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    launch_data_storage = get_launch_data_storage()
    flask_request = FlaskRequest()

    target_link_uri = flask_request.get_param('target_link_uri')
    if not target_link_uri:
        raise Exception('Missing "target_link_uri" param')

    oidc_login = FlaskOIDCLogin(
        flask_request,
        tool_conf,
        launch_data_storage=launch_data_storage
    )

    return oidc_login\
        .enable_check_cookies()\
        .redirect(target_link_uri)

The above function contains some elements that we will see repeat in the following – such as the tool conf, FlaskRequest, and launch data storage. The login route is used to invoke an LTI 1.3 launch where you first receive a login initialization request and return to the platform. The request is handled by first creating a new OIDCLogin object. An exception is raised if an OIDC registration or redirect url doesn’t exist.

  • Line 76 – ToolConfJsonFile() – receives location of the json config file defined in Step 1.
  • Line 77 – get_launch_data_storage() – retrieves Flask Cache storage.
  • Line 78 – Set up the session through a Flask request.
@app.route('/launch/', methods=['POST'])
def launch():
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    request = FlaskRequest()
    launch_data_storage = get_launch_data_storage()
    message_launch = FlaskMessageLaunch(
        request,
        tool_conf,
        launch_data_storage=launch_data_storage
    )
    message_launch_data = message_launch.get_launch_data()
    #pprint.pprint(message_launch_data)

    tpl_kwargs = {
        'page_title': 'LTI 1.3 Flask App',
        'is_deep_link_launch': message_launch.is_deep_link_launch(),
        'launch_data': message_launch.get_launch_data(),
        'launch_id': message_launch.get_launch_id(),
        'curr_user_name': message_launch_data.get('name', ''),
    }
    return render_template('index.html', **tpl_kwargs)

The launch route above is similar in set up as the login() function and performs an LTI message launch, following successful authorization.

The tpl_kwargs dictionary includes arguments that will be sent to the browser when rendering the Flask template page. Some of these aren’t necessarily used for this tutorial and will be ignored when launching the react quiz app. The unused keys in this project include deep link verification and passing in the current user name.

@app.route('/api/score/<launch_id>/<earned_score>/', methods=['POST'])
def score(launch_id, earned_score):
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    flask_request = FlaskRequest()
    launch_data_storage = get_launch_data_storage()
    message_launch = FlaskMessageLaunch.from_cache(launch_id, flask_request, tool_conf,
                                                           launch_data_storage=launch_data_storage)

    resource_link_id = message_launch.get_launch_data() \
        .get('https://purl.imsglobal.org/spec/lti/claim/resource_link', {}).get('id')

    if not message_launch.has_ags():
        raise Forbidden("Don't have grades!")

    sub = message_launch.get_launch_data().get('sub')
    timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
    earned_score = int(earned_score)

    grades = message_launch.get_ags()
    sc = Grade()
    sc.set_score_given(earned_score) \
        .set_score_maximum(100) \
        .set_timestamp(timestamp) \
        .set_activity_progress('Completed') \
        .set_grading_progress('FullyGraded') \
        .set_user_id(sub)

    sc_line_item = LineItem()
    sc_line_item.set_tag('score') \
        .set_score_maximum(100) \
        .set_label('Score')
    if resource_link_id:
        sc_line_item.set_resource_id(resource_link_id)

    result = grades.put_grade(sc, sc_line_item)

    return jsonify({'success': True, 'result': result.get('body')})

The above will POST the final grade to the platform (LMS) through the LTI Assignment and Grade Service (AGS). We’ll accomplish the following, assuming the user has the proper role and the assignment has been set up in the platform to accept the grade update:

  • A check is performed for AGS availability and will send any errors back to the server log.
  • The score passed to the API endpoint is pushed to the grade center (or gradebook) after setting up the column with the proper configuration and timestamp.

This final route is set up for exposing the public key for our tool:

@app.route('/jwks/', methods=['GET'])
def get_jwks():
    tool_conf = ToolConfJsonFile(get_lti_config_path())
    return jsonify({'keys': tool_conf.get_jwks()})

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

In part 3, we will write our (very simple) Quiz front-end using React.js and Tailwind CSS.

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

Let’s begin again! Welcome to the real part 1 of this series.

Part 1 – Initial Setup of the Flask Backend

Goals:

  • Build a Python Flask-based backend for a simple quiz application using the pyLTI1p3 library that supports LTI 1.3 / LTI Advantage. Score passback is included in the launch.
  • Build a simple react.js front-end that reads quiz questions from APIs provided by Flask. The front-end will load all questions up-front in our example.
  • The Flask backend of the final quiz tool will handle the following tasks:
    • LTI conversations with the platform (LMS), leveraging a set of decorators and functions.
    • Store and retrieve newly added quiz questions in a sqlite 3 database.
    • Provide a set of APIs for adding and retrieving quiz questions and answers.
  • The react.js front-end expects React 18 and in this initial exercise, we will be building react into static files in our development environment. While this is not generally the most efficient or best approach, we are building a small application where this is acceptable. For larger projects, alternate approaches using Webpack or a separate development nginx server would be useful.
  • I have also added Tailwind to this project, even though some vanilla CSS would probably do the trick. Why not!

Assumptions:

  • You have a working knowledge of Python 3 and React.js 18.
  • While I will do my best to walk through every line of code, even I do not have a full understanding of the libraries in use. Where I don’t have a ton of information, I will do my best provide links to sites with the most up-to-date stuff.
  • You are working on a Linux / MacOS – based system. Most of the commands are transferable if you are using GitBash on Windows… or a Linux subsystem.
  • Much of this code was inspired by Dmitry Viskov’s Flask Example repository. The code implements a fun breakout game written in plain JS that pushes a score and time back to the LMS. It also supports some deep linking for setting a difficulty. I may introduce deep linking at a later time (possibly for giving a teacher the ability to create new quiz questions) but that particular LTI service is not part of this tutorial.

Alright, let’s get started!

Make sure you are in an empty project folder, name of your choosing. Here are the commands you’ll want to use for getting your environment set up:

python3 -m venv quizenv
source quizenv/bin/activate

Add the dependencies to your project. Create a requirements.txt file in your project directory and add the following text. We won’t be using all of these in the first couple of parts but will eventually need them:

antiorm==1.2.1
blinker==1.7.0
cachelib==0.9.0
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
cryptography==41.0.5
db==0.1.1
db-sqlite3==0.0.1
Deprecated==1.2.14
Flask==3.0.0
Flask-Caching==2.1.0
Flask-Cors==4.0.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
jwcrypto==1.5.0
MarkupSafe==2.1.3
pycparser==2.21
PyJWT==2.8.0
PyLTI1p3==2.0.0
requests==2.31.0
typing_extensions==4.8.0
urllib3==2.0.7
Werkzeug==3.0.1
wrapt==1.16.0

In the same directory, load up the required dependencies

pip install -r requirements.txt

In your project directory, create an app.py by using the ‘touch’ command or creating the file directly in your code editor. My preferred is VSCode. Place the following code at the top of the new app.py file:

import os, pprint, datetime
from tempfile import mkdtemp

# 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

from flask import Flask, request, jsonify, render_template, url_for
from flask_cors import CORS
from flask_caching import Cache

from werkzeug.exceptions import Forbidden
from werkzeug.utils import redirect
from pylti1p3.contrib.flask import FlaskOIDCLogin, FlaskMessageLaunch, FlaskRequest, FlaskCacheDataStorage
from pylti1p3.deep_link_resource import DeepLinkResource
from pylti1p3.grade import Grade
from pylti1p3.lineitem import LineItem
from pylti1p3.tool_config import ToolConfJsonFile
from pylti1p3.registration import Registration

class ReverseProxied:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        scheme = environ.get('HTTP_X_FORWARDED_PROTO')
        if scheme:
            environ['wsgi.url_scheme'] = scheme
        return self.app(environ, start_response)

app = Flask(__name__,
            static_url_path='', 
            static_folder='./frontend/static',
            template_folder='./frontend/templates')

CORS(app, resources={r"/*": {"origins": "*"}})

app.wsgi_app = ReverseProxied(app.wsgi_app)

config = {
    "DEBUG": True,
    "ENV": "development",
    "CACHE_TYPE": "simple",
    "CACHE_DEFAULT_TIMEOUT": 600,
    "SECRET_KEY": "3dj90jdwi0d320edj9d",
    "SESSION_TYPE": "filesystem",
    "SESSION_FILE_DIR": mkdtemp(),
    "SESSION_COOKIE_NAME": "pylti1p3-flask-app-sessionid",
    "SESSION_COOKIE_HTTPONLY": True,
    "SESSION_COOKIE_SECURE": False,   # should be True in case of HTTPS usage (production)
    "SESSION_COOKIE_SAMESITE": None,  # should be 'None' in case of HTTPS usage (production)
    "DEBUG_TB_INTERCEPT_REDIRECTS": False
}
app.config.from_mapping(config)
cache = Cache(app)
  • Line 1 – importing some common Python modules. pprint is mostly used for debug.
  • Line 2 – tempfile is used for a temporary storage location. Specifically, a call to mkdtemp will be used to define a session file location. More info.
  • Lines 4-5 – commenting these out for now. We’ll build these custom dependencies / modules out later.
  • Line 7 – Import common Flask libraries for working with APIs and rendering pages.
  • Line 8 – CORS support for working with external domains / separate ports. This may not be necessary for our current approach, since we’re only running a single server in development, with React builds… but I like to make sure all bases are covered for all scenarios.
  • Line 9 – import the caching libraries needed for our backend. This will get configured later when our app is initialized.
  • Lines 11-12 – WSGI utilities for handling exceptions and redirects.
  • Lines 13-18 – PyLTI1p3 imports. More information can be found here: https://pypi.org/project/PyLTI1p3/
  • Lines 20-28 – A class that’s designed as a safeguard against URL munging when a web client is serving up static files (JS / CSS), when Flask generates HTML content.
  • Lines 30-33 – Create an instance of the imported Flask class.
  • Line 35 – CORS is used here for specifying origins that are allowed to access our future API endpoints.
  • Line 37 – Applying our reverse proxy from Line 20 to our wsgi application.
  • Ines 39 – 54 – Configure Flask for session and cache handling. You’ll need to generate your own “SECRET_KEY”. I just mashed some keys on this one. In production, this is used to keep client/server sessions secure. Feel free to paste in the output from something like this — run it in the REPL:
>>> import os
>>> os.urandom(24)

Now, let’s define some Python functions in app.py, used to further configure our LTI application:

def get_lti_config_path():
    return os.path.join(app.root_path, 'configs', 'reactquiz.json')

def get_launch_data_storage():
    return FlaskCacheDataStorage(cache)

def get_jwk_from_public_key(key_name):
    key_path = os.path.join(app.root_path, '..', 'configs', key_name)
    f = open(key_path, 'r')
    key_content = f.read()
    jwk = Registration.get_jwk(key_content)
    f.close()
    return jwk
  • lines 59-60: A method for accessing the Flask cache.
  • lines 62-68: A method that can be used for serving up the public jwks key for our tool.

At the bottom of your app.py, add the following lines of code (with some additional debug comments in case they are needed for future troubleshooting):

if __name__ == "__main__":
    #app.debug = True
    #app.run(debug=True)
    app.run()

As far as the rest of the initial configuration – make sure other config files that are necessary (at least a public and private key) are available in the <application root>/configs directory (see my screenshot below). More information on these files can be found in my other posts and in the pyLTI1p3 documentation:
LTI Moodle Integration using PyLTI 1.3
Adhoc Platforms for LTI Tool Integrations Testing
pyLTI1p3 – LTI 1.3 Advantage Tool

In part 2, we will finish up the app.py by adding some LTI-related routes that will eventually be fetched by our React Quiz front-end.

Revised on 5/25/24

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

Update: 11/20/2023: Please use the newer PART 1 of this series here:
Build a Simple LMS-Integrated Quiz App Using Python, React.js, and LTI 1.3 – Part 1
The code below references an older Python code that’s not a part of pyLTI1p3.

Update: 5/3/2023: In the process of creating part 2 of this series, I discovered a lot in my research (like, maybe this isn’t the right path for a simple solution) and will be expanding by finding a more viable and maintainable option. Several folks have reached out to me about this article, only to discover that they could use a simpler approach. I began asking questions like: 1) Why use React.js when I could use Flask’s templating engine? Perhaps because React is more common than something like Jinja? 2) Why not use a framework that is built for this purpose, such as LTIjs? More to share on this topic at a future date as I continue to explore the latest frameworks like Vue.js 3 and using Django instead of Flask from the outset. I love it that this is already developing into a tiny community, helping to give this site some direction… and if you feel strongly about the direction I’m taking with this post / series, please click “leave a comment” in the title sidebar.


The project idea is all in the title… Let’s plan out what is involved and what the result might look like.

This is just a high-level overview of a Quiz application built with Python Flask and React.js Javascript library. We’ll have multiple choice as our only option — again just keeping the development effort somewhat simple. The actual implementation will depend on the requirements below and the LMS that we are integrating with BUT I want to make this as agnostic as possible, where the LMS really shouldn’t matter as long as the platform we’re integrating with is LTI1.3/Advantage certified.

Dall-E generated: a surrealist painting of a cyberpunk, sitting at a desk in a desert at night

1. Flask Backend

  • Create a Flask application that implements LTI 1.3 by using an LTI library. I think we have a good idea of what that will be: PyLTI1p3.
  • Create a RESTful API for the quiz application that allows React frontend to retrieve questions and submit answers.
  • The backend should also be able to store the scores of the students and their grading status. Perhaps we’ll just use SQLite, although I believe this isn’t the best choice. The intention here will be to migrate to PostgreSQL but SQLite is good enough for establishing the initial database schema.

2. React Front-end

  • Create a React application that fetches questions from the Flask backend and displays them to the user.
  • Allow users to submit answers to the questions and provide feedback on their responses.
  • The frontend should also be able to retrieve the scores and grading status of the students.
  • If this were Django, and since it’s not a complex build, I would likely skip this part and implement the front-end part of the stack through it’s native templating engine. My favorite options are Vue.js and React.js because of their flexibility and they are fine to use with Django.

3. Integration with the LMS

  • Implement the LTI 1.3 protocol in the Flask backend to communicate with the LMS.
  • Pass the scores of the students back to the LMS and create a gradebook column in the LMS.
  • Post the scores along with grading status to the LMS.

Preview of Step 2 (which I will post soon) — Using PyLTI1p3 with Flask might look something like this. Let’s explore further in Part 2, where we’ll refine this and test it out.

from flask import Flask, request
from pylti1p3.flask import lti

app = Flask(__name__)

@app.route('/api/questions', methods=['GET'])
@lti(request='launch', role='instructor')
def get_questions():
    # Retrieve questions from the database
    # ...

    return json.dumps(questions)

@app.route('/api/scores', methods=['POST'])
@lti(request='launch', role='student')
def submit_score():
    score = request.form['score']
    # Store the score in the database
    # ...

    # Pass the score back to the LMS and create a gradebook column
    request.lti_outcome_service().post_replace_result(score)
    return 'Score submitted successfully'

if __name__ == '__main__':
    app.run()

This code uses the @lti decorator from pylti1p3 to implement the LTI 1.3 protocol and check if the request is valid and if the user has the correct role.

The get_questions function retrieves the questions from the database and returns them to the frontend.

The submit_score function stores the score in the database and passes it back to the LMS using the post_replace_result method from the LTI outcome service.

This is just sample code and we will need to make many modifications in order to fit the specific requirements of our project.

Thank you and have a great day!

LTI Moodle Integration using pyLTI 1.3 (Python/Flask Game Example)

Dall-E generated: “sunny surrealist painting of a video game integrated into a python’s brain”

Definitions and References for this post

  • IMS Global / 1EdTech – An excellent organization that continues to put forth great standards and frameworks for EdTech integrations.
  • LTI = Learning Tools Interoperability: A set of standards and services, primarily used for the proper integration of platforms (Learning Management Systems) and third-party tool providers, typically servicing Higher Ed and K12.
  • dmitry-viskov’s pylti1.3 Github repo – a Python implementation of the 1EdTech PHP tool. We’re going to use the example game provided by the developer.
  • URLs used for the local tool configuration. Note that you can configure these how you want, so they could vary. The default URLs are:
    • OIDC Login URL: http://127.0.0.1:9001/login/
    • LTI Launch URL: http://127.0.0.1:9001/launch/
    • JWKS URL: http://127.0.0.1:9001/jwks/ (if you choose to implement a public keyset URL).
  • Moodle LTI 1.3 Support page: https://docs.moodle.org/dev/LTI_1.3_support
  • Moodle 4 Doc for Publishing an LTI Tool and an excellent primer for a better understanding of LTI!

This post builds off of my previous post where I used the saLTIre platform emulator to test out the example breakout game using the pyLTI1.3 Flask test implementation. The game does some score pass-back and all seems to work well with Moodle. I’ll walk through all the steps!

My Test Environment

When I was working for Kaltura as a Solutions Engineer, I set up this Moodle 4 instance for demos. This particular subdomain falls within a shared hosting environment in my reseller Hostgator account, so there were some initial issues with being able to write back to Moodle via a POST method (from the locally hosted Flask LTI Game). Their support was able to make some Apache Mod Security exceptions for me without screwing with needed security… and we got things going. For those having to do their own troubleshooting in this matter — and if you’re using a cheap-o shared / test environment like me, here’s a snippet from the chat transcript and what Hostgator support had to do for me:

[*] URLS Affected: /mod/lti/services.php/4/lineitems
[*] Description: Atomicorp.com WAF Rules: Request content type is not allowed by policy
I am trying to check and whitelist the Mod Security from my end
[*] Whitelisting rule 391213 for moodle.videotesting.org
[*] Testing apache configuration…
[*] Configuration good! Restarting Apache…DONE!

More about tuning Mod_Security for your needs can be found here.

Step 1 – Local App Configuration

As far as the local configuration, I updated the existing Moodle configuration example in /configs/game.json with my Moodle parameters:

  • You’ll find the needed URLs in your Moodle integration setup (in the next step).
  • The client_id and deployment_ids parameters needed here are generated by Moodle once the configuration is saved.
  • As you can see, I’m still using the same private.key file from my previous post about implementing with saLTIre. You’ll copy the contents of that file for use in the next step.

Step 2 – Moodle 4 configuration

  • I implemented the LTI globally (not at the course level): Site Administration -> Plug-ins -> Activity Modules ->External Tool -> Manage Tools -> Configure a Tool Manually
  • Tool URL: We’re using the localhost address and port configured in the Python Flask game.
  • LTI Version: LTI 1.3
  • Client ID: Retrieved once saved.
  • Public key type: RSA key. If you want to give the keyset URL option a try, you’ll use the JWKS URL from the resource section and the top of my post.
  • Public Key: Paste in from the public.key file in Step 1
  • Login URL and Redirection URI: Use the localhost’s configuration.
  • Services and Privacy settings can be configured based on institutional preferences but we want to make sure that grade sync / column management and student info passback is enabled if you want to see the student name and get the full exchange of information with the LTI game.
  • Deep Linking options should be enabled.
  • Under Miscellaneous, I made sure that my tool appeared in all courses for my entire site, selecting Site Hostname.
Click to Enlarge
Click to enlarge
Click to Enlarge

Once the changes are saved, you can grab what you need by going back to Site Administration -> Plug-ins -> Activity Modules ->External Tool -> Manage Tools and selecting the “View Configuration Details” button on the card for the Flask Game tool that was just created. From there you can grab the Client ID, URLs, and Deployment ID info needed by the too configuration in Step 1.

After all has been saved. Let’s test with a student in a course!

As the instructor, if I have a look at the gradebook in my course, I can see Morty’s scores. Granted, I probably need to change the range based on the difficulty of the game, etc.

Ad-hoc platforms for LTI tool integrations testing

Definitions and References for this post

The search for up-to-date tool provider examples written in Python

In attempting to learn more about how I could leverage Python Flask or Django in developing a new LTI tool provider, I discovered some excellent (and up-to-date, as of this post) examples that I could use to build something locally for testing, and that I’m able to run fairly easily. This search wasn’t super-easy since there isn’t a lot that takes into account the newer standards with LTI 1.3 / Advantage, and isn’t dated like MITs pyLTI with support only for LTI 1.1 and/or 2.0 standards.

Dall-E generated: “oil painting of three dimensional bridge connecting two computers with a night sky background”

Finding a platform for testing an LTI

With the newer GitHub example project, I was able to build a local development environment, start up a Flask server, and serve up a simple tool that could be tested. BUT, I need a platform to do the proper testing! Launching endpoints will just error out until I have the context in place, within a course, in an LMS… or at least a simulated version of an LMS. That’s where saLTIre came in handy for this purpose. Let’s a take a look at how to properly set up this Flask example by making some changes to the tool provider’s configuration in order to get it to work properly with saLTIre platform emulator.

Setting up the Flask app for use with saLTIre

Step 1 – Set up your development environment and clone the repo

If you know how to deploy a Docker container to your desktop, that’s probably the easier method, outlined here: https://github.com/dmitry-viskov/pylti1.3-flask-example. I personally prefer getting things set up locally in a Python virtual environment, so that’s the option I used.

$ cd ~/dev
$ git clone git@github.com:dmitry-viskov/pylti1.3-flask-example.git
$ python -m venv ~/dev/python_envs/flask_lti13
$ source ~/dev/python_envs/flask_lti13/Scripts/activate
$ cd ~/dev/plylti1.3-flask-example
$ pip install -r requirements.txt
$ cd game
$ python app.py

The above is copied from the bottom of the GitHub repo’s README for this project, with a couple of minor changes for my personal environment. I’m using Git-Bash for Windows for this one and I like to store all my Python virtual environments in one directory, embedded in my development environment. I already have a dev directory in my home directory that contains all my various tests and projects.

Really, it’s up to you!

Please refer to Real Python’s primer for doing this properly in your choice of virtual environment.

Running python app.py will run the server… and if there are no errors to address, go ahead and shut it down before completing the next steps (Ctrl+C).

Step 2 – Add a new configuration that works with saLTIre

One thing you’ll notice with this project is that it includes several configurations in /configs/game.json for major Learning Management Systems like Canvas, Blackboard, Moodle, and D2L. It also includes additional client configurations for 1EdTech’s platform emulator / reference implementation and validator.

Since there wasn’t one for saLTIre, I added the following to the bottom of the file. It looks something like this:

"https://saltire.lti.app/platform": [{
        "default": true,
        "client_id": "saltire.lti.app",
        "auth_login_url": "https://saltire.lti.app/platform/auth",
        "auth_token_url": "https://saltire.lti.app/platform/token/1026212c6ba048ba418392ef4c51013d",
        "key_set_url": "https://saltire.lti.app/platform/jwks/1026212c6ba048ba418392ef4c51013d",
        "key_set": null,
        "private_key_file": "private.key",
        "public_key_file": "public.key",
        "deployment_ids": ["cLWwj9cbmkSrCNsckEFBmA"]
}]

Where did I get most of these parameters from? I’ll show you in the next step. We’ll get them from the saLTIre platform emulator. Go ahead and save the new platform config to the game.json file. You can adjust anything you need in here once the next steps are complete.

Setting Up saLTIre

Step 1 – Create an account

Go to https://saltire.lti.app/platform and create an account. This may not be necessary but it allows you to save some of your tool-level configurations for subsequent use. You must have a Google account in order to sign in.

Click the Options dropdown -> Sign in

Step 2 – Configure the Security Model

Select Security Model and configure the following:

  • LTI version -> 1.3
  • Message URL -> http://127.0.0.1:9001/launch/
    • Use the tool’s launch URL for this. If you followed the Flask app setup above (along with the github readme, you should be working with localhost and TCP port 9001.
  • Ensure that the signature method is set to -> RS256 (JWT Authorization).

Step 3 – Enter the LTI Tool Details

Whilst you are still on the Security Model page:

  • Initiate login URL -> http://127.0.0.1:9001/login/
  • Redirection URI(s) -> http://127.0.0.1:9001/launch/
  • Public key -> This (full text) needs to be copied from the key included with your Flask tool implementation. You can grab that from here (assuming your paths are like mine): ~/dev/plylti1.3-flask-example/configs/public.key. Essentially, the public key that you’ll want is inside your projects root directory -> configs, where we also found the game.json file.

After this is complete, you can register the tool, make it public and save your config. This is purely up to you but is great for future reference.

Click Save at the top of the screen, next to the saLTIre logo.

Step 4 – Confirm/configure platform details in the tool configuration

Yes, you are still on the Security Model page…

You’ll want to ensure that the /config/game.json contains the following platform details for the saltire platform configuration.

  • Client ID -> client_id
  • Deployment ID -> deployment_ids
  • Application Request URL -> auth_login_url
  • Access Token service URL -> auth_token_url
  • Public keyset URL -> key_set_url

Save the changes to the game.json file.

Step 5 – Configure the Message Type in saLTIre

Select the Message from the left navigation. At this point you will have different results based on the type of app you are using. With the game that we’re trying to launch, either deep linking or resource link requests will work, but each with different results. You will want to use the Deep Linking request in order to complete a full test and get back a response.

In this example, I’m selecting LtiDeepLinkingRequest

Once Deep Linking is selected, you’ll be presented with more options. Feel free to play around with this once things are up and running to gauge what the various results will be based on certain selections.

  • Beyond the defaults, at least LtiResourceLink needs to be added to the list of accepted types.
  • Don’t forget to click SAVE at the top of the page.

Launch the App locally and connect!

Launch the app.py in your <app root>/game directory. Don’t forget to enable the virtual environment. Should look something like this (I’m using git-bash on Windows):

source ~/dev/python_envs/flask_lti13/Scripts/activate
cd ~/dev/flask_projects/pylti1.3-flask-example/game
python app.py

If all is well and without errors, try launching into the LTI tool from the platform by clicking on Connect at the top right corner of the screen. You can also select some options from the Connect drop-down for various launch conditions. One excellent way to debug the connection payload is to use the Preview connection option — one of my favorite items.

Once Launched the tool should present a difficulty selection…

… and with the deep linking option in saLTIre, you won’t get an opportunity to play the game but you’ll return to the platform with a whole lot of info and a verification status. You should see a green Passed if all went well.

You are able to play the game if you change the message type from LtiDeepLinkingRequest to LtiResourceLinkRequest. You can also see the name of the student in the game.

A score is likely being passed back… but that’s where I wasn’t able to make it ALL happen inside of saLTIre. I can certainly get us to an end, but would love to see this workflow work properly.

Launch and Authenticate -> Select Difficulty -> Play game to completion and close -> Score is passed back to the “gradebook” and a column created.

I’m hoping it’s just something I missed in the config but would be happy to hear what you think and if anyone out there can help me craft a solution. This is a great example of a working app inside of an LMS platform with score pass-back as a potential option.

Thanks! I’m looking forward to your feedback. Follow-ups and upcoming posts will include:

  • An explanation of what all this is and an exploration of authentication in LTI 1.3.
  • Trying out the Django version of this example in saLTIre.
  • More closely checking out the tool inside the 1EdTech platform emulator. I attempted this option prior to using saLTIre and was disappointed in the results and received several errors. Perhaps saLTIre is just forgiving? Not sure.
  • Building an LTI app using Python and potentially exploring lowcode / nocode tools for implementing an LTI. What are the security challenges(?), etc.

Welcome to my site!

Greetings, everyone! Greetings, EdTech Developers, Vendors, Admins, Staff, Instructors, and Students.

I’m super-excited to be starting one of many new endeavors — a blog about development for Educational Technology and Integrations. Here you will find a resource for everything Learning Tools Interoperability. I’ve worked in the EdTech industry for many years — for vendors and institutions — in a variety of roles; developer, admin, engineer.

I’ve learned along the way that LTI can be a great thing for institutions (colleges, universities, and corporations) wanting to integrate third party solutions into their LMS(s) that do a thing better than the core LMS itself, perhaps acting as a complementary tool if built to the right specifications. Tool providers can be simple (attendance tool) or complex (portfolio application integrations) and some of those solutions can adhere to LTI standards very well, but I would say that many do it very poorly — perhaps quick development of an integration helps to provide another checkbox item for a marketing campaign, if you will…?

“We Integrate with ALL the major LMSs”… followed by a graphic with dated logos that looks something like this”:

We’re tired of used to seeing this LMS rundown. Right? 🙂

Anyway, the goals of this site at the outset:

  • Provide a place for LMS / EdTech Admins and Tool Providers to connect
  • LTI 1.3 / Advantage standards are relatively new to developers. We need a place to discuss ideas and workflows further, beyond the awesome documentation provided on the IMS Global / 1EdTech site.
  • Provide a place for tutorials and simplification for implementing LTI using a variety of different development platforms.
  • Not explaining LTI! Don’t expect this site to explain what LTI is or write documentation around the standards. You’ll have to go to the source for that: https://www.imsglobal.org/activity/learning-tools-interoperability
  • Having a look at tool provider documentation and complementing or helping to make improvements.

Just getting started! More to come…