Skip to main content

canvas_lms_api/resources/
course.rs

1use crate::{
2    error::Result,
3    http::Requester,
4    pagination::PageStream,
5    params::wrap_params,
6    resources::{
7        assignment::Assignment,
8        blueprint::{BlueprintSubscription, BlueprintTemplate},
9        collaboration::Collaboration,
10        content_export::{ContentExport, ContentExportParams},
11        content_migration::{ContentMigration, Migrator},
12        discussion_topic::DiscussionTopic,
13        enrollment::Enrollment,
14        external_tool::{ExternalTool, ExternalToolParams},
15        feature::{Feature, FeatureFlag},
16        file::File,
17        grade_change_log::GradeChangeEvent,
18        gradebook_history::{Day, Grader, SubmissionHistory, SubmissionVersion},
19        grading_period::GradingPeriod,
20        grading_standard::GradingStandard,
21        group::Group,
22        lti_resource_link::{CreateLtiResourceLinkParams, LtiResourceLink},
23        module::Module,
24        outcome::{OutcomeGroup, OutcomeLink, UpdateOutcomeGroupParams},
25        page::Page,
26        params::{
27            assignment_params::CreateAssignmentParams, course_params::UpdateCourseParams,
28            quiz_params::CreateQuizParams,
29        },
30        quiz::Quiz,
31        rubric::{Rubric, RubricAssociation, RubricParams},
32        section::Section,
33        tab::Tab,
34        types::WorkflowState,
35        user::User,
36    },
37};
38use chrono::{DateTime, Utc};
39use serde::{Deserialize, Serialize};
40use std::sync::Arc;
41
42#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
43pub struct Course {
44    pub id: u64,
45    pub name: Option<String>,
46    pub course_code: Option<String>,
47    pub workflow_state: Option<WorkflowState>,
48    pub account_id: Option<u64>,
49    pub root_account_id: Option<u64>,
50    pub enrollment_term_id: Option<u64>,
51    pub sis_course_id: Option<String>,
52    pub start_at: Option<DateTime<Utc>>,
53    pub end_at: Option<DateTime<Utc>>,
54    pub grading_standard_id: Option<u64>,
55    pub is_public: Option<bool>,
56    pub license: Option<String>,
57    pub locale: Option<String>,
58    pub time_zone: Option<String>,
59    pub total_students: Option<u64>,
60    pub default_view: Option<String>,
61    pub syllabus_body: Option<String>,
62    pub public_description: Option<String>,
63    pub hide_final_grades: Option<bool>,
64    pub apply_assignment_group_weights: Option<bool>,
65    pub restrict_enrollments_to_course_dates: Option<bool>,
66
67    #[serde(skip)]
68    pub(crate) requester: Option<Arc<Requester>>,
69}
70
71impl Course {
72    /// Stream all assignments in this course.
73    ///
74    /// # Canvas API
75    /// `GET /api/v1/courses/:course_id/assignments`
76    pub fn get_assignments(&self) -> PageStream<Assignment> {
77        PageStream::new(
78            Arc::clone(self.req()),
79            &format!("courses/{}/assignments", self.id),
80            vec![],
81        )
82    }
83
84    /// Fetch a single assignment.
85    ///
86    /// # Canvas API
87    /// `GET /api/v1/courses/:course_id/assignments/:id`
88    pub async fn get_assignment(&self, assignment_id: u64) -> Result<Assignment> {
89        self.req()
90            .get(
91                &format!("courses/{}/assignments/{assignment_id}", self.id),
92                &[],
93            )
94            .await
95    }
96
97    /// Create a new assignment in this course.
98    ///
99    /// # Canvas API
100    /// `POST /api/v1/courses/:id/assignments`
101    pub async fn create_assignment(&self, params: CreateAssignmentParams) -> Result<Assignment> {
102        let form = wrap_params("assignment", &params);
103        self.req()
104            .post(&format!("courses/{}/assignments", self.id), &form)
105            .await
106    }
107
108    /// Stream all sections in this course.
109    ///
110    /// # Canvas API
111    /// `GET /api/v1/courses/:course_id/sections`
112    pub fn get_sections(&self) -> PageStream<Section> {
113        PageStream::new(
114            Arc::clone(self.req()),
115            &format!("courses/{}/sections", self.id),
116            vec![],
117        )
118    }
119
120    /// Fetch a single section by ID.
121    ///
122    /// # Canvas API
123    /// `GET /api/v1/courses/:id/sections/:section_id`
124    pub async fn get_section(&self, section_id: u64) -> Result<Section> {
125        self.req()
126            .get(&format!("courses/{}/sections/{section_id}", self.id), &[])
127            .await
128    }
129
130    /// Stream all enrollments in this course.
131    ///
132    /// # Canvas API
133    /// `GET /api/v1/courses/:course_id/enrollments`
134    pub fn get_enrollments(&self) -> PageStream<Enrollment> {
135        PageStream::new(
136            Arc::clone(self.req()),
137            &format!("courses/{}/enrollments", self.id),
138            vec![],
139        )
140    }
141
142    /// Stream all users in this course.
143    ///
144    /// # Canvas API
145    /// `GET /api/v1/courses/:course_id/users`
146    pub fn get_users(&self) -> PageStream<User> {
147        PageStream::new(
148            Arc::clone(self.req()),
149            &format!("courses/{}/users", self.id),
150            vec![],
151        )
152    }
153
154    /// Update this course.
155    ///
156    /// # Canvas API
157    /// `PUT /api/v1/courses/:id`
158    pub async fn update(&self, params: UpdateCourseParams) -> Result<Course> {
159        let form = wrap_params("course", &params);
160        let mut course: Course = self
161            .req()
162            .put(&format!("courses/{}", self.id), &form)
163            .await?;
164        course.requester = self.requester.clone();
165        Ok(course)
166    }
167
168    /// Delete this course. Canvas returns the deleted course object.
169    ///
170    /// # Canvas API
171    /// `DELETE /api/v1/courses/:id`
172    pub async fn delete(&self) -> Result<Course> {
173        let params = vec![("event".to_string(), "delete".to_string())];
174        let mut course: Course = self
175            .req()
176            .delete(&format!("courses/{}", self.id), &params)
177            .await?;
178        course.requester = self.requester.clone();
179        Ok(course)
180    }
181
182    /// Stream all quizzes in this course.
183    ///
184    /// # Canvas API
185    /// `GET /api/v1/courses/:id/quizzes`
186    pub fn get_quizzes(&self) -> PageStream<Quiz> {
187        PageStream::new(
188            Arc::clone(self.req()),
189            &format!("courses/{}/quizzes", self.id),
190            vec![],
191        )
192    }
193
194    /// Fetch a single quiz.
195    ///
196    /// # Canvas API
197    /// `GET /api/v1/courses/:id/quizzes/:quiz_id`
198    pub async fn get_quiz(&self, quiz_id: u64) -> Result<Quiz> {
199        self.req()
200            .get(&format!("courses/{}/quizzes/{quiz_id}", self.id), &[])
201            .await
202    }
203
204    /// Create a new quiz in this course.
205    ///
206    /// # Canvas API
207    /// `POST /api/v1/courses/:id/quizzes`
208    pub async fn create_quiz(&self, params: CreateQuizParams) -> Result<Quiz> {
209        let form = wrap_params("quiz", &params);
210        self.req()
211            .post(&format!("courses/{}/quizzes", self.id), &form)
212            .await
213    }
214
215    /// Stream all modules in this course.
216    ///
217    /// # Canvas API
218    /// `GET /api/v1/courses/:id/modules`
219    pub fn get_modules(&self) -> PageStream<Module> {
220        PageStream::new(
221            Arc::clone(self.req()),
222            &format!("courses/{}/modules", self.id),
223            vec![],
224        )
225    }
226
227    /// Fetch a single module.
228    ///
229    /// # Canvas API
230    /// `GET /api/v1/courses/:id/modules/:module_id`
231    pub async fn get_module(&self, module_id: u64) -> Result<Module> {
232        self.req()
233            .get(&format!("courses/{}/modules/{module_id}", self.id), &[])
234            .await
235    }
236
237    /// Stream all pages in this course.
238    ///
239    /// # Canvas API
240    /// `GET /api/v1/courses/:id/pages`
241    pub fn get_pages(&self) -> PageStream<Page> {
242        PageStream::new(
243            Arc::clone(self.req()),
244            &format!("courses/{}/pages", self.id),
245            vec![],
246        )
247    }
248
249    /// Fetch a single page by URL slug or ID.
250    ///
251    /// # Canvas API
252    /// `GET /api/v1/courses/:id/pages/:url_or_id`
253    pub async fn get_page(&self, url_or_id: &str) -> Result<Page> {
254        self.req()
255            .get(&format!("courses/{}/pages/{url_or_id}", self.id), &[])
256            .await
257    }
258
259    /// Stream all discussion topics in this course.
260    ///
261    /// # Canvas API
262    /// `GET /api/v1/courses/:id/discussion_topics`
263    pub fn get_discussion_topics(&self) -> PageStream<DiscussionTopic> {
264        PageStream::new(
265            Arc::clone(self.req()),
266            &format!("courses/{}/discussion_topics", self.id),
267            vec![],
268        )
269    }
270
271    /// Fetch a single discussion topic.
272    ///
273    /// # Canvas API
274    /// `GET /api/v1/courses/:id/discussion_topics/:topic_id`
275    pub async fn get_discussion_topic(&self, topic_id: u64) -> Result<DiscussionTopic> {
276        self.req()
277            .get(
278                &format!("courses/{}/discussion_topics/{topic_id}", self.id),
279                &[],
280            )
281            .await
282    }
283
284    /// Stream all files in this course.
285    ///
286    /// # Canvas API
287    /// `GET /api/v1/courses/:id/files`
288    pub fn get_files(&self) -> PageStream<File> {
289        PageStream::new(
290            Arc::clone(self.req()),
291            &format!("courses/{}/files", self.id),
292            vec![],
293        )
294    }
295
296    /// Stream all tabs in this course.
297    ///
298    /// # Canvas API
299    /// `GET /api/v1/courses/:id/tabs`
300    pub fn get_tabs(&self) -> PageStream<Tab> {
301        PageStream::new(
302            Arc::clone(self.req()),
303            &format!("courses/{}/tabs", self.id),
304            vec![],
305        )
306    }
307
308    /// Stream all collaborations in this course.
309    ///
310    /// # Canvas API
311    /// `GET /api/v1/courses/:id/collaborations`
312    pub fn get_collaborations(&self) -> PageStream<Collaboration> {
313        let course_id = self.id;
314        PageStream::new_with_injector(
315            Arc::clone(self.req()),
316            &format!("courses/{course_id}/collaborations"),
317            vec![],
318            {
319                let req = Arc::clone(self.req());
320                move |mut c: Collaboration, _| {
321                    c.requester = Some(Arc::clone(&req));
322                    c
323                }
324            },
325        )
326    }
327
328    /// Stream all groups in this course.
329    ///
330    /// # Canvas API
331    /// `GET /api/v1/courses/:id/groups`
332    pub fn get_groups(&self) -> PageStream<Group> {
333        let course_id = self.id;
334        PageStream::new_with_injector(
335            Arc::clone(self.req()),
336            &format!("courses/{course_id}/groups"),
337            vec![],
338            |mut g: Group, req| {
339                g.requester = Some(Arc::clone(&req));
340                g
341            },
342        )
343    }
344
345    /// Upload a file to this course.
346    ///
347    /// Canvas uses a two-step upload: first POSTing metadata to obtain an upload URL,
348    /// then POSTing the file as multipart form data to that URL.
349    ///
350    /// # Canvas API
351    /// `POST /api/v1/courses/:id/files`
352    pub async fn upload_file(
353        &self,
354        request: crate::upload::UploadRequest,
355        data: Vec<u8>,
356    ) -> crate::error::Result<crate::resources::file::File> {
357        crate::upload::initiate_and_upload(
358            self.req(),
359            &format!("courses/{}/files", self.id),
360            request,
361            data,
362        )
363        .await
364    }
365
366    // -------------------------------------------------------------------------
367    // External Tools
368    // -------------------------------------------------------------------------
369
370    /// Fetch a single external tool by ID.
371    ///
372    /// # Canvas API
373    /// `GET /api/v1/courses/:course_id/external_tools/:id`
374    pub async fn get_external_tool(&self, tool_id: u64) -> Result<ExternalTool> {
375        let mut tool: ExternalTool = self
376            .req()
377            .get(
378                &format!("courses/{}/external_tools/{tool_id}", self.id),
379                &[],
380            )
381            .await?;
382        tool.requester = self.requester.clone();
383        Ok(tool)
384    }
385
386    /// Stream all external tools for this course.
387    ///
388    /// # Canvas API
389    /// `GET /api/v1/courses/:course_id/external_tools`
390    pub fn get_external_tools(&self) -> PageStream<ExternalTool> {
391        let course_id = self.id;
392        PageStream::new_with_injector(
393            Arc::clone(self.req()),
394            &format!("courses/{course_id}/external_tools"),
395            vec![],
396            |mut t: ExternalTool, req| {
397                t.requester = Some(Arc::clone(&req));
398                t
399            },
400        )
401    }
402
403    /// Create an external tool on this course.
404    ///
405    /// # Canvas API
406    /// `POST /api/v1/courses/:course_id/external_tools`
407    pub async fn create_external_tool(&self, params: ExternalToolParams) -> Result<ExternalTool> {
408        let form = wrap_params("external_tool", &params);
409        let mut tool: ExternalTool = self
410            .req()
411            .post(&format!("courses/{}/external_tools", self.id), &form)
412            .await?;
413        tool.requester = self.requester.clone();
414        Ok(tool)
415    }
416
417    // -------------------------------------------------------------------------
418    // Rubrics
419    // -------------------------------------------------------------------------
420
421    /// Fetch a single rubric by ID.
422    ///
423    /// # Canvas API
424    /// `GET /api/v1/courses/:course_id/rubrics/:id`
425    pub async fn get_rubric(&self, rubric_id: u64) -> Result<Rubric> {
426        let mut rubric: Rubric = self
427            .req()
428            .get(&format!("courses/{}/rubrics/{rubric_id}", self.id), &[])
429            .await?;
430        rubric.requester = self.requester.clone();
431        Ok(rubric)
432    }
433
434    /// Stream all rubrics for this course.
435    ///
436    /// # Canvas API
437    /// `GET /api/v1/courses/:course_id/rubrics`
438    pub fn get_rubrics(&self) -> PageStream<Rubric> {
439        let course_id = self.id;
440        PageStream::new_with_injector(
441            Arc::clone(self.req()),
442            &format!("courses/{course_id}/rubrics"),
443            vec![],
444            |mut r: Rubric, req| {
445                r.requester = Some(Arc::clone(&req));
446                r
447            },
448        )
449    }
450
451    /// Create a rubric in this course.
452    ///
453    /// # Canvas API
454    /// `POST /api/v1/courses/:course_id/rubrics`
455    pub async fn create_rubric(&self, params: RubricParams) -> Result<Rubric> {
456        let form = wrap_params("rubric", &params);
457        let mut rubric: Rubric = self
458            .req()
459            .post(&format!("courses/{}/rubrics", self.id), &form)
460            .await?;
461        rubric.requester = self.requester.clone();
462        Ok(rubric)
463    }
464
465    /// Fetch a single rubric association by ID.
466    ///
467    /// # Canvas API
468    /// `GET /api/v1/courses/:course_id/rubric_associations/:id`
469    pub async fn get_rubric_association(&self, association_id: u64) -> Result<RubricAssociation> {
470        let mut assoc: RubricAssociation = self
471            .req()
472            .get(
473                &format!("courses/{}/rubric_associations/{association_id}", self.id),
474                &[],
475            )
476            .await?;
477        assoc.requester = self.requester.clone();
478        Ok(assoc)
479    }
480
481    /// Stream all rubric associations for this course.
482    ///
483    /// # Canvas API
484    /// `GET /api/v1/courses/:course_id/rubric_associations`
485    pub fn get_rubric_associations(&self) -> PageStream<RubricAssociation> {
486        let course_id = self.id;
487        PageStream::new_with_injector(
488            Arc::clone(self.req()),
489            &format!("courses/{course_id}/rubric_associations"),
490            vec![],
491            |mut a: RubricAssociation, req| {
492                a.requester = Some(Arc::clone(&req));
493                a
494            },
495        )
496    }
497
498    // -------------------------------------------------------------------------
499    // Blueprint
500    // -------------------------------------------------------------------------
501
502    /// Fetch the blueprint template for this course.
503    ///
504    /// `template_id` is typically `"default"` or a numeric ID.
505    ///
506    /// # Canvas API
507    /// `GET /api/v1/courses/:course_id/blueprint_templates/:template_id`
508    pub async fn get_blueprint(&self, template_id: &str) -> Result<BlueprintTemplate> {
509        let mut tmpl: BlueprintTemplate = self
510            .req()
511            .get(
512                &format!("courses/{}/blueprint_templates/{template_id}", self.id),
513                &[],
514            )
515            .await?;
516        tmpl.requester = self.requester.clone();
517        Ok(tmpl)
518    }
519
520    /// Stream blueprint subscriptions for this (child) course.
521    ///
522    /// # Canvas API
523    /// `GET /api/v1/courses/:course_id/blueprint_subscriptions`
524    pub fn get_blueprint_subscriptions(&self) -> PageStream<BlueprintSubscription> {
525        let course_id = self.id;
526        PageStream::new_with_injector(
527            Arc::clone(self.req()),
528            &format!("courses/{course_id}/blueprint_subscriptions"),
529            vec![],
530            |mut s: BlueprintSubscription, req| {
531                s.requester = Some(Arc::clone(&req));
532                s
533            },
534        )
535    }
536
537    // -------------------------------------------------------------------------
538    // Content Migrations
539    // -------------------------------------------------------------------------
540
541    /// Fetch a single content migration by ID.
542    ///
543    /// # Canvas API
544    /// `GET /api/v1/courses/:course_id/content_migrations/:id`
545    pub async fn get_content_migration(&self, migration_id: u64) -> Result<ContentMigration> {
546        let mut migration: ContentMigration = self
547            .req()
548            .get(
549                &format!("courses/{}/content_migrations/{migration_id}", self.id),
550                &[],
551            )
552            .await?;
553        migration.requester = self.requester.clone();
554        Ok(migration)
555    }
556
557    /// Stream all content migrations for this course.
558    ///
559    /// # Canvas API
560    /// `GET /api/v1/courses/:course_id/content_migrations`
561    pub fn get_content_migrations(&self) -> PageStream<ContentMigration> {
562        let course_id = self.id;
563        PageStream::new_with_injector(
564            Arc::clone(self.req()),
565            &format!("courses/{course_id}/content_migrations"),
566            vec![],
567            |mut m: ContentMigration, req| {
568                m.requester = Some(Arc::clone(&req));
569                m
570            },
571        )
572    }
573
574    /// Create a content migration for this course.
575    ///
576    /// # Canvas API
577    /// `POST /api/v1/courses/:course_id/content_migrations`
578    pub async fn create_content_migration(
579        &self,
580        migration_type: &str,
581        params: &[(String, String)],
582    ) -> Result<ContentMigration> {
583        let mut form = vec![("migration_type".to_string(), migration_type.to_string())];
584        form.extend_from_slice(params);
585        let mut migration: ContentMigration = self
586            .req()
587            .post(&format!("courses/{}/content_migrations", self.id), &form)
588            .await?;
589        migration.requester = self.requester.clone();
590        Ok(migration)
591    }
592
593    /// Stream available content migration types for this course.
594    ///
595    /// # Canvas API
596    /// `GET /api/v1/courses/:course_id/content_migrations/migrators`
597    pub fn get_migrators(&self) -> PageStream<Migrator> {
598        PageStream::new(
599            Arc::clone(self.req()),
600            &format!("courses/{}/content_migrations/migrators", self.id),
601            vec![],
602        )
603    }
604
605    // -------------------------------------------------------------------------
606    // Outcome Groups
607    // -------------------------------------------------------------------------
608
609    /// Stream all outcome group links for this course.
610    ///
611    /// # Canvas API
612    /// `GET /api/v1/courses/:course_id/outcome_group_links`
613    pub fn get_outcome_group_links(&self) -> PageStream<OutcomeLink> {
614        PageStream::new(
615            Arc::clone(self.req()),
616            &format!("courses/{}/outcome_group_links", self.id),
617            vec![],
618        )
619    }
620
621    /// Fetch a single outcome group by ID.
622    ///
623    /// # Canvas API
624    /// `GET /api/v1/courses/:course_id/outcome_groups/:id`
625    pub async fn get_outcome_group(&self, group_id: u64) -> Result<OutcomeGroup> {
626        let mut group: OutcomeGroup = self
627            .req()
628            .get(
629                &format!("courses/{}/outcome_groups/{group_id}", self.id),
630                &[],
631            )
632            .await?;
633        group.requester = self.requester.clone();
634        Ok(group)
635    }
636
637    /// Create a top-level outcome group on this course.
638    ///
639    /// # Canvas API
640    /// `POST /api/v1/courses/:course_id/outcome_groups`
641    pub async fn create_outcome_group(
642        &self,
643        params: UpdateOutcomeGroupParams,
644    ) -> Result<OutcomeGroup> {
645        let form = wrap_params("outcome_group", &params);
646        let mut group: OutcomeGroup = self
647            .req()
648            .post(&format!("courses/{}/outcome_groups", self.id), &form)
649            .await?;
650        group.requester = self.requester.clone();
651        Ok(group)
652    }
653
654    // -------------------------------------------------------------------------
655    // Gradebook History
656    // -------------------------------------------------------------------------
657
658    /// Stream the days for which there is gradebook history in this course.
659    ///
660    /// # Canvas API
661    /// `GET /api/v1/courses/:course_id/gradebook_history/days`
662    pub fn get_gradebook_history_dates(&self) -> PageStream<Day> {
663        PageStream::new(
664            Arc::clone(self.req()),
665            &format!("courses/{}/gradebook_history/days", self.id),
666            vec![],
667        )
668    }
669
670    /// Stream graders who worked in this course on a given date.
671    ///
672    /// `date` should be formatted as `YYYY-MM-DD`.
673    ///
674    /// # Canvas API
675    /// `GET /api/v1/courses/:course_id/gradebook_history/:date`
676    pub fn get_gradebook_history_details(&self, date: &str) -> PageStream<Grader> {
677        PageStream::new(
678            Arc::clone(self.req()),
679            &format!("courses/{}/gradebook_history/{date}", self.id),
680            vec![],
681        )
682    }
683
684    /// Stream submission versions graded by a specific grader on a specific assignment and date.
685    ///
686    /// # Canvas API
687    /// `GET /api/v1/courses/:course_id/gradebook_history/:date/graders/:grader_id/assignments/:assignment_id/submissions`
688    pub fn get_submission_history(
689        &self,
690        date: &str,
691        grader_id: u64,
692        assignment_id: u64,
693    ) -> PageStream<SubmissionHistory> {
694        PageStream::new(
695            Arc::clone(self.req()),
696            &format!(
697                "courses/{}/gradebook_history/{date}/graders/{grader_id}/assignments/{assignment_id}/submissions",
698                self.id
699            ),
700            vec![],
701        )
702    }
703
704    /// Stream all submission versions (uncollated) for this course.
705    ///
706    /// # Canvas API
707    /// `GET /api/v1/courses/:course_id/gradebook_history/feed`
708    pub fn get_uncollated_submissions(&self) -> PageStream<SubmissionVersion> {
709        PageStream::new(
710            Arc::clone(self.req()),
711            &format!("courses/{}/gradebook_history/feed", self.id),
712            vec![],
713        )
714    }
715
716    // -------------------------------------------------------------------------
717    // New Quizzes (feature = "new-quizzes")
718    // -------------------------------------------------------------------------
719
720    /// Fetch a single New Quiz by its assignment ID.
721    ///
722    /// # Canvas API
723    /// `GET /api/quiz/v1/courses/:course_id/quizzes/:assignment_id`
724    #[cfg(feature = "new-quizzes")]
725    pub async fn get_new_quiz(
726        &self,
727        assignment_id: &str,
728    ) -> Result<crate::resources::new_quiz::NewQuiz> {
729        let mut quiz: crate::resources::new_quiz::NewQuiz = self
730            .req()
731            .nq_get(&format!("courses/{}/quizzes/{assignment_id}", self.id), &[])
732            .await?;
733        quiz.requester = self.requester.clone();
734        quiz.course_id = Some(self.id);
735        Ok(quiz)
736    }
737
738    /// Stream all New Quizzes for this course.
739    ///
740    /// # Canvas API
741    /// `GET /api/quiz/v1/courses/:course_id/quizzes`
742    #[cfg(feature = "new-quizzes")]
743    pub fn get_new_quizzes(&self) -> PageStream<crate::resources::new_quiz::NewQuiz> {
744        let course_id = self.id;
745        PageStream::new_with_injector_nq(
746            Arc::clone(self.req()),
747            &format!("courses/{course_id}/quizzes"),
748            vec![],
749            move |mut q: crate::resources::new_quiz::NewQuiz, req| {
750                q.requester = Some(Arc::clone(&req));
751                q.course_id = Some(course_id);
752                q
753            },
754        )
755    }
756
757    /// Create a New Quiz in this course.
758    ///
759    /// # Canvas API
760    /// `POST /api/quiz/v1/courses/:course_id/quizzes`
761    #[cfg(feature = "new-quizzes")]
762    pub async fn create_new_quiz(
763        &self,
764        params: crate::resources::new_quiz::NewQuizParams,
765    ) -> Result<crate::resources::new_quiz::NewQuiz> {
766        let body = serde_json::to_value(&params).unwrap_or_default();
767        let mut quiz: crate::resources::new_quiz::NewQuiz = self
768            .req()
769            .nq_post(&format!("courses/{}/quizzes", self.id), &body)
770            .await?;
771        quiz.requester = self.requester.clone();
772        quiz.course_id = Some(self.id);
773        Ok(quiz)
774    }
775
776    // -------------------------------------------------------------------------
777    // Grading Periods
778    // -------------------------------------------------------------------------
779
780    /// Stream all grading periods for this course.
781    ///
782    /// # Canvas API
783    /// `GET /api/v1/courses/:course_id/grading_periods`
784    pub fn get_grading_periods(&self) -> PageStream<GradingPeriod> {
785        let course_id = self.id;
786        PageStream::new_with_injector(
787            Arc::clone(self.req()),
788            &format!("courses/{course_id}/grading_periods"),
789            vec![],
790            move |mut gp: GradingPeriod, req| {
791                gp.requester = Some(Arc::clone(&req));
792                gp.course_id = Some(course_id);
793                gp
794            },
795        )
796    }
797
798    // -------------------------------------------------------------------------
799    // Grading Standards
800    // -------------------------------------------------------------------------
801
802    /// Stream all grading standards for this course.
803    ///
804    /// # Canvas API
805    /// `GET /api/v1/courses/:course_id/grading_standards`
806    pub fn get_grading_standards(&self) -> PageStream<GradingStandard> {
807        PageStream::new(
808            Arc::clone(self.req()),
809            &format!("courses/{}/grading_standards", self.id),
810            vec![],
811        )
812    }
813
814    /// Create a grading standard for this course.
815    ///
816    /// # Canvas API
817    /// `POST /api/v1/courses/:course_id/grading_standards`
818    pub async fn create_grading_standard(
819        &self,
820        params: crate::resources::grading_standard::GradingStandardParams,
821    ) -> Result<GradingStandard> {
822        let form = wrap_params("grading_scheme_entry", &params.grading_scheme_entry)
823            .into_iter()
824            .chain([("title".into(), params.title)])
825            .collect::<Vec<_>>();
826        self.req()
827            .post(&format!("courses/{}/grading_standards", self.id), &form)
828            .await
829    }
830
831    // -------------------------------------------------------------------------
832    // Content Exports
833    // -------------------------------------------------------------------------
834
835    /// Fetch a single content export by ID.
836    ///
837    /// # Canvas API
838    /// `GET /api/v1/courses/:course_id/content_exports/:id`
839    pub async fn get_content_export(&self, export_id: u64) -> Result<ContentExport> {
840        self.req()
841            .get(
842                &format!("courses/{}/content_exports/{export_id}", self.id),
843                &[],
844            )
845            .await
846    }
847
848    /// Stream all content exports for this course.
849    ///
850    /// # Canvas API
851    /// `GET /api/v1/courses/:course_id/content_exports`
852    pub fn get_content_exports(&self) -> PageStream<ContentExport> {
853        PageStream::new(
854            Arc::clone(self.req()),
855            &format!("courses/{}/content_exports", self.id),
856            vec![],
857        )
858    }
859
860    /// Create a content export for this course.
861    ///
862    /// # Canvas API
863    /// `POST /api/v1/courses/:course_id/content_exports`
864    pub async fn create_content_export(
865        &self,
866        params: ContentExportParams,
867    ) -> Result<ContentExport> {
868        let form = vec![
869            ("export_type".into(), params.export_type),
870            (
871                "skip_notifications".into(),
872                params.skip_notifications.unwrap_or(false).to_string(),
873            ),
874        ];
875        self.req()
876            .post(&format!("courses/{}/content_exports", self.id), &form)
877            .await
878    }
879
880    // -------------------------------------------------------------------------
881    // Grade Change Log
882    // -------------------------------------------------------------------------
883
884    /// Stream grade change audit events for this course.
885    ///
886    /// The Canvas API wraps the array in `{ "events": [...] }`; `PageStream`
887    /// handles this automatically.
888    ///
889    /// # Canvas API
890    /// `GET /api/v1/audit/grade_change/courses/:course_id`
891    pub fn get_grade_change_events(&self) -> PageStream<GradeChangeEvent> {
892        PageStream::new(
893            Arc::clone(self.req()),
894            &format!("audit/grade_change/courses/{}", self.id),
895            vec![],
896        )
897    }
898
899    // -------------------------------------------------------------------------
900    // Features
901    // -------------------------------------------------------------------------
902
903    /// Stream all feature flags for this course.
904    ///
905    /// # Canvas API
906    /// `GET /api/v1/courses/:course_id/features`
907    pub fn get_features(&self) -> PageStream<Feature> {
908        PageStream::new(
909            Arc::clone(self.req()),
910            &format!("courses/{}/features", self.id),
911            vec![],
912        )
913    }
914
915    /// Fetch a specific feature flag for this course by feature name.
916    ///
917    /// # Canvas API
918    /// `GET /api/v1/courses/:course_id/features/flags/:feature`
919    pub async fn get_feature_flag(&self, feature: &str) -> Result<FeatureFlag> {
920        self.req()
921            .get(
922                &format!("courses/{}/features/flags/{feature}", self.id),
923                &[],
924            )
925            .await
926    }
927
928    /// List all enabled feature names for this course.
929    ///
930    /// # Canvas API
931    /// `GET /api/v1/courses/:course_id/features/enabled`
932    pub async fn get_enabled_features(&self) -> Result<Vec<String>> {
933        self.req()
934            .get(&format!("courses/{}/features/enabled", self.id), &[])
935            .await
936    }
937
938    // -------------------------------------------------------------------------
939    // LTI Resource Links
940    // -------------------------------------------------------------------------
941
942    /// Stream all LTI resource links in this course.
943    ///
944    /// # Canvas API
945    /// `GET /api/v1/courses/:course_id/lti_resource_links`
946    pub fn get_lti_resource_links(&self) -> PageStream<LtiResourceLink> {
947        PageStream::new(
948            Arc::clone(self.req()),
949            &format!("courses/{}/lti_resource_links", self.id),
950            vec![],
951        )
952    }
953
954    /// Fetch a single LTI resource link.
955    ///
956    /// # Canvas API
957    /// `GET /api/v1/courses/:course_id/lti_resource_links/:id`
958    pub async fn get_lti_resource_link(&self, link_id: u64) -> Result<LtiResourceLink> {
959        self.req()
960            .get(
961                &format!("courses/{}/lti_resource_links/{link_id}", self.id),
962                &[],
963            )
964            .await
965    }
966
967    /// Create a new LTI resource link in this course.
968    ///
969    /// # Canvas API
970    /// `POST /api/v1/courses/:course_id/lti_resource_links`
971    pub async fn create_lti_resource_link(
972        &self,
973        params: CreateLtiResourceLinkParams,
974    ) -> Result<LtiResourceLink> {
975        let form = crate::params::flatten_params(&serde_json::to_value(&params).unwrap());
976        self.req()
977            .post(&format!("courses/{}/lti_resource_links", self.id), &form)
978            .await
979    }
980}