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