atomic_lti/assignment_grade_services/
score.rs

1use crate::errors::AssignmentGradeServicesError;
2use crate::request::send_request;
3use chrono::{DateTime, Utc};
4use reqwest::header;
5use serde::{Deserialize, Serialize};
6use std::time::SystemTime;
7
8#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
9pub enum ActivityProgress {
10  Initialized,
11  Started,
12  InProgress,
13  Submitted,
14  Completed,
15}
16
17#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
18pub enum GradingProgress {
19  NotReady,
20  Failed,
21  Pending,
22  PendingManual,
23  FullyGraded,
24}
25
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct Score {
28  pub user_id: String, // The lti_user_id
29  pub score_given: f32,
30  pub score_maximum: f32,
31  pub comment: Option<String>,
32  pub timestamp: String,
33  pub activity_progress: ActivityProgress,
34  pub grading_progress: GradingProgress,
35  #[serde(rename = "https://canvas.instructure.com/lti/submission[new_submission]")]
36  pub new_submission: Option<bool>,
37  #[serde(rename = "https://canvas.instructure.com/lti/submission[preserve_score]")]
38  pub preserve_score: Option<bool>,
39  #[serde(rename = "https://canvas.instructure.com/lti/submission[prioritize_non_tool_grade]")]
40  pub prioritize_non_tool_grade: Option<bool>,
41  #[serde(rename = "https://canvas.instructure.com/lti/submission[submission_type]")]
42  pub submission_type: Option<String>,
43  #[serde(rename = "https://canvas.instructure.com/lti/submission[submission_data]")]
44  pub submission_data: Option<String>,
45  #[serde(rename = "https://canvas.instructure.com/lti/submission[submitted_at]")]
46  pub submitted_at: Option<String>,
47  #[serde(rename = "https://canvas.instructure.com/lti/submission[content_items]")]
48  pub content_items: Option<Vec<ScoreContentItem>>,
49}
50
51#[derive(Debug, Serialize, Deserialize, Clone)]
52pub struct ScoreContentItem {
53  #[serde(rename = "type")]
54  pub type_: String,
55  pub url: String,
56  pub title: Option<String>,
57  pub media_type: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone)]
61pub struct ScoreResponse {
62  #[serde(alias = "resultUrl")]
63  pub result_url: String,
64  #[serde(rename = "https://canvas.instructure.com/lti/submission")]
65  pub submission: Submission,
66}
67
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct Submission {
70  pub content_items: Vec<ScoreContentItem>,
71}
72
73impl Score {
74  pub fn new(
75    user_id: String,
76    score_given: f32,
77    score_maximum: f32,
78    comment: Option<String>,
79    activity_progress: ActivityProgress,
80    grading_progress: GradingProgress,
81  ) -> Self {
82    let now = SystemTime::now();
83    let datetime: DateTime<Utc> = now.into();
84    let iso_string = datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
85    Self {
86      user_id,
87      score_given,
88      score_maximum,
89      comment,
90      timestamp: iso_string,
91      activity_progress,
92      grading_progress,
93      new_submission: None,
94      preserve_score: None,
95      prioritize_non_tool_grade: None,
96      submission_type: None,
97      submission_data: None,
98      submitted_at: None,
99      content_items: None,
100    }
101  }
102
103  pub fn default(user_id: &str, score_given: f32, score_maximum: f32) -> Self {
104    let now = SystemTime::now();
105    let datetime: DateTime<Utc> = now.into();
106    let iso_string = datetime.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
107    Self {
108      user_id: user_id.to_string(),
109      score_given,
110      score_maximum,
111      comment: None,
112      timestamp: iso_string,
113      activity_progress: ActivityProgress::Initialized,
114      grading_progress: GradingProgress::NotReady,
115      new_submission: None,
116      preserve_score: None,
117      prioritize_non_tool_grade: None,
118      submission_type: None,
119      submission_data: None,
120      submitted_at: None,
121      content_items: None,
122    }
123  }
124}
125
126pub async fn send_score(
127  api_token: &str,
128  id: &str,
129  score: &Score,
130) -> Result<ScoreResponse, AssignmentGradeServicesError> {
131  let url = format!("{}/scores", id);
132
133  let json = serde_json::to_string(score)
134    .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
135  let client = reqwest::Client::new();
136  let request = client
137    .post(url)
138    .header(header::AUTHORIZATION, format!("Bearer {}", api_token))
139    .header("Content-Type", "application/vnd.ims.lis.v1.score+json")
140    .header(header::ACCEPT, "application/json")
141    .body(json);
142  let body = send_request(request).await?;
143
144  let response: ScoreResponse = serde_json::from_str(&body)
145    .map_err(|e| AssignmentGradeServicesError::RequestFailed(e.to_string()))?;
146
147  Ok(response)
148}
149
150#[cfg(test)]
151mod tests {
152  use super::*;
153  use mockito;
154
155  #[tokio::test]
156  async fn test_send_score() {
157    let mut server = mockito::Server::new_async().await;
158    let server_url = server.url();
159    let mock = server
160      .mock("POST", "/123/scores")
161      .with_status(200)
162      .with_header("content-type", "application/json")
163      .with_body(
164        r#"{
165          "resultUrl": "https://canvas.instructure.com/url/to/result",
166          "https://canvas.instructure.com/lti/submission": {
167            "content_items": [
168              {
169                "type": "file",
170                "url": "https://instructure.com/test_file.txt",
171                "title": "Submission File",
172                "progress": "https://canvas.instructure.com/url/to/progress"
173              }
174            ]
175          }
176        }"#,
177      )
178      .create();
179
180    let api_token = "not a real token";
181    let user_id = "1".to_string();
182    let score = Score::default(&user_id, 9.0, 10.0);
183    let id = format!("{}/123", server_url);
184    let result = send_score(api_token, &id, &score).await;
185
186    dbg!(&result);
187    mock.assert();
188    assert!(&result.is_ok());
189    let response = result.unwrap();
190    assert_eq!(
191      response.result_url,
192      "https://canvas.instructure.com/url/to/result"
193    );
194    assert_eq!(response.submission.content_items.len(), 1);
195  }
196}