atomic_lti/assignment_grade_services/
score.rs1use 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, 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}