Skip to main content

canvas_lms_api/resources/
assignment.rs

1use crate::{
2    error::{CanvasError, Result},
3    http::Requester,
4    pagination::PageStream,
5    params::wrap_params,
6    resources::{progress::Progress, submission::Submission, user::User},
7};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11
12use crate::resources::types::{SubmissionType, WorkflowState};
13
14/// Parameters for creating or editing a Canvas assignment.
15#[derive(Debug, Default, Clone, Serialize)]
16pub struct AssignmentParams {
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub name: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub position: Option<u64>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub submission_types: Option<Vec<String>>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub allowed_extensions: Option<Vec<String>>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub turnitin_enabled: Option<bool>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub allowed_attempts: Option<i64>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub points_possible: Option<f64>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub grading_type: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub due_at: Option<DateTime<Utc>>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub lock_at: Option<DateTime<Utc>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub unlock_at: Option<DateTime<Utc>>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub description: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub published: Option<bool>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub omit_from_final_grade: Option<bool>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub assignment_group_id: Option<u64>,
47}
48
49/// Parameters for submitting an assignment.
50#[derive(Debug, Default, Clone, Serialize)]
51pub struct SubmitAssignmentParams {
52    pub submission_type: String,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub body: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub url: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub file_ids: Option<Vec<u64>>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub media_comment_id: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub media_comment_type: Option<String>,
63}
64
65/// Parameters for creating an assignment override.
66#[derive(Debug, Default, Clone, Serialize)]
67pub struct AssignmentOverrideParams {
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub student_ids: Option<Vec<u64>>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub group_id: Option<u64>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub course_section_id: Option<u64>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub title: Option<String>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub due_at: Option<DateTime<Utc>>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub unlock_at: Option<DateTime<Utc>>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub lock_at: Option<DateTime<Utc>>,
82}
83
84/// Parameters for creating or updating an assignment group.
85#[derive(Debug, Default, Clone, Serialize)]
86pub struct AssignmentGroupParams {
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub name: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub group_weight: Option<f64>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub position: Option<u64>,
93}
94
95/// A Canvas assignment.
96#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
97pub struct Assignment {
98    pub id: u64,
99    pub course_id: Option<u64>,
100    pub name: Option<String>,
101    pub description: Option<String>,
102    pub due_at: Option<DateTime<Utc>>,
103    pub unlock_at: Option<DateTime<Utc>>,
104    pub lock_at: Option<DateTime<Utc>>,
105    pub points_possible: Option<f64>,
106    pub grading_type: Option<String>,
107    pub assignment_group_id: Option<u64>,
108    pub workflow_state: Option<WorkflowState>,
109    pub submission_types: Option<Vec<SubmissionType>>,
110    pub published: Option<bool>,
111    pub muted: Option<bool>,
112    pub html_url: Option<String>,
113    pub has_overrides: Option<bool>,
114    pub needs_grading_count: Option<u64>,
115    pub position: Option<u64>,
116    pub omit_from_final_grade: Option<bool>,
117    pub locked_for_user: Option<bool>,
118
119    #[serde(skip)]
120    pub(crate) requester: Option<Arc<Requester>>,
121}
122
123impl Assignment {
124    fn course_prefix(&self) -> Result<String> {
125        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
126            message: "Assignment has no course_id".to_string(),
127            errors: vec![],
128        })?;
129        Ok(format!("courses/{course_id}/assignments/{}", self.id))
130    }
131
132    fn propagate(&self, a: &mut Assignment) {
133        a.requester = self.requester.clone();
134        a.course_id = self.course_id;
135    }
136
137    fn propagate_sub(&self, s: &mut Submission) {
138        s.requester = self.requester.clone();
139        s.course_id = self.course_id;
140    }
141
142    fn propagate_override(&self, o: &mut AssignmentOverride) {
143        o.requester = self.requester.clone();
144        o.course_id = self.course_id;
145    }
146
147    /// Edit this assignment.
148    ///
149    /// # Canvas API
150    /// `PUT /api/v1/courses/:course_id/assignments/:id`
151    pub async fn edit(&self, params: AssignmentParams) -> Result<Assignment> {
152        let prefix = self.course_prefix()?;
153        let form = wrap_params("assignment", &params);
154        let mut a: Assignment = self.req().put(&prefix, &form).await?;
155        self.propagate(&mut a);
156        Ok(a)
157    }
158
159    /// Delete this assignment.
160    ///
161    /// # Canvas API
162    /// `DELETE /api/v1/courses/:course_id/assignments/:id`
163    pub async fn delete(&self) -> Result<Assignment> {
164        let prefix = self.course_prefix()?;
165        let mut a: Assignment = self.req().delete(&prefix, &[]).await?;
166        self.propagate(&mut a);
167        Ok(a)
168    }
169
170    /// Stream all submissions for this assignment.
171    ///
172    /// # Canvas API
173    /// `GET /api/v1/courses/:course_id/assignments/:id/submissions`
174    pub fn get_submissions(&self) -> PageStream<Submission> {
175        let course_id = self.course_id.unwrap_or(0);
176        let assignment_id = self.id;
177        PageStream::new_with_injector(
178            Arc::clone(self.req()),
179            &format!("courses/{course_id}/assignments/{assignment_id}/submissions"),
180            vec![],
181            move |mut s: Submission, req| {
182                s.requester = Some(Arc::clone(&req));
183                s.course_id = Some(course_id);
184                s
185            },
186        )
187    }
188
189    /// Fetch a single submission by user ID.
190    ///
191    /// # Canvas API
192    /// `GET /api/v1/courses/:course_id/assignments/:id/submissions/:user_id`
193    pub async fn get_submission(&self, user_id: u64) -> Result<Submission> {
194        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
195            message: "Assignment has no course_id".to_string(),
196            errors: vec![],
197        })?;
198        let mut s: Submission = self
199            .req()
200            .get(
201                &format!(
202                    "courses/{course_id}/assignments/{}/submissions/{user_id}",
203                    self.id
204                ),
205                &[],
206            )
207            .await?;
208        self.propagate_sub(&mut s);
209        Ok(s)
210    }
211
212    /// Submit this assignment.
213    ///
214    /// # Canvas API
215    /// `POST /api/v1/courses/:course_id/assignments/:id/submissions`
216    pub async fn submit(&self, params: SubmitAssignmentParams) -> Result<Submission> {
217        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
218            message: "Assignment has no course_id".to_string(),
219            errors: vec![],
220        })?;
221        let form = wrap_params("submission", &params);
222        let mut s: Submission = self
223            .req()
224            .post(
225                &format!("courses/{course_id}/assignments/{}/submissions", self.id),
226                &form,
227            )
228            .await?;
229        self.propagate_sub(&mut s);
230        Ok(s)
231    }
232
233    /// Stream all overrides for this assignment.
234    ///
235    /// # Canvas API
236    /// `GET /api/v1/courses/:course_id/assignments/:id/overrides`
237    pub fn get_overrides(&self) -> PageStream<AssignmentOverride> {
238        let course_id = self.course_id.unwrap_or(0);
239        let assignment_id = self.id;
240        PageStream::new_with_injector(
241            Arc::clone(self.req()),
242            &format!("courses/{course_id}/assignments/{assignment_id}/overrides"),
243            vec![],
244            move |mut o: AssignmentOverride, req| {
245                o.requester = Some(Arc::clone(&req));
246                o.course_id = Some(course_id);
247                o
248            },
249        )
250    }
251
252    /// Fetch a single override by ID.
253    ///
254    /// # Canvas API
255    /// `GET /api/v1/courses/:course_id/assignments/:id/overrides/:override_id`
256    pub async fn get_override(&self, override_id: u64) -> Result<AssignmentOverride> {
257        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
258            message: "Assignment has no course_id".to_string(),
259            errors: vec![],
260        })?;
261        let mut o: AssignmentOverride = self
262            .req()
263            .get(
264                &format!(
265                    "courses/{course_id}/assignments/{}/overrides/{override_id}",
266                    self.id
267                ),
268                &[],
269            )
270            .await?;
271        self.propagate_override(&mut o);
272        Ok(o)
273    }
274
275    /// Create an override for this assignment.
276    ///
277    /// # Canvas API
278    /// `POST /api/v1/courses/:course_id/assignments/:id/overrides`
279    pub async fn create_override(
280        &self,
281        params: AssignmentOverrideParams,
282    ) -> Result<AssignmentOverride> {
283        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
284            message: "Assignment has no course_id".to_string(),
285            errors: vec![],
286        })?;
287        let form = wrap_params("assignment_override", &params);
288        let mut o: AssignmentOverride = self
289            .req()
290            .post(
291                &format!("courses/{course_id}/assignments/{}/overrides", self.id),
292                &form,
293            )
294            .await?;
295        self.propagate_override(&mut o);
296        Ok(o)
297    }
298
299    /// Stream all peer reviews for this assignment.
300    ///
301    /// # Canvas API
302    /// `GET /api/v1/courses/:course_id/assignments/:id/peer_reviews`
303    pub fn get_peer_reviews(&self) -> PageStream<serde_json::Value> {
304        let course_id = self.course_id.unwrap_or(0);
305        PageStream::new(
306            Arc::clone(self.req()),
307            &format!("courses/{course_id}/assignments/{}/peer_reviews", self.id),
308            vec![],
309        )
310    }
311
312    /// Stream all gradeable students for this assignment.
313    ///
314    /// # Canvas API
315    /// `GET /api/v1/courses/:course_id/assignments/:id/gradeable_students`
316    pub fn get_gradeable_students(&self) -> PageStream<User> {
317        let course_id = self.course_id.unwrap_or(0);
318        PageStream::new(
319            Arc::clone(self.req()),
320            &format!(
321                "courses/{course_id}/assignments/{}/gradeable_students",
322                self.id
323            ),
324            vec![],
325        )
326    }
327
328    /// Set extensions for this assignment for one or more students.
329    ///
330    /// # Canvas API
331    /// `POST /api/v1/courses/:course_id/assignments/:id/extensions`
332    pub async fn set_extensions(&self, params: &[(String, String)]) -> Result<serde_json::Value> {
333        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
334            message: "Assignment has no course_id".to_string(),
335            errors: vec![],
336        })?;
337        self.req()
338            .post(
339                &format!("courses/{course_id}/assignments/{}/extensions", self.id),
340                params,
341            )
342            .await
343    }
344
345    /// Stream grade change events for this assignment.
346    ///
347    /// # Canvas API
348    /// `GET /api/v1/audit/grade_change/assignments/:id`
349    pub fn get_grade_change_events(&self) -> PageStream<serde_json::Value> {
350        PageStream::new(
351            Arc::clone(self.req()),
352            &format!("audit/grade_change/assignments/{}", self.id),
353            vec![],
354        )
355    }
356
357    /// Stream students selected for moderation on this assignment.
358    ///
359    /// # Canvas API
360    /// `GET /api/v1/courses/:course_id/assignments/:id/moderated_students`
361    pub fn get_students_selected_for_moderation(&self) -> PageStream<User> {
362        let course_id = self.course_id.unwrap_or(0);
363        PageStream::new(
364            Arc::clone(self.req()),
365            &format!(
366                "courses/{course_id}/assignments/{}/moderated_students",
367                self.id
368            ),
369            vec![],
370        )
371    }
372
373    /// Select students for moderation on this assignment.
374    ///
375    /// # Canvas API
376    /// `POST /api/v1/courses/:course_id/assignments/:id/moderated_students`
377    pub async fn select_students_for_moderation(
378        &self,
379        student_ids: &[u64],
380    ) -> Result<Vec<serde_json::Value>> {
381        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
382            message: "Assignment has no course_id".to_string(),
383            errors: vec![],
384        })?;
385        let params: Vec<(String, String)> = student_ids
386            .iter()
387            .map(|id| ("student_ids[]".to_string(), id.to_string()))
388            .collect();
389        self.req()
390            .post(
391                &format!(
392                    "courses/{course_id}/assignments/{}/moderated_students",
393                    self.id
394                ),
395                &params,
396            )
397            .await
398    }
399
400    /// Check whether a student's submission needs a provisional grade.
401    ///
402    /// # Canvas API
403    /// `GET /api/v1/courses/:course_id/assignments/:id/provisional_grades/status`
404    pub async fn get_provisional_grades_status(
405        &self,
406        student_id: u64,
407    ) -> Result<serde_json::Value> {
408        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
409            message: "Assignment has no course_id".to_string(),
410            errors: vec![],
411        })?;
412        let params = vec![("student_id".to_string(), student_id.to_string())];
413        self.req()
414            .get(
415                &format!(
416                    "courses/{course_id}/assignments/{}/provisional_grades/status",
417                    self.id
418                ),
419                &params,
420            )
421            .await
422    }
423
424    /// Select which provisional grade the student should receive.
425    ///
426    /// # Canvas API
427    /// `PUT /api/v1/courses/:course_id/assignments/:id/provisional_grades/:provisional_grade_id/select`
428    pub async fn selected_provisional_grade(
429        &self,
430        provisional_grade_id: u64,
431    ) -> Result<serde_json::Value> {
432        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
433            message: "Assignment has no course_id".to_string(),
434            errors: vec![],
435        })?;
436        self.req()
437            .put(
438                &format!(
439                    "courses/{course_id}/assignments/{}/provisional_grades/{provisional_grade_id}/select",
440                    self.id
441                ),
442                &[],
443            )
444            .await
445    }
446
447    /// Publish provisional grades for all submissions on this assignment.
448    ///
449    /// # Canvas API
450    /// `POST /api/v1/courses/:course_id/assignments/:id/provisional_grades/publish`
451    pub async fn publish_provisional_grades(&self) -> Result<serde_json::Value> {
452        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
453            message: "Assignment has no course_id".to_string(),
454            errors: vec![],
455        })?;
456        self.req()
457            .post(
458                &format!(
459                    "courses/{course_id}/assignments/{}/provisional_grades/publish",
460                    self.id
461                ),
462                &[],
463            )
464            .await
465    }
466
467    /// Show whether a student's submission needs a provisional grade (anonymous grading).
468    ///
469    /// # Canvas API
470    /// `GET /api/v1/courses/:course_id/assignments/:id/anonymous_provisional_grades/status`
471    pub async fn show_provisional_grades_for_student(
472        &self,
473        anonymous_id: u64,
474    ) -> Result<serde_json::Value> {
475        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
476            message: "Assignment has no course_id".to_string(),
477            errors: vec![],
478        })?;
479        let params = vec![("anonymous_id".to_string(), anonymous_id.to_string())];
480        self.req()
481            .get(
482                &format!(
483                    "courses/{course_id}/assignments/{}/anonymous_provisional_grades/status",
484                    self.id
485                ),
486                &params,
487            )
488            .await
489    }
490
491    /// Bulk-update grades for this assignment asynchronously.
492    ///
493    /// # Canvas API
494    /// `POST /api/v1/courses/:course_id/assignments/:id/submissions/update_grades`
495    pub async fn submissions_bulk_update(&self, params: &[(String, String)]) -> Result<Progress> {
496        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
497            message: "Assignment has no course_id".to_string(),
498            errors: vec![],
499        })?;
500        let mut p: Progress = self
501            .req()
502            .post(
503                &format!(
504                    "courses/{course_id}/assignments/{}/submissions/update_grades",
505                    self.id
506                ),
507                params,
508            )
509            .await?;
510        p.requester = self.requester.clone();
511        Ok(p)
512    }
513}
514
515/// A Canvas assignment group.
516#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
517pub struct AssignmentGroup {
518    pub id: u64,
519    pub name: Option<String>,
520    pub group_weight: Option<f64>,
521    pub position: Option<u64>,
522    pub rules: Option<serde_json::Value>,
523    pub assignments: Option<Vec<serde_json::Value>>,
524
525    #[serde(skip)]
526    pub(crate) requester: Option<Arc<Requester>>,
527    #[serde(skip)]
528    pub course_id: Option<u64>,
529}
530
531impl AssignmentGroup {
532    /// Edit this assignment group.
533    ///
534    /// # Canvas API
535    /// `PUT /api/v1/courses/:course_id/assignment_groups/:id`
536    pub async fn edit(&self, params: AssignmentGroupParams) -> Result<AssignmentGroup> {
537        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
538            message: "AssignmentGroup has no course_id".to_string(),
539            errors: vec![],
540        })?;
541        let form = wrap_params("assignment_group", &params);
542        let mut g: AssignmentGroup = self
543            .req()
544            .put(
545                &format!("courses/{course_id}/assignment_groups/{}", self.id),
546                &form,
547            )
548            .await?;
549        g.requester = self.requester.clone();
550        g.course_id = self.course_id;
551        Ok(g)
552    }
553
554    /// Delete this assignment group.
555    ///
556    /// # Canvas API
557    /// `DELETE /api/v1/courses/:course_id/assignment_groups/:id`
558    pub async fn delete(&self) -> Result<AssignmentGroup> {
559        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
560            message: "AssignmentGroup has no course_id".to_string(),
561            errors: vec![],
562        })?;
563        let mut g: AssignmentGroup = self
564            .req()
565            .delete(
566                &format!("courses/{course_id}/assignment_groups/{}", self.id),
567                &[],
568            )
569            .await?;
570        g.requester = self.requester.clone();
571        g.course_id = self.course_id;
572        Ok(g)
573    }
574}
575
576/// An override for a Canvas assignment (adjusts due dates for specific students/groups/sections).
577#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
578pub struct AssignmentOverride {
579    pub id: u64,
580    pub assignment_id: Option<u64>,
581    pub student_ids: Option<Vec<u64>>,
582    pub group_id: Option<u64>,
583    pub course_section_id: Option<u64>,
584    pub title: Option<String>,
585    pub due_at: Option<DateTime<Utc>>,
586    pub unlock_at: Option<DateTime<Utc>>,
587    pub lock_at: Option<DateTime<Utc>>,
588
589    #[serde(skip)]
590    pub(crate) requester: Option<Arc<Requester>>,
591    #[serde(skip)]
592    pub course_id: Option<u64>,
593}
594
595impl AssignmentOverride {
596    fn prefix(&self) -> Result<String> {
597        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
598            message: "AssignmentOverride has no course_id".to_string(),
599            errors: vec![],
600        })?;
601        let assignment_id = self.assignment_id.ok_or_else(|| CanvasError::BadRequest {
602            message: "AssignmentOverride has no assignment_id".to_string(),
603            errors: vec![],
604        })?;
605        Ok(format!(
606            "courses/{course_id}/assignments/{assignment_id}/overrides/{}",
607            self.id
608        ))
609    }
610
611    fn propagate(&self, o: &mut AssignmentOverride) {
612        o.requester = self.requester.clone();
613        o.course_id = self.course_id;
614    }
615
616    /// Edit this override.
617    ///
618    /// # Canvas API
619    /// `PUT /api/v1/courses/:course_id/assignments/:assignment_id/overrides/:id`
620    pub async fn edit(&self, params: AssignmentOverrideParams) -> Result<AssignmentOverride> {
621        let prefix = self.prefix()?;
622        let form = wrap_params("assignment_override", &params);
623        let mut o: AssignmentOverride = self.req().put(&prefix, &form).await?;
624        self.propagate(&mut o);
625        Ok(o)
626    }
627
628    /// Delete this override.
629    ///
630    /// # Canvas API
631    /// `DELETE /api/v1/courses/:course_id/assignments/:assignment_id/overrides/:id`
632    pub async fn delete(&self) -> Result<AssignmentOverride> {
633        let prefix = self.prefix()?;
634        let mut o: AssignmentOverride = self.req().delete(&prefix, &[]).await?;
635        self.propagate(&mut o);
636        Ok(o)
637    }
638}