Tutorial: Canvas-GitLab Use Case

How to create a Use Case

First, create a new Haskell project. For example, with the following command if you’re using stack.

stack new my-use-case

Then add canvas-haskell and gitlab-haskell to the dependencies in your package.yaml file:

dependencies:
  - canvas-haskell
  - gitlab-haskell >= 1.0.0.1

And add them to the extra-dependencies of your stack.yaml file:

extra-deps:
  - git: https://gitlab.com/lauraschauer/canvas-haskell-library.git
    commit: <latest commit hash>
  - git: https://gitlab.com/robstewart57/gitlab-haskell.git
    commit: <latest commit hash>

And you should be ready to write our use case program! Navigate to your Main.hs file and start with importing canvas-haskell and gitlab-haskell:

{-# LANGUAGE OverloadedStrings #-}

module Main (main) where 

import Canvas
import GitLab 
import Data.Text as T

main :: IO () 
main = print ()

Before any data can be obtained from either API, you need to authenticate (or log-in). For that, you will need to set up access tokens on both systems. Once you have these access tokens, you can authenticate like so:

main :: IO () 
main = do
  -- Authenticating with Canvas
  let cToken = T.pack "<your-canvas-token>"

  let securityScheme = bearerAuthenticationSecurityScheme cToken
  let canvasConfig =
        defaultConfiguration
          { configBaseURL = "<your-canvas-instance-URL>",
            configSecurityScheme = securityScheme
          }

  -- Authenticating with GitLab
  let gToken = T.pack "<your-gitlab-token>"

  let gitlabConfig =
        defaultGitLabServer
          { url = "<your-gitlab-instance-URL>",
            token = gToken
          }

  -- to stop Haskell from complaining 
  print () 

Now the set-up is done, let’s think about what you actually need from Canvas and GitLab to be able to add a student’s GitLab project URL to Canvas. First, you need the GitLab project URL of the student’s coursework project. Then you need to find the student’s assignment on Canvas, and add the URL to their submission. You can therefore split your program into two phases:

  1. Interaction with GitLab
    1. Obtain the student’s project URL
  2. Interaction with Canvas
    1. Find the Canvas assignment corresponding to the GitLab project
    2. Add the URL to the student’s submission for that assignment

So far, this structure proved to be useful to all use cases. First, do some stuff on GitLab, then do some stuff on Canvas. Or the other way around.

Let’s start by importing one student’s URL. Arguably, this program would be much more helpful if it imported the URLs for all students on a course, but once it imports one, this is just a question of performing the same operation for a list. Therefore, let’s assume that we are in possession of the student’s username and the project’s name.

let gUsername = "<the student username>"
let gProjectName = "<the project name>"

As per README.md of canvas-haskell, GitLab actions are run with runGitLab:

runGitLab gitlabConfig $ do
  -- code to obtain the project URL 

The GitLab API has a Project API, which contains an endpoint called Search for projects. We can either search for projects with their name only (which might give us hundreds in the case of a whole roster of students), or we search for projects with their path and name. As per documentation, this endpoint “gets a project with the given name for the given full path of the namespace”:

projectWithPathAndName :: Text -> Text -> GitLab (Either (Response Bytestring) (Maybe Project))

-- example usage
projectwithPathAndName "user1" "project1"

If everything goes right, the function will return a Project object, which contains a field called project_web_url - exactly the URL we need. Let’s do that.

-- Omits the first part of the main function (it's above)

-- Authenticating with GitLab
let gToken = T.pack "<your-gitlab-token>"

let gitlabConfig =
      defaultGitLabServer
        { url = "<your-gitlab-instance-URL>",
          token = gToken
        }

let gUsername = "<the student username>"
let gProjectName = "<the project name>"

gProjectURL <- runGitLab gitlabConfig $ do 
  -- send the API request
  result <- projectWithPathAndName gUserName gProjectName 
  -- pattern match on the response of the request 
  case result of 
    Left response -> error (show response) -- error because the project couldn't be found
    Right Nothing -> error "Project not found" -- error because the project couldn't be found
    Right (Just p) -> return (project_web_url p) -- return project

-- Omits the end of the main function (it's above)

Now that you’ve (hopefully) obtained the project’s URL, it’s time to import it into Canvas. We’ve got two bullet points from our list left:

  1. Find the Canvas assignment corresponding to the GitLab project
  2. Add the URL to the student’s submission for that assignment

Just like for GitLab, let’s run a Canvas action with canvas-haskell. Instead of passing the query parameters as singular parameters to a function, canvas-haskell usually uses a query parameter object, e.g. userParams below. The (slightly) lengthy variable are due to the library being auto-generated.

In order to get the Canvas assignment associated with the GitLab project, you can use the listAssignmentsForUser function which sends an API request to the endpoint with the same name. List assignments for a user allows you to search for a certain assignment with the search_term parameter. To use this endpoint, you’ll need the student’s user ID. Fortunately, there’s another convenient Canvas endpoint that returns all student information given a student’s username, including their ID:

listUsersInCourseUsers :: forall m . MonadHTTP m => ListUsersInCourseUsersParameters -> ListUsersInCourseUsersResponse

One important feature in the set-up at Heriot-Watt university is that each student’s username on Canvas and GitLab are the same. Hence, we can use the student’s GitLab username gUserName to obtain the student’s user object from Canvas. Let’s use the endpoint mentioned above to retrieve the student’s user:

  runWithConfiguration canvasConfig $ do 
  -- get user from Canvas with username
    let userParams =
          (mkListUsersInCourseUsersParameters cCourseId)
            { listUsersInCourseUsersParametersQuerySearchTerm = Just gUserName
            }

    studentUser <- do
      -- send the API request
      response <- listUsersInCourseUsers userParams
      -- pattern match on the response of the request
      case responseBody response of
        ListUsersInCourseUsersResponseError err -> error (show err)
        ListUsersInCourseUsersResponse200 theUsers -> return (head theUsers) -- get the first of the list as there is only one user associated with each username

    -- get the student's ID
    let studentId = userId studentUser

And voilà, you’re in possession of the student’s ID. Now you can use it to create the parameter object for the assignment endpoint. Don’t forget to set the search term to the GitLab project name:

    -- create parameters
    let assignmentParams =
          (mkListAssignmentsForUserParameters cCourseId testStudentId)
            { listAssignmentsForUserParametersQuerySearchTerm = Just gProjectName -- set the search term
            }

    assignment <- do
      -- send the API request
      response <- listAssignmentsForUser assignmentParams
      -- pattern match on the response 
      case responseBody response of
        ListAssignmentsForUserResponseError err -> error (show err)
        ListAssignmentsForUserResponse200 theAssignments -> return (head theAssignments) -- there should only be one assignment with this name, hence we can call `head`

    -- obtain the assignment ID (it's needed later)
    let cAssignmentId = T.pack (show (safeNullNothing (-1) (assignmentId assignment)))

The safeNullNothing function used here is a simple function that unwraps the Maybe (Nullable assignmentId). It’s implemented in Common.hs of canvas-gitlab and has the following definition:

-- Function to safely extract a value from Maybe (Nullable a)
safeNullNothing :: a -> Maybe (Nullable a) -> a
safeNullNothing x Nothing = x
safeNullNothing x (Just Null) = x
safeNullNothing _ (Just (NonNull a)) = a

With the assignment ID, you now have everything you need in order to add a comment to a student’s submission.

If you’re thinking: Why do we add this to a student’s submission? Then the answer is that we couldn’t find a better ‘space’ for this information to be displayed on Canvas. It needs to be linked to a one student and one assignment. Only the student can create a submission, which eradicates the option of creating a new submission object (also, it would be weird for the student to see a new submission they didn’t submit). Therefore, we decided to just add a comment to their most recent one. The comment will only be visible to students after the deadline.

The Submissions API of the Canvas API allows us to change an existing submission using the following endpoint (here for the Canvas documentation):

gradeOrCommentOnSubmissionCourses :: forall m . MonadHTTP m => GradeOrCommentOnSubmissionCoursesParameters
  -> Maybe GradeOrCommentOnSubmissionCoursesRequestBody -- ^ The request body to send
  -> ClientT m GradeOrCommentOnSubmissionCoursesResponse -- ^ Monadic computation which returns the result of the operation

This function looks a bit more complicated, but just takes not only an object for query parameters, but also an object for a request body. In this request body, we can include the textual comment we would like to add, in our case the project URL from GitLab. Through trial and error, we’ve found that changing the submission without including a grade in the request body, the submission’s grade gets overwritten and erased. Hence, we’ll first need to obtain the student’s current grade, and add it with the URL to the request body.

Therefore, let’s start by obtaining the student’s current grade the most recent submission. We can do so by using the following function covering the Get a single submission endpoint:

getSingleSubmissionCourses :: forall m . MonadHTTP m => GetSingleSubmissionCoursesParameters -> GetSingleSubmissionCoursesResponse

Adding this to our code:

    -- create the paramters to obtain the submission
    let submissionParams = mkGetSingleSubmissionCoursesParameters cAssignmentId cCourseId studentId

    studentSubmission <- do
      -- send the API request
      response <- getSingleSubmissionCourses submissionParams
      -- pattern match on the response
      case responseBody response of
        GetSingleSubmissionCoursesResponseError err -> error (show err)
        GetSingleSubmissionCoursesResponse200 theSubmission -> return theSubmission

    -- get the student's current grade 
    let cGrade = safeNullNothing T.empty (submissionGrade studentSubmission)

Now that we’ve got the student’s grade, we’re finally ready to comment the URL of their GitLab project on their submission.

    -- create parameters to comment on the submission
    let params = mkGradeOrCommentOnSubmissionCoursesParameters cAssignmentId cCourseId studentId

    -- create the request body containing the comment and the grade
    let reqBody =
          mkGradeOrCommentOnSubmissionCoursesRequestBody
            { gradeOrCommentOnSubmissionCoursesRequestBodyCommentTextComment_ = Just ("The GitLab project URL: " <> gProjectURL),
              gradeOrCommentOnSubmissionCoursesRequestBodySubmissionPostedGrade_ = Just cGrade
            }

    void $ do
      -- send the API request
      response <- gradeOrCommentOnSubmissionCourses params (Just reqBody)
      -- pattern match on the response one last time
      case responseBody response of
        GradeOrCommentOnSubmissionCoursesResponseError err -> error (show err)
        GradeOrCommentOnSubmissionCoursesResponse200 theSubmission -> return theSubmission

And done! If everything went well, this program should have updated the student’s submission with the GitLab project URL. If you want to see the full version of this use case, click here. This code adds the URL of GitLab projects as soon as students push.

It’s down to you if you want to add more information from GitLab to the comment, or if you want to save the URL somewhere else, e.g., a spreadsheet.

If you have any questions, please don’t hesitate to just email or message me on LinkedIn.