Skip to main content

canvas_lms_api/resources/
module.rs

1use crate::{
2    error::{CanvasError, Result},
3    http::Requester,
4    pagination::PageStream,
5    params::wrap_params,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10
11/// Parameters for creating or updating a Canvas module.
12#[derive(Debug, Default, Clone, Serialize)]
13pub struct UpdateModuleParams {
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub name: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub position: Option<u64>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub unlock_at: Option<DateTime<Utc>>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub require_sequential_progress: Option<bool>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub publish_final_grade: Option<bool>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub published: Option<bool>,
26}
27
28/// Parameters for creating a new Canvas module.
29pub type CreateModuleParams = UpdateModuleParams;
30
31/// Parameters for creating a module item.
32#[derive(Debug, Clone, Serialize)]
33pub struct CreateModuleItemParams {
34    #[serde(rename = "type")]
35    pub item_type: String,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub content_id: Option<u64>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub position: Option<u64>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub indent: Option<u64>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub page_url: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub external_url: Option<String>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub new_tab: Option<bool>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub published: Option<bool>,
50}
51
52/// Parameters for updating a module item.
53#[derive(Debug, Default, Clone, Serialize)]
54pub struct UpdateModuleItemParams {
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub title: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub position: Option<u64>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub indent: Option<u64>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub external_url: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub new_tab: Option<bool>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub published: Option<bool>,
67}
68
69/// A Canvas course module (a collection of ordered content items).
70#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
71pub struct Module {
72    pub id: u64,
73    pub course_id: Option<u64>,
74    pub name: Option<String>,
75    pub position: Option<u64>,
76    pub unlock_at: Option<DateTime<Utc>>,
77    pub require_sequential_progress: Option<bool>,
78    pub prerequisite_module_ids: Option<Vec<u64>>,
79    pub items_count: Option<u64>,
80    pub items_url: Option<String>,
81    pub state: Option<String>,
82    pub completed_at: Option<DateTime<Utc>>,
83    pub publish_final_grade: Option<bool>,
84    pub published: Option<bool>,
85
86    #[serde(skip)]
87    pub(crate) requester: Option<Arc<Requester>>,
88}
89
90impl Module {
91    fn course_id_or_err(&self) -> Result<u64> {
92        self.course_id.ok_or_else(|| CanvasError::BadRequest {
93            message: "Module does not have a course_id".to_string(),
94            errors: vec![],
95        })
96    }
97
98    fn propagate(&self, m: &mut Module) {
99        m.requester = self.requester.clone();
100        m.course_id = self.course_id;
101    }
102
103    fn propagate_item(&self, item: &mut ModuleItem) {
104        item.requester = self.requester.clone();
105        item.course_id = self.course_id;
106        item.module_id = Some(self.id);
107    }
108
109    /// Edit this module's settings.
110    ///
111    /// # Canvas API
112    /// `PUT /api/v1/courses/:course_id/modules/:id`
113    pub async fn edit(&self, params: UpdateModuleParams) -> Result<Module> {
114        let course_id = self.course_id_or_err()?;
115        let form = wrap_params("module", &params);
116        let mut m: Module = self
117            .req()
118            .put(&format!("courses/{course_id}/modules/{}", self.id), &form)
119            .await?;
120        self.propagate(&mut m);
121        Ok(m)
122    }
123
124    /// Delete this module.
125    ///
126    /// # Canvas API
127    /// `DELETE /api/v1/courses/:course_id/modules/:id`
128    pub async fn delete(&self) -> Result<Module> {
129        let course_id = self.course_id_or_err()?;
130        let mut m: Module = self
131            .req()
132            .delete(&format!("courses/{course_id}/modules/{}", self.id), &[])
133            .await?;
134        self.propagate(&mut m);
135        Ok(m)
136    }
137
138    /// Re-lock this module's progressions.
139    ///
140    /// # Canvas API
141    /// `PUT /api/v1/courses/:course_id/modules/:id/relock`
142    pub async fn relock(&self) -> Result<Module> {
143        let course_id = self.course_id_or_err()?;
144        let mut m: Module = self
145            .req()
146            .put(
147                &format!("courses/{course_id}/modules/{}/relock", self.id),
148                &[],
149            )
150            .await?;
151        self.propagate(&mut m);
152        Ok(m)
153    }
154
155    /// Stream all items in this module.
156    ///
157    /// # Canvas API
158    /// `GET /api/v1/courses/:course_id/modules/:id/items`
159    pub fn get_module_items(&self) -> PageStream<ModuleItem> {
160        let course_id = self.course_id.unwrap_or(0);
161        let module_id = self.id;
162        PageStream::new_with_injector(
163            Arc::clone(self.req()),
164            &format!("courses/{course_id}/modules/{module_id}/items"),
165            vec![],
166            move |mut item: ModuleItem, req| {
167                item.requester = Some(Arc::clone(&req));
168                item.course_id = Some(course_id);
169                item.module_id = Some(module_id);
170                item
171            },
172        )
173    }
174
175    /// Fetch a single module item by ID.
176    ///
177    /// # Canvas API
178    /// `GET /api/v1/courses/:course_id/modules/:id/items/:item_id`
179    pub async fn get_module_item(&self, item_id: u64) -> Result<ModuleItem> {
180        let course_id = self.course_id_or_err()?;
181        let mut item: ModuleItem = self
182            .req()
183            .get(
184                &format!("courses/{course_id}/modules/{}/items/{item_id}", self.id),
185                &[],
186            )
187            .await?;
188        self.propagate_item(&mut item);
189        Ok(item)
190    }
191
192    /// Create a new item in this module.
193    ///
194    /// # Canvas API
195    /// `POST /api/v1/courses/:course_id/modules/:id/items`
196    pub async fn create_module_item(&self, params: CreateModuleItemParams) -> Result<ModuleItem> {
197        let course_id = self.course_id_or_err()?;
198        // content_id is required for all types except SubHeader
199        if params.item_type != "SubHeader" && params.content_id.is_none() {
200            return Err(CanvasError::BadRequest {
201                message: "content_id is required for this module item type".to_string(),
202                errors: vec![],
203            });
204        }
205        let form = wrap_params("module_item", &params);
206        let mut item: ModuleItem = self
207            .req()
208            .post(
209                &format!("courses/{course_id}/modules/{}/items", self.id),
210                &form,
211            )
212            .await?;
213        self.propagate_item(&mut item);
214        Ok(item)
215    }
216}
217
218/// An individual item within a Canvas module.
219#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
220pub struct ModuleItem {
221    pub id: u64,
222    pub module_id: Option<u64>,
223    pub position: Option<u64>,
224    pub title: Option<String>,
225    pub indent: Option<u64>,
226    #[serde(rename = "type")]
227    pub item_type: Option<String>,
228    pub content_id: Option<u64>,
229    pub html_url: Option<String>,
230    pub url: Option<String>,
231    pub page_url: Option<String>,
232    pub external_url: Option<String>,
233    pub completion_requirement: Option<CompletionRequirement>,
234    pub published: Option<bool>,
235
236    #[serde(skip)]
237    pub(crate) requester: Option<Arc<Requester>>,
238    #[serde(skip)]
239    pub course_id: Option<u64>,
240}
241
242impl ModuleItem {
243    fn course_module_prefix(&self) -> Result<String> {
244        let course_id = self.course_id.ok_or_else(|| CanvasError::BadRequest {
245            message: "ModuleItem does not have a course_id".to_string(),
246            errors: vec![],
247        })?;
248        let module_id = self.module_id.ok_or_else(|| CanvasError::BadRequest {
249            message: "ModuleItem does not have a module_id".to_string(),
250            errors: vec![],
251        })?;
252        Ok(format!("courses/{course_id}/modules/{module_id}"))
253    }
254
255    fn propagate(&self, item: &mut ModuleItem) {
256        item.requester = self.requester.clone();
257        item.course_id = self.course_id;
258        item.module_id = self.module_id;
259    }
260
261    /// Edit this module item.
262    ///
263    /// # Canvas API
264    /// `PUT /api/v1/courses/:course_id/modules/:module_id/items/:id`
265    pub async fn edit(&self, params: UpdateModuleItemParams) -> Result<ModuleItem> {
266        let prefix = self.course_module_prefix()?;
267        let form = wrap_params("module_item", &params);
268        let mut item: ModuleItem = self
269            .req()
270            .put(&format!("{prefix}/items/{}", self.id), &form)
271            .await?;
272        self.propagate(&mut item);
273        Ok(item)
274    }
275
276    /// Delete this module item.
277    ///
278    /// # Canvas API
279    /// `DELETE /api/v1/courses/:course_id/modules/:module_id/items/:id`
280    pub async fn delete(&self) -> Result<ModuleItem> {
281        let prefix = self.course_module_prefix()?;
282        let mut item: ModuleItem = self
283            .req()
284            .delete(&format!("{prefix}/items/{}", self.id), &[])
285            .await?;
286        self.propagate(&mut item);
287        Ok(item)
288    }
289
290    /// Mark this module item as complete.
291    ///
292    /// # Canvas API
293    /// `PUT /api/v1/courses/:course_id/modules/:module_id/items/:id/done`
294    pub async fn complete(&self) -> Result<ModuleItem> {
295        let prefix = self.course_module_prefix()?;
296        let mut item: ModuleItem = self
297            .req()
298            .put(&format!("{prefix}/items/{}/done", self.id), &[])
299            .await?;
300        self.propagate(&mut item);
301        Ok(item)
302    }
303
304    /// Mark this module item as incomplete.
305    ///
306    /// # Canvas API
307    /// `DELETE /api/v1/courses/:course_id/modules/:module_id/items/:id/done`
308    pub async fn uncomplete(&self) -> Result<ModuleItem> {
309        let prefix = self.course_module_prefix()?;
310        let mut item: ModuleItem = self
311            .req()
312            .delete(&format!("{prefix}/items/{}/done", self.id), &[])
313            .await?;
314        self.propagate(&mut item);
315        Ok(item)
316    }
317}
318
319/// Completion requirement for a module item.
320#[derive(Debug, Clone, Deserialize, Serialize)]
321pub struct CompletionRequirement {
322    #[serde(rename = "type")]
323    pub requirement_type: Option<String>,
324    pub min_score: Option<f64>,
325    pub completed: Option<bool>,
326}