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:
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:
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.