Tools

Category : hku


While teaching and supervising are what working at the HKU is all about, you simply cannot escape the administration that comes with it. Luckily, as a developer, I can build tools to automate some of the work.

Canvas

At the HKU, we use a Learning Management System (LMS) called Canvas. It’s a platform where we can create assignments, manage and grade submissions, share resources, etc. Luckily for me, Canvas has a very nice REST API, that offers all the same functionality as the normal website, and more! Some examples:

Grade Calculator

Our first-year Processing course has a slightly complicated way of calculating the final grade. There’s small practice assignments every week, and there’s a final test at the end. To get to the final grade, we take the average of all the assignments, and average that with the final test. To pass, the final grade needs to be at least a 5.5 or higher, and the assignment average ànd the grade for the test needs to be a 5.0 or higher. This way, you cannot easily compensate one with the other, while still allowing sòme leeway.

To calculate this final grade, I’ve written a small tool that uses the Canvas API and gives me all the results.

using var client = new HttpClient { BaseAddress = new Uri("https://hku.instructure.com") };
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", access_token);

var students = await client.GetFromJsonAsync<List<Student>>($"api/v1/courses/{course_id}/users?enrollment_type[]=student");

var sb = new StringBuilder();

foreach(var student in students)
{
  var assignmentGrades = new List<float>();

  var submissions = await client.GetFromJsonAsync<List<Submission>>($"api/v1/courses/{course_id}/students/submissions?student_ids[]={student.id}&per_page=100&include[]=assignment");
  foreach (var submission in submissions.Where(s => s.assignment.name.Contains("Practicum Opdracht")))
  {
    assignmentGrades.Add(submission.score ?? .0f);
  }

  float testGrade = submissions.Where(s => s.assignment.name.StartsWith("Toets ")).Max(s => s.score) ?? .0f;

  string line = $"{student.name} practica: {assignmentGrades.Average()}, toets: {testGrade}, final: {CalculateFinalGrade(assignmentGrades.Average(), testGrade)}";

  sb.AppendLine(line);

  Console.WriteLine(line);
}

Assignment Shuffler

For this same Processing course, we let the students make artwork for each other’s game. In the first class, their assignment is to create sketches and determine how large their images are going to be. These specifications will then be sent to a random classmate. They will then need to implement placeholders using rectangles according to the sizes they’ve received. A few weeks later, they will create the actual artwork, which will be sent to that same classmate. If they’ve done everything correctly, they’ll only have to replace the rectangles with the images and the whole game will continue working as it was. If not, something went wrong with the sizes.

To facilitate this, the students needs to upload the specifications to an assignment in Canvas. The API then reads all the submissions, and spreads them around randomly. A few weeks later, the process is repeated with the actual artwork, remembering whose specifications got sent to whom.

foreach(var pair in pairs)
{
	Console.WriteLine($"{pair.Student.id} <=> {pair.Submission.user_id}");

	var assignmentName = templateAssignment.name.Replace("<naam>", pair.Student.name);
	var assignmentDescription = templateAssignment.description.Replace("<p>&lt;afmetingen&gt;</p>", pair.Submission.body).Replace("\n", "");

	var content = new 
	{
		assignment = new 
		{
			name = assignmentName,
			description = assignmentDescription,
			assignment_group_id,
			submission_types = new[] { "online_upload" },
			allowed_extensions = new[] { "zip" },
			grading_type = "pass_fail",
			only_visible_to_overrides = true,
			published = false,
			assignment_overrides = new[] 
			{
				new
				{
					student_ids = new[] { pair.Student.id },
					due_at = "2021-09-22T23:59:00"
				}
			}
		}
	};

	var result = await client.PostAsJsonAsync($"api/v1/courses/{course_id}/assignments", content);

	var assignment = JsonConvert.DeserializeObject<Assignment>(await result.Content.ReadAsStringAsync());
	createdAssignments.Add(assignment);
}

Grade exporter

Canvas can be a great tool, but sometimes it’s limited in some weird and unexpected ways. That’s why I’ve set out to create an add-on for Canvas that adds some much-needed functionality, available for all my colleagues to use. It’s a web app that integrates directly into Canvas. The user is automatically authorized with their Canvas login using the OAuth2 protocol, which generates an Access Token that can subsequently be used to call the Canvas API, thus preventing unauthorized access to sensitive student data.

var client = new HttpClient();

Request.Cookies.TryGetValue("client_id", out string clientID);

var result = await client.PostAsync($"https://hku.instructure.com/login/oauth2/token?grant_type=authorization_code&client_id={clientID}&client_secret=secret&redirect_url=https://{Request.Host}/api/oauth2response&code={code}", null);
if(result.IsSuccessStatusCode)
{
    var stateTemplate = new { r = "", course_id = "" };

    state = Encoding.Default.GetString(Convert.FromBase64String(state));

    var returnState = JsonConvert.DeserializeAnonymousType(state, stateTemplate);

    var info = await result.Content.ReadFromJsonAsync<Oauth2Token>();

    Response.Cookies.Append("access_token", info.access_token, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.None, IsEssential = true, MaxAge = TimeSpan.FromSeconds(info.expires_in) });
    Response.Cookies.Append("refresh_token", info.refresh_token, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.None, IsEssential = true, MaxAge = TimeSpan.FromDays(100.0) });

    return Redirect($"/exportgradebook/{returnState.course_id}");
}

The first tool I’ve built for this add-on is an Excel exporter that exports the grades in a way that can easily be inserted into Osiris, the platform where the final student progression is recorded.

columnName = startColumn;
foreach(var assignment in assignments)
{
    var submission = group.submissions.Where(s => s.submitted_at != null && s.assignment.id == assignment.id).OrderByDescending(s => s.submitted_at).FirstOrDefault();

    if(submission != null)
    {
        if (submission.graded_at != null)
        {
            index = InsertSharedStringItem(submission.grade, shareStringPart);
        }
        else
        {
            index = InsertSharedStringItem("Ingeleverd", shareStringPart);
        }

        cell = InsertCellInWorksheet(columnName.ToString(), rowIndex, worksheetPart);
        cell.CellValue = new CellValue(index.ToString());
        cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
    }

    ++columnName;
}

About Vincent Booman

Hi, my name is Vincent Booman. For me, development is not about a specific language or software, it's about what kind of impact they enable.

Follow @Feathora