canvas_lms_api/resources/
discussion_topic.rs1use 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#[derive(Debug, Default, Clone, Serialize)]
13pub struct UpdateDiscussionParams {
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub title: Option<String>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub message: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub discussion_type: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub published: Option<bool>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub delayed_post_at: Option<DateTime<Utc>>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub locked: Option<bool>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub pinned: Option<bool>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub require_initial_post: Option<bool>,
30}
31
32#[derive(Debug, Default, Clone, Serialize)]
34pub struct PostEntryParams {
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub message: Option<String>,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
41pub struct DiscussionTopic {
42 pub id: u64,
43 pub course_id: Option<u64>,
44 pub title: Option<String>,
45 pub message: Option<String>,
46 pub html_url: Option<String>,
47 pub posted_at: Option<DateTime<Utc>>,
48 pub last_reply_at: Option<DateTime<Utc>>,
49 pub require_initial_post: Option<bool>,
50 pub user_can_see_posts: Option<bool>,
51 pub discussion_subentry_count: Option<u64>,
52 pub read_state: Option<String>,
53 pub unread_count: Option<u64>,
54 pub subscribed: Option<bool>,
55 pub discussion_type: Option<String>,
56 pub published: Option<bool>,
57 pub locked: Option<bool>,
58 pub pinned: Option<bool>,
59 pub locked_for_user: Option<bool>,
60 pub assignment_id: Option<u64>,
61 pub delayed_post_at: Option<DateTime<Utc>>,
62 pub due_at: Option<DateTime<Utc>>,
63
64 #[serde(skip)]
65 pub(crate) requester: Option<Arc<Requester>>,
66 #[serde(skip)]
67 pub course_id_ctx: Option<u64>,
68 #[serde(skip)]
69 pub group_id: Option<u64>,
70}
71
72impl DiscussionTopic {
73 fn parent_prefix(&self) -> Result<String> {
74 if let Some(id) = self.course_id.or(self.course_id_ctx) {
75 Ok(format!("courses/{id}"))
76 } else if let Some(id) = self.group_id {
77 Ok(format!("groups/{id}"))
78 } else {
79 Err(CanvasError::BadRequest {
80 message: "DiscussionTopic has no course_id or group_id".to_string(),
81 errors: vec![],
82 })
83 }
84 }
85
86 fn propagate(&self, topic: &mut DiscussionTopic) {
87 topic.requester = self.requester.clone();
88 topic.course_id_ctx = self.course_id.or(self.course_id_ctx);
89 topic.group_id = self.group_id;
90 }
91
92 fn make_entry(&self, entry: DiscussionEntry) -> DiscussionEntry {
93 let mut e = entry;
94 e.requester = self.requester.clone();
95 e.course_id = self.course_id.or(self.course_id_ctx);
96 e.group_id = self.group_id;
97 e.topic_id = Some(self.id);
98 e
99 }
100
101 pub async fn update(&self, params: UpdateDiscussionParams) -> Result<DiscussionTopic> {
106 let prefix = self.parent_prefix()?;
107 let form = wrap_params("discussion_topic", ¶ms);
108 let mut topic: DiscussionTopic = self
109 .req()
110 .put(&format!("{prefix}/discussion_topics/{}", self.id), &form)
111 .await?;
112 self.propagate(&mut topic);
113 Ok(topic)
114 }
115
116 pub async fn delete(&self) -> Result<()> {
121 let prefix = self.parent_prefix()?;
122 self.req()
123 .delete_void(&format!("{prefix}/discussion_topics/{}", self.id))
124 .await
125 }
126
127 pub async fn post_entry(&self, params: PostEntryParams) -> Result<DiscussionEntry> {
132 let prefix = self.parent_prefix()?;
133 let form = wrap_params("discussion_entry", ¶ms);
134 let entry: DiscussionEntry = self
135 .req()
136 .post(
137 &format!("{prefix}/discussion_topics/{}/entries", self.id),
138 &form,
139 )
140 .await?;
141 Ok(self.make_entry(entry))
142 }
143
144 pub fn get_topic_entries(&self) -> PageStream<DiscussionEntry> {
149 let course_id = self.course_id.or(self.course_id_ctx);
150 let group_id = self.group_id;
151 let topic_id = self.id;
152 let prefix = if let Some(id) = course_id {
153 format!("courses/{id}")
154 } else if let Some(id) = group_id {
155 format!("groups/{id}")
156 } else {
157 String::new()
158 };
159 PageStream::new_with_injector(
160 Arc::clone(self.req()),
161 &format!("{prefix}/discussion_topics/{topic_id}/entries"),
162 vec![],
163 move |mut e: DiscussionEntry, req| {
164 e.requester = Some(Arc::clone(&req));
165 e.course_id = course_id;
166 e.group_id = group_id;
167 e.topic_id = Some(topic_id);
168 e
169 },
170 )
171 }
172
173 pub async fn get_entries(&self, ids: &[u64]) -> Result<Vec<DiscussionEntry>> {
178 let prefix = self.parent_prefix()?;
179 let params: Vec<(String, String)> = ids
180 .iter()
181 .map(|id| ("ids[]".to_string(), id.to_string()))
182 .collect();
183 let entries: Vec<DiscussionEntry> = self
184 .req()
185 .get(
186 &format!("{prefix}/discussion_topics/{}/entry_list", self.id),
187 ¶ms,
188 )
189 .await?;
190 Ok(entries.into_iter().map(|e| self.make_entry(e)).collect())
191 }
192
193 pub async fn mark_as_read(&self) -> Result<()> {
198 let prefix = self.parent_prefix()?;
199 self.req()
200 .put_void(&format!("{prefix}/discussion_topics/{}/read", self.id))
201 .await
202 }
203
204 pub async fn mark_as_unread(&self) -> Result<()> {
209 let prefix = self.parent_prefix()?;
210 self.req()
211 .delete_void(&format!("{prefix}/discussion_topics/{}/read", self.id))
212 .await
213 }
214
215 pub async fn mark_entries_as_read(&self, forced: bool) -> Result<()> {
220 let prefix = self.parent_prefix()?;
221 let params = if forced {
222 vec![("forced_read_state".to_string(), "true".to_string())]
223 } else {
224 vec![]
225 };
226 let _ = params; self.req()
228 .put_void(&format!("{prefix}/discussion_topics/{}/read_all", self.id))
229 .await
230 }
231
232 pub async fn mark_entries_as_unread(&self, forced: bool) -> Result<()> {
237 let prefix = self.parent_prefix()?;
238 let _ = forced;
239 self.req()
240 .delete_void(&format!("{prefix}/discussion_topics/{}/read_all", self.id))
241 .await
242 }
243
244 pub async fn subscribe(&self) -> Result<()> {
249 let prefix = self.parent_prefix()?;
250 self.req()
251 .put_void(&format!(
252 "{prefix}/discussion_topics/{}/subscribed",
253 self.id
254 ))
255 .await
256 }
257
258 pub async fn unsubscribe(&self) -> Result<()> {
263 let prefix = self.parent_prefix()?;
264 self.req()
265 .delete_void(&format!(
266 "{prefix}/discussion_topics/{}/subscribed",
267 self.id
268 ))
269 .await
270 }
271}
272
273#[derive(Debug, Clone, Deserialize, Serialize, canvas_lms_api_derive::CanvasResource)]
275pub struct DiscussionEntry {
276 pub id: u64,
277 pub user_id: Option<u64>,
278 pub discussion_id: Option<u64>,
279 pub parent_id: Option<u64>,
280 pub message: Option<String>,
281 pub created_at: Option<DateTime<Utc>>,
282 pub updated_at: Option<DateTime<Utc>>,
283
284 #[serde(skip)]
285 pub(crate) requester: Option<Arc<Requester>>,
286 #[serde(skip)]
287 pub course_id: Option<u64>,
288 #[serde(skip)]
289 pub group_id: Option<u64>,
290 #[serde(skip)]
291 pub topic_id: Option<u64>,
292}
293
294impl DiscussionEntry {
295 fn parent_prefix(&self) -> Result<String> {
296 if let Some(id) = self.course_id {
297 Ok(format!("courses/{id}"))
298 } else if let Some(id) = self.group_id {
299 Ok(format!("groups/{id}"))
300 } else {
301 Err(CanvasError::BadRequest {
302 message: "DiscussionEntry has no course_id or group_id".to_string(),
303 errors: vec![],
304 })
305 }
306 }
307
308 fn topic_id_or_err(&self) -> Result<u64> {
309 self.topic_id.ok_or_else(|| CanvasError::BadRequest {
310 message: "DiscussionEntry has no topic_id".to_string(),
311 errors: vec![],
312 })
313 }
314
315 fn propagate(&self, entry: &mut DiscussionEntry) {
316 entry.requester = self.requester.clone();
317 entry.course_id = self.course_id;
318 entry.group_id = self.group_id;
319 entry.topic_id = self.topic_id;
320 }
321
322 pub async fn update(&self, message: &str) -> Result<DiscussionEntry> {
327 let prefix = self.parent_prefix()?;
328 let topic_id = self.topic_id_or_err()?;
329 let params = vec![("message".to_string(), message.to_string())];
330 let mut entry: DiscussionEntry = self
331 .req()
332 .put(
333 &format!("{prefix}/discussion_topics/{topic_id}/entries/{}", self.id),
334 ¶ms,
335 )
336 .await?;
337 self.propagate(&mut entry);
338 Ok(entry)
339 }
340
341 pub async fn delete(&self) -> Result<()> {
346 let prefix = self.parent_prefix()?;
347 let topic_id = self.topic_id_or_err()?;
348 self.req()
349 .delete_void(&format!(
350 "{prefix}/discussion_topics/{topic_id}/entries/{}",
351 self.id
352 ))
353 .await
354 }
355
356 pub async fn post_reply(&self, message: &str) -> Result<DiscussionEntry> {
361 let prefix = self.parent_prefix()?;
362 let topic_id = self.topic_id_or_err()?;
363 let params = vec![("message".to_string(), message.to_string())];
364 let mut entry: DiscussionEntry = self
365 .req()
366 .post(
367 &format!(
368 "{prefix}/discussion_topics/{topic_id}/entries/{}/replies",
369 self.id
370 ),
371 ¶ms,
372 )
373 .await?;
374 self.propagate(&mut entry);
375 Ok(entry)
376 }
377
378 pub fn get_replies(&self) -> PageStream<DiscussionEntry> {
383 let course_id = self.course_id;
384 let group_id = self.group_id;
385 let topic_id = self.topic_id.unwrap_or(0);
386 let entry_id = self.id;
387 let prefix = if let Some(id) = course_id {
388 format!("courses/{id}")
389 } else if let Some(id) = group_id {
390 format!("groups/{id}")
391 } else {
392 String::new()
393 };
394 PageStream::new_with_injector(
395 Arc::clone(self.req()),
396 &format!("{prefix}/discussion_topics/{topic_id}/entries/{entry_id}/replies"),
397 vec![],
398 move |mut e: DiscussionEntry, req| {
399 e.requester = Some(Arc::clone(&req));
400 e.course_id = course_id;
401 e.group_id = group_id;
402 e.topic_id = Some(topic_id);
403 e
404 },
405 )
406 }
407
408 pub async fn mark_as_read(&self) -> Result<()> {
413 let prefix = self.parent_prefix()?;
414 let topic_id = self.topic_id_or_err()?;
415 self.req()
416 .put_void(&format!(
417 "{prefix}/discussion_topics/{topic_id}/entries/{}/read",
418 self.id
419 ))
420 .await
421 }
422
423 pub async fn mark_as_unread(&self) -> Result<()> {
428 let prefix = self.parent_prefix()?;
429 let topic_id = self.topic_id_or_err()?;
430 self.req()
431 .delete_void(&format!(
432 "{prefix}/discussion_topics/{topic_id}/entries/{}/read",
433 self.id
434 ))
435 .await
436 }
437
438 pub async fn rate(&self, rating: u8) -> Result<()> {
443 if rating > 1 {
444 return Err(CanvasError::BadRequest {
445 message: "rating must be 0 or 1".to_string(),
446 errors: vec![],
447 });
448 }
449 let prefix = self.parent_prefix()?;
450 let topic_id = self.topic_id_or_err()?;
451 let params = vec![("rating".to_string(), rating.to_string())];
452 self.req()
453 .post_void_with_params(
454 &format!(
455 "{prefix}/discussion_topics/{topic_id}/entries/{}/rating",
456 self.id
457 ),
458 ¶ms,
459 )
460 .await
461 }
462}