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.

This entry was posted in Google, LTI and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s