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”:
Anyway, the goals of this site at the outset:
Provide a place for LMS / EdTech Admins and Tool Providers to connect
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.
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.
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:
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.
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:
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:
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:
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
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:
render_template('index.html', **tpl_kwargs)
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.
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:
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.
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:
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:
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.
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:
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:
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.
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:
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.
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.
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.
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.
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:
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:
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:
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 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:
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()
I’ve been working with some higher ed institutions on building some quick integrations leveraging LMS and Kaltura APIs. While doing some preliminary testing I decided to put together a little Python script for applying privacy contexts to an app token.
This will allow for restricting access to specific categories by third party and internal integrations testing.
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.
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.
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.
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.
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.
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.
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.
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:
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.
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).
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.
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.