Sending LTI Outcomes to Google Classroom

In “Using LTI Tools in Google Classroom” I showed how to use Google’s Classroom share button to assign a link that ultimately launches an LTI Tool. For example,

https://hostname/gc2lti/{nonce}?url={lti tool}

Where gc2lti is an instance of a Gc2LtiController, {nonce} is a unique random number to differentiate between multiple assignments of the same LTI Tool, and {lti tool} is the URL of the actual LTI Tool. Gc2LtiController receives the launch request from Google Classroom (a very plain GET request), uses Google APIs to create a well-formed LTI Basic Launch Request, and POSTs it to the LTI Tool. However, that LTI request did not support outcomes. So, there was no way for the LTI Tool to send outcomes back to Google Classroom.

In this blog post I show how to add support for LTI 1.x outcomes.

I used Visual Studio Community 2017 and .NET Core 2.0 for everything in this POC, and I’ve put the entire solution (both versions) up in github. There is also a running version of this version (with outcomes) on Azure.

Google Classroom Requirements

Google has two requirements for sending grades back to Google Classroom via the Google Classroom API:

  1. Tools (or apps…I use the terms interchangeably) can only assign grades to assignments created using the same Web Client ID. Tools cannot assign grades to assignments the teacher manually created, or to assignments created by other apps, including the Google share button. That meant creating a replacement for the Classroom share button that uses my Web Client ID.
  2. Only the teacher can assign a grade to an assignment. If the Tool was launched by a student, then the Tool must impersonate a teacher when it comes time to send the grade back to Google Classroom. And not just any teacher, the Tool must impersonate the teacher that created the assignment. That means capturing the teacher’s OAuth token when they create the assignment.

Once those two requirements are dealt with, assigning the grade is simple. Let’s get started.

Custom Classroom Share Button

My version of a custom Classroom share button is in the HomeController with associated razor views. The default page (/Index) does not require authentication. The Index action displays a sample “catalog” page with the custom share button. When you click on the button, it opens a new window and starts the sharing process with the Course action,

<a href="Share" title="Share to Classroom"
   onclick="share('@Model.Url', '@Model.Title', '@Model.Description');return false;">
    <img src="images/32x32_yellow_stroke_icon@1x.png"/>
    Share to Classroom
</a>
<script type="text/javascript">
    function share(shareUrl, title, description) {
        window.open(`Home/Course?url=${encodeURI(shareUrl)}&title=${title}&description=${description}`,
            "_blank",
            "toolbar=no,width=640,height=400,left=100,top=100");
        return false;
    }
</script>

The Course action does require Google authorization and this is when the teacher’s authorization token is captured for later use when saving the grade in Google Classroom.

var result = await new AuthorizationCodeMvcApp(thisnew AppFlowTeacherMetadata(ClientId, ClientSecret, Db))
    .AuthorizeAsync(cancellationToken)
    .ConfigureAwait(false);

if (!await SaveOfflineToken(cancellationToken, classroomService, result))
{
    return RedirectToAction("Course", model);
}

Normally the AuthResult returned by AuthorizationCodeMvcApp has an AccessToken that expires in 1 hour, but no RefreshToken. This is sometimes called the “automatic” AccessToken. But I need an AuthResult that includes a RefreshToken so that the grade can be recorded hours or days later.

AuthorizationCodeMvcApp will return an AuthResult with a RefreshToken if the user explicitly logs in. This is sometimes called the “offline” AccessToken. SaveOfflineToken checks to see if the AuthResult has a RefreshToken. If not, the user’s credential is revoked and the user is redirected back to the Course action. This time through, Google will require the user to explicitly login. Once I have an AuthResult with a RefreshToken, I save a reference to the corresponding UserId so I can retrieve it later.

Once the AuthResult with an “offline” token is recorded, the user (teacher) picks the Google Course which will get the assignment, and then is sent to the Confirm action. Confirm asks the teacher to confirm the title, instructions, LTI Tool URL, and the maximum number of points possible for this assignment.

If an assignment does not have MaxPoints, Google will not display the grade to the either the teacher or the student.

When the user clicks on Assign, the Assign action is invoked which creates the Google CourseWork in the Course.

When the CourseWork is created, a Google server will GET the link URL to grab a screenshot and link title. Even if the link title is specified in the API call, Google will overwrite it with the title of the web page at https://hostname/gc2lti. For that reason, Gc2LtiController tries to identify these probing GETs and return a nice looking thumbnail with a page title that matches the assignment title.

Converting a Launch from Google Classroom into an LTI Basic Launch Request

In the previous post I showed how to use Google’s Classroom share button to insert a specially formatted link for the LTI Tool so that a simple GET request (which is all you can count on from Google) can be turned into an LTI Basic Launch Request. This version of the POC works the same way…the custom Classroom share button inserts a similar special link so that the Gc2LtiController can intercept the GET request, attach all the required LTI parameters, and POST it to the LTI Tool.

There is one big enhancement in this version of Gc2LtiController versus the previous version. This version of Gc2LtiController fills in LisResultSourcedId and LisOutcomeServiceUrl so the Tool knows to send outcomes.

LisOutcomeServiceUrl points to a new OutcomesController. And LisResultSourcedId includes the Google CourseId, CourseWorkId, StudentId, and TeacherId,

var lisResultSourcedId = new LisResultSourcedId
{
    CourseId = ltiRequest.ContextId,
    CourseWorkId = ltiRequest.ResourceLinkId,
    StudentId = ltiRequest.UserId,
    TeacherId = courseWork.CreatorUserId
};
ltiRequest.LisResultSourcedId =
    JsonConvert.SerializeObject(lisResultSourcedId, Formatting.None);

Converting an LTI Outcomes Request into a Google Classroom Grade

So now the LTI Tool has been launched and it is time to send a grade back to the assignment in Google Classroom.

OutcomesController receives the LTI Outcomes request from the Tool, parses LisResultSourcedId into CourseId, CourseWorkId, StudentId, and TeacherId; and then calls the Google Classroom API to set the grade for the assignment.

Google Classroom grades are part of student CourseWork Submissions. There are two types of grades: assignedGrade and draftGrade. Only teachers see draftGrade. It is primarily for situations where the teacher must evaluate the submission before assigning a final grade (assignedGrade). Students see assignedGrade. OutcomesController reads and writes the assignedGrade.

The Google Classroom API supports reading (get) and writing (patch) grades, but not support deleting deleting them. OutcomesController handles all 3 LTI Outcomes requests (ReadResult, ReplaceResult, and DeleteResult), but returns Not Implemented when the request is to DeleteResult.

When the request is to ReplaceResult, OutcomesController performs these steps:

  1. Authenticate the LTI Request.
  2. Lookup the “offline” TokenResponse for the TeacherId.
  3. Using TokenResponse, create a valid AccessToken.
  4. Using the AccessToken, create an instance of the ClassroomService.
  5. Using the ClassroomService, get the CourseWork.
  6. Then get the student’s StudentSubmission.
  7. Set the AssignedGrade = {LTI Result} * CourseWork.MaxPoints.
  8. And finally patch the StudentSubmission with the new AssignedGrade.

LTI Results are always between 0.0 and 1.0. So, if the LTI Result is 0.51 and the CourseWork.MaxPoints are 100, then AssignedGrade = 51. Students will see 51/100.

When the request is to ReadResult, OutcomesController performs similar steps, then returns {LTI Result} = StudentSubmission.AssignedGrade / CourseWork.MaxPoints.

Posted in Google, LTI | Tagged , , , , , | Leave a comment

Using LTI Tools in Google Classroom

According to the the EdNet Insight report, Educational Technology Trends: State of the K-12 Market 2016”, 67% of US school districts are using Google Classroom as an LMS. Unfortunately, Google does not yet support the IMS LTI standards for interoperability. In this post I walk through a proof of concept (POC) that converts a request coming from Google Classroom into an LTI request to a Tool.

Here’s an overview:

  1. Use a Classroom share button to add a specially formatted link into the course. The link points to a .NET Controller (Gc2LtiController) that will create the LTI request and post it to the LTI Tool.
  2. When the Gc2LtiController  receives the get request from Google Classroom, it uses several Google APIs to get the data needed to form the LTI request.
  3. The Gc2LtiController also needs to determine which key and secret to use to sign the request. I imagine this can be done using the information collected above, but since this is all happening in a browser session, you could also prompt the user for a code or another clue to find the right key and secret.
  4. The Gc2LtiController then signs the LTI request and posts it to the Tool.

 

I used Visual Studio Community 2017 and .NET Core 2.0 for everything in this POC, and I’ve put the entire solution up in github. This post walks through some parts of the POC and explains how they work.

Share to Classroom

The Classroom share button needs to insert a link to the Gc2LtiController and pass the the URL of the actual LTI Tool. Something like this:

https://localhost:44319/gc2lti?url=https://lti.tools/test/tp.php

Google’s share button will create a Coursework resource with this link. The Gc2LtiController will use the request URL to look up the matching Coursework resource. If this link has been assigned more than once to the same course, then Gc2LtiController will use the most recent.

That is not a problem if your Tool does not treat each assignment uniquely. For example, if your Tool is a game without bookmarking or scores, then it does not matter which Coursework resource it was launched from.

But if your Tool needs to differentiate between multiple assignments of the same Tool (for example, if your Tool uses the LTI resource_link_id then it probably does)  then the link URL will need something to differentiate each assignment. I couldn’t find a way to modify the link URL with each click, but it was pretty easy to add a nonce to the link URL when the Classroom share button is rendered so it looks like this:

https://localhost:44319/gc2lti/9d362ba3feac48ffaa0cd1f1e2cd6e1c?url=http://lti.tools/test/tp.php

The data-url attribute of the Classroom share button gets the URL from the ShareUrl property of the PageModel,
classroom-share-button

The ShareUrl property is calculated when the page is rendered. Skip the nonce if you don’t need it, and use a URL to your actual LTI tool. You can use http://lti.tools/test/tp.php. It is a very handle Tool for testing that validates the LTI request and dumps the parameters.
resource-code

When you click on the Classroom share button and follow the prompts, you will end up with something like this,
poc-lti-tool

The preview image is broken because Google’s screen thumbnail capture service can’t access localhost. The Gc2LtiController will recognize when Google is trying to get a thumbnail image and you can supply whatever you want in return.

Now let’s walk through the Gc2LtiController.

Gc2LtiController

The Gc2LtiController (Google Classroom to LTI) will receive a get request when the teacher or a student in the classroom clicks on the Link. Gc2LtiController uses clues in the request (e.g. the Referer header), the Google Classroom API, and the Google Admin Directory API to fill in most of the required and useful parameters of an LTI request.

The only required parameters this POC version of Gc2LtiController cannot determine is the oauth_consumer_key and secret. I’ve hard coded the key and secret for https://lti.tools/test/tp.php. A real implementation will need to determine the key and secret based on the information gathered from Google, or by asking the user to supply a clue such as a code.

I found the Google Classroom API .NET Quickstart and the web application section of the OAuth 2.0 quide useful when writing Gc2LtiController. I ran into one glitch: Google’s quickstarts and .NET API libraries were created before .NET Core 2.0. This was really only an issue with the Google.Apis.Auth.Mvc library. For this POC, I used @buzallen‘s replacement he calls Google.Apis.Auth.AspMvcCore.

Prerequisites

To run this POC, you will need several things:

  • Visual Studio 2017.
  • Access to the internet and a web browser.
  • A Google account (for you as the developer of the project).
  • A second Google account with Google Classroom enabled for testing. A G Suite for Education account is preferred, but this works with other account types with some degradation.
  • Download @buzallen‘s Google.Apis.Auth.Mvc replacement he calls Google.Apis.Auth.AspMvcCore. You will probably need to fix the Google.Apis.Auth.AspMvcCore project reference in the gc2lti-poc solution so that it points where you downloaded the project.
  • Enable the Classroom API and the Admin SDK using the Google Developers Console. See Google’s Classroom Quickstart for details.
  • Create an OAuth Client ID for a web application, also using the Google Developers Console.
  • Add https://localhost:44319/AuthCallback as an authorized Redirect URL to the Client ID.
  • Store the Client ID and Secret for the Gc2LtiController using the Secret Manager:
    1. Right click on the gc2lti project and select Manage User Secrets.
    2. Store your Client ID and Secret in the secrets.json file.
      {
        "Authentication:Google:ClientId": "YOUR CLIENT ID",
        "Authentication:Google:ClientSecret": "YOUR SECRET"
      }
      

You should be able to run both the catalog and gc2lti projects now. The rest of this post walks through some of the interesting bits of Gc2LtiController.

The Gc2LtiController.Index Action

Links to Gc2LtiController looks like one of these:

https://localhost:44319/gc2lti?url=http://lti.tools/test/tp.php
https://localhost:44319/gc2lti/9d362ba3feac48ffaa0cd1f1e2cd6e1c?url=http://lti.tools/test/tp.php

To allow either format, the route for the default (Index) action makes the nonce optional,

And since nonce has no value to Gc2LtiController by itself, there is no matching parameter. The full request URL (including the nonce if provided) is used to find a matching Coursework resource later.

Google calls the default (Index) Action many times for different purposes. The first time is to capture a thumbnail sized screenshot of your LTI Tool. This is called by one of Google’s servers (not through a browser), so it is not a good time to actually launch your tool. I suggest returning a generic page that looks nice.

The next time the Index Action is called, the teacher or student has clicked on the link hoping to launch the Tool. To form an LTI request, Gc2LtiController needs a bunch of information from Google. That means the controller needs authorization.

Google’s authorization flow redirects away and back to the Index Action 2 or 3 times during the authorization flow (3 the first time a user interacts with your copy of Gc2Lti so they can agree to allow you access). The first bit of data is collected we need is collected from these requests: the alternate link for the course,

Once the app is authorized, it’s time to start forming the LTI request,

The first set of information comes from the Google Classroom API,

And the second from the Google Directory API,

At this point the LTI request has all the user, context, and resource information. It’s time look up the key and secret, sign the request, and post it to your LTI Tool,

FillInUserAndPersonInfo

This is very straightforward because Google stores very LTI-like information in the Classroom UserProfile,

FillInContextAndResourceInfo

This one is a little more complex because we need to find the matching Course and Coursework. When the teacher or student clicks on the link and if the scheme is https, then Google will include the Course Alternative Link in the request Referer header. To find the matching course, we need to search the list of this user’s courses, looking for the one with the matching AlternativeLink,

Once we have the context_id (= Google Classroom Course ID), then we can search the Coursework for one with a Link that matching the Request URL. This code will find the first (most recent) Coursework with a matching URL. If the teacher manually created the link or used the Classroom Reuse feature, the matching Coursework might not be the one that was originally assigned with the Classroom share button,

FillInContextRoleInfo

Getting the context role is also pretty straightforward…just check to see if the current user is in the list of course teachers,

FillInPersonSyncInfo

Google has 3 ways for schools to associate SIS information with users:

  1. Google School Directory Sync for G Suite for Education customers.
  2. Google Cloud Directory Sync for G Suite customers.
  3. G Suite Bulk Account Update for G Suite customers.

There is no way to add syncing information to normal Google accounts (i.e. anyone@gmail.com).

All 3 ways store the syncing information in the user’s Google Account as ExternalIds, which we can get to with the Google Directory API. The syncing id is stored in slightly different ways depending on the way it was captured,

What’s Next?

The next thing I’ll look at is how to handle LTI outcomes (and gradebook when that spec is public). I’m thinking the Gc2LtiController will need an outcomes endpoint that receives results from the LTI Tool and forwards them to Google Classroom using the Google Classroom API. I’m a little worried this may not be easy because of this statement in the documentation,

Posted in Google, LTI | Tagged , , | 1 Comment

Splitting off LtiLibrary 1.6 Repository

Soon after releasing LtiLibrary 1.6, I started work on LtiLibrary 2.0 using Visual Studio 2017 which had much better tooling for .NET Core than VS 2015. I decided to also pay off some technical debt that had accrued. The outcome was a new naming library scheme (LtiLibrary.NetCore and LtiLibrary.AspNetCore), a new internal naming scheme (e.g. Lti\v1 instead of Lti1), and more consistent controller APIs. All the while, I kept the 1.6 code, solution, and project files in the same repository.

Recently Microsoft release .NET Core 2.0 and an update to VS 2017 with even better .NET Core tooling. Prior to updating LtiLibrary.NetCore and LtiLibrary.AspNetCore to .NET Core 2.0, I decided to split the two code bases. The LtiLibrary 1.6 codebase is now in its own repository. I don’t plan on actively maintaining this repository.

Posted in Uncategorized | 1 Comment

IMS LTI Outcomes 1.0 Versus 2.0

IMS LTI Outcomes 1.0

First introduced in March 2012 with LTI 1.1, Outcomes-1 has changed little over the years. It finally got its own spec in January 2015, and it’s own name: Learning Tools Interoperability Outcomes Management Service 1.0.

In a nutshell, the Tool Provider can read, write, and delete a decimal score between 0.0 and 1.0 as the result, or outcome, of an LTI launch. The Tool Provider can do this immediately, for example if the tool is a self-scoring game. Or later, for example if the teacher launches the tool later to manually grade an essay submitted in response to the original launch.

IMS LTI Outcomes 2.0

A draft version of Outcomes-2 was introduced in December 2014, but it has not been finalized yet. Outcomes-2 has a similarly long name: Learning Tools Interoperability Outcomes Management Service 2.0.

There are many differences between the specifications (see the table below), but as a product manager, I want to focus on the problems they solve. In this sense, Outcomes-2 is a superset of Outcomes-1: every problem you can solve with Outcomes-1 can also be solved by Outcomes-2. But Outcomes-2 can solve one pervasive problem much better than Outcomes-1.

Comparing Outcomes-1 to Outcomes-2

Outcomes-1 Outcomes-2
LTI compatibility LTI 1.x LTI 1.x and 2.x
Service endpoints One Two
Service methods POST
(data content determines action)
DELETE, GET, POST, and PUT
(method determines action in a RESTful way)
Data format XML JSON-LD
Data schemas A single result A single resultAll the results for a single assignment

A single assignment

All the assignments for a class

Result schema Score earned by the learner between 0.0 and 1.0 Score earned by the learner between 0.0 and 1.0The learner that earned the score

Penalty score to be deducted

Extra credit score to be added

Score constraints to be applied

Total score on the assignment

Resulting text score to be displayed

The result status

A comment associated with this result

Assignment schema Not applicable Gradebook labelThe Tool Provider’s activity that produces the associated results

Score constraints to be applied to all associated results

The associated results

What Problems do Outcomes-1 and Outcomes-2 Solve?

Not Enough Time in the Day

Both Outcomes-1 and Outcomes-2 help solve the most common problem teachers want solved: enough time to adjust their teaching to improve student learning. But manually gathering the information they need to adjust their teaching takes a lot of time…time that they are not teaching. Anything that frees up some of their time is a terrific boon. Tool Providers that automatically send information to the teacher are helping that teacher teach and his or her students learn. Even a simple score tells the teacher which students have completed an assignment and if they were able to complete it successfully.

But self-scoring activities and simple scores are not the right fit for all teaching and learning experiences, and that’s were Outcomes-2 starts to shine.

Demonstrating Competence

Teachers often use a style of teaching that asks students to demonstrate their understanding of a concept or their ability to synthesize multiple concepts. There are many ways of demonstrating competence, many of which don’t involve technology at all (such as giving a speech or performing a play). But technology can improve some existing techniques (turning in a document online versus a physically turning in a piece of paper removes the constraints of time and place), and make other techniques possible, such as producing an interactive timeline to demonstrate cause and effect relationships.

One characteristic shared by all demonstrations of competence is the need for an audience, and in particular the teacher. Someone to demonstrate to, and someone to get feedback from. And this is where Outcomes-2 has the advantage.

An Outcomes-2 result has two properties that enable online demonstrations of competence without wasting the teacher’s time and without the constraining either the student or teacher to time and place: the result status and comment. The result status allows the student to tell the teacher where they are in the process: “I’ve started working on assignment” and later, “I’ve completed the assignment”. The result comment can be used to provide more details such as “I’ve finished part 1 of 3”.

Once the result status is “Completed”, the teacher takes over to review the demonstration of competence. Once that review is complete, the teacher can tell the student the final grade and provide simple feedback with the comment (the Tool Provider should provide a mechanism for the teacher to provide detailed feedback if necessary).

Recommendation

Outcomes-2 won’t do anything if it is not built into the learning systems, tools, and content that schools use. Like LTI 2.x, it looks like we are in a stand-off until schools demand that vendors support Outcomes-2. If you are a school trying to implement any kind of online competency based learning, talk to your vendors about how their solution could improve if they implemented Outcomes-2. If you are a vendor with a cool tool that uses Outcomes-2, talk to your schools about how much better your tools would be if their other vendors also supported Outcomes-2.

Posted in Uncategorized | Tagged , , | 3 Comments

LtiLibrary 1.6

So finally, huh!

LtiLibrary (my .NET library for LTI web applications) is now at version 1.6. The big change is the addition Outcomes 2.0 (Draft). Outcomes 2.0 has been in draft status since December 2014. I hope by including support in LtiLibrary, a few people will take up the charge to implement Outcomes 2.0 in production which will help move the spec into final status.

There are two packages on NuGet

You’ll want both if you want Outcomes 2.0. All the source code is on GitHub

Getting Started with Outcomes-2

There are two ApiControllers in LtiLibrary.AspNet that implement the LineItems and LISResult endpoints.

  • LtiLibrary.AspNet.Outcomes.v2.LineItemsControllerBase
  • LtiLibrary.AspNet.Outcomes.v2.ResultsControllerBase

You will need to create your own controllers in your application that inherit from these to handle the actual data. For example,

image

There are two sample applications in LtiSamples that implement Outcomes-2. SimpleLti is a single page app that hosts both the Tool Consumer and Tool Provider. ConsumerCertification is a simple Tool Consumer that makes all the necessary launch requests to a Tool Provider (yours or someone else’s) to exercise the certification tests.

The biggest advantage I see in Outcomes-2 over Outcomes-1 is that Outcomes-2 supports a comment and a status with each result. In K12, publishers are starting to deliver content catalogs via Thin Common Cartridge where each LTI link points to a single activity such as a lesson or quiz. Let’s say those activities require manual grading. With Outcomes-2, the activity can send a “Completed” result (perhaps with a comment like “Submitted”) to tell the teacher the activity is ready for grading. When the teacher grades the submission, the activity could send a “Final” result (with “Good job!”).

Have fun!

Posted in Uncategorized | Tagged , , , | 8 Comments

LtiLibrary 1.5

LtiLibrary (my .NET library for LTI applications) is now at version 1.5. This coincides with IMS releasing the LTI v1.2 and LTI Outcomes v1.0 Final Specifications, http://www.imsglobal.org/lti/.

LtiLibrary 1.5 also supports Content-Item Message v1.0 Public Draft (3 February 2015) and parts of LTI Outcomes Management v2.0 Public Draft (15 December 2015).

LtiLibrary now comes in two parts: LtiLibrary.Core and LtiLibrary.AspNet. Both are available on NuGet,

The source code and samples are available on GitHub,

And the sample Consumer and Provider applications are running on Azure,

If you have any questions or comments about LtiLibrary, please leave them on GitHub.

Andy

Posted in Uncategorized | Tagged , , , | Leave a comment

Moving to GitHub

Two years ago I wrote that I was trying both CodePlex and GitHub. For awhile I kept both in sync. But that got tiresome so I let GitHub fall out of sync and focussed on CodePlex. That worked really well while it was just me working on LtiLibrary. CodePlex was my source control system and it integrated very well with Visual Studio.

But recently, I have had code suggestions from folks which they had to carefully format in the CodePlex issues forum. It worked, but it must have been very hard to write. Even more recently, some friends at work convinced me to use Git instead of TFS. That would allow contributors to submit pull requests that I could easily review and merge into master.

So first I converted the LtiSamples project in CodePlex into a Git repository. No problem. Then I wanted to split LtiSamples into two related repositories. Not so simple. I think I would have to create a new project in CodePlex with a new project site (i.e. separate Issues, Documentation, etc). What I really wanted was one “project” with two repositories.

Well that is exactly what GitHub does: LtiLibrary, LtiSamples

image

So now I’m using Git and the repositories are on GitHub. Wish me luck.

Posted in Uncategorized | Tagged , , , , , | 2 Comments