1use crate::domain::UpdateNonNullable;
3use chrono::{DateTime, Utc};
4use macros::make_path_parts;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use strum_macros::Display;
8
9use self::unit::{CourseUnit, CourseUnitId};
10
11use super::{
12 super::api::endpoints::PathPart,
13 additional_resource::AdditionalResource,
14 asset::{DraftOrLive, PrivacyLevel, UserOrMe},
15 category::CategoryId,
16 meta::ResourceTypeId,
17 module::LiteModule,
18 user::UserId,
19};
20
21pub mod unit;
22
23wrap_uuid! {
24 pub struct CourseId
26}
27
28make_path_parts!(CourseCreatePath => "/v1/course");
29
30#[derive(Serialize, Deserialize, Debug, Default)]
34#[serde(rename_all = "camelCase")]
35pub struct CourseCreateRequest {
36 #[serde(default)]
38 pub display_name: String,
39
40 #[serde(default)]
42 pub description: String,
43
44 #[serde(default)]
48 pub language: String,
49
50 #[serde(skip_serializing_if = "Vec::is_empty")]
52 #[serde(default)]
53 pub categories: Vec<CategoryId>,
54}
55
56#[derive(Serialize, Deserialize, Clone, Debug)]
58#[serde(rename_all = "camelCase")]
59pub struct CourseData {
60 pub draft_or_live: DraftOrLive,
62
63 pub display_name: String,
65
66 pub language: String,
70
71 pub description: String,
73
74 pub last_edited: Option<DateTime<Utc>>,
76
77 pub duration: Option<u32>,
79
80 pub privacy_level: PrivacyLevel,
82
83 pub other_keywords: String,
85
86 pub translated_keywords: String,
88
89 #[serde(default)]
91 pub translated_description: HashMap<String, String>,
92
93 pub cover: Option<LiteModule>,
95
96 pub categories: Vec<CategoryId>,
98
99 pub additional_resources: Vec<AdditionalResource>,
101
102 pub units: Vec<CourseUnit>,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
108#[cfg_attr(feature = "backend", derive(sqlx::Type))]
109#[serde(rename_all = "camelCase")]
110#[repr(i16)]
111pub enum CourseRating {
112 #[allow(missing_docs)]
113 One = 1,
114 #[allow(missing_docs)]
115 Two = 2,
116 #[allow(missing_docs)]
117 Three = 3,
118}
119
120impl TryFrom<u8> for CourseRating {
121 type Error = ();
122
123 fn try_from(num: u8) -> Result<Self, Self::Error> {
124 match num {
125 1 => Ok(Self::One),
126 2 => Ok(Self::Two),
127 3 => Ok(Self::Three),
128 _ => Err(()),
129 }
130 }
131}
132
133#[derive(Serialize, Deserialize, Clone, Debug, Default)]
135#[serde(rename_all = "camelCase")]
136pub struct CourseAdminData {
137 #[serde(default)]
139 pub rating: Option<CourseRating>,
140
141 pub blocked: bool,
143
144 pub curated: bool,
146
147 pub premium: bool,
149}
150
151#[derive(Serialize, Deserialize, Debug, Clone)]
153#[serde(rename_all = "camelCase")]
154pub struct CourseResponse {
155 pub id: CourseId,
157
158 pub published_at: Option<DateTime<Utc>>,
160
161 pub creator_id: Option<UserId>,
163
164 pub author_id: Option<UserId>,
166
167 pub author_name: Option<String>,
169
170 pub likes: i64,
172
173 pub plays: i64,
175
176 pub live_up_to_date: bool,
178
179 pub course_data: CourseData,
181
182 pub admin_data: CourseAdminData,
184}
185
186make_path_parts!(CourseGetLivePath => "/v1/course/{}/live" => CourseId);
187
188make_path_parts!(CourseGetDraftPath => "/v1/course/{}/draft" => CourseId);
189
190make_path_parts!(CourseUpdateDraftDataPath => "/v1/course/{}" => CourseId);
191
192make_path_parts!(CourseClonePath => "/v1/course/{}/clone" => CourseId);
193
194#[derive(Serialize, Deserialize, Debug, Default)]
196#[serde(rename_all = "camelCase")]
197pub struct CourseUpdateDraftDataRequest {
198 #[serde(skip_serializing_if = "Option::is_none")]
200 #[serde(default)]
201 pub display_name: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 #[serde(default)]
206 pub author_id: Option<UserId>,
207
208 #[serde(skip_serializing_if = "Option::is_none")]
210 #[serde(default)]
211 pub description: Option<String>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
215 #[serde(default)]
216 pub duration: Option<u32>,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
222 #[serde(default)]
223 pub language: Option<String>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
227 #[serde(default)]
228 pub privacy_level: Option<PrivacyLevel>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 #[serde(default)]
233 pub other_keywords: Option<String>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 #[serde(default)]
238 pub categories: Option<Vec<CategoryId>>,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
242 #[serde(default)]
243 pub units: Option<Vec<CourseUnitId>>,
244}
245
246make_path_parts!(CoursePublishPath => "/v1/course/{}/draft/publish" => CourseId);
247
248make_path_parts!(CourseBrowsePath => "/v1/course/browse");
249
250#[derive(Serialize, Deserialize, Clone, Debug, Default)]
252#[serde(rename_all = "camelCase")]
253pub struct CourseBrowseQuery {
254 #[serde(default)]
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub is_published: Option<bool>,
258
259 #[serde(default)]
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub author_id: Option<UserOrMe>,
263
264 #[serde(default)]
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub page: Option<u32>,
268
269 #[serde(default)]
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub draft_or_live: Option<DraftOrLive>,
273
274 #[serde(default)]
276 #[serde(deserialize_with = "super::from_csv")]
277 #[serde(skip_serializing_if = "Vec::is_empty")]
278 pub privacy_level: Vec<PrivacyLevel>,
279
280 #[serde(default)]
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub blocked: Option<bool>,
284
285 #[serde(default)]
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub page_limit: Option<u32>,
289
290 #[serde(default)]
292 #[serde(serialize_with = "super::csv_encode_uuids")]
293 #[serde(deserialize_with = "super::from_csv")]
294 #[serde(skip_serializing_if = "Vec::is_empty")]
295 pub resource_types: Vec<ResourceTypeId>,
296
297 #[serde(default)]
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub order_by: Option<OrderBy>,
301}
302
303#[derive(Serialize, Deserialize, Clone, Debug)]
305#[serde(rename_all = "camelCase")]
306pub struct CourseBrowseResponse {
307 pub courses: Vec<CourseResponse>,
309
310 pub pages: u32,
312
313 pub total_course_count: u64,
315}
316
317make_path_parts!(CourseSearchPath => "/v1/course");
318
319#[derive(Serialize, Deserialize, Clone, Debug, Default)]
321#[serde(rename_all = "camelCase")]
322pub struct CourseSearchQuery {
323 #[serde(default)]
325 #[serde(skip_serializing_if = "String::is_empty")]
326 pub q: String,
327
328 #[serde(default)]
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub page: Option<u32>,
332
333 #[serde(default)]
335 #[serde(skip_serializing_if = "Option::is_none")]
336 pub language: Option<String>,
337
338 #[serde(default)]
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub is_published: Option<bool>,
342
343 #[serde(default)]
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub author_id: Option<UserOrMe>,
347
348 #[serde(default)]
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub author_name: Option<String>,
352
353 #[serde(default)]
355 #[serde(skip_serializing_if = "Option::is_none")]
356 pub other_keywords: Option<String>,
357
358 #[serde(default)]
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub translated_keywords: Option<String>,
362
363 #[serde(default)]
365 #[serde(deserialize_with = "super::from_csv")]
366 #[serde(skip_serializing_if = "Vec::is_empty")]
367 pub privacy_level: Vec<PrivacyLevel>,
368
369 #[serde(default)]
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub blocked: Option<bool>,
373
374 #[serde(default)]
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub page_limit: Option<u32>,
378
379 #[serde(default)]
381 #[serde(serialize_with = "super::csv_encode_uuids")]
382 #[serde(deserialize_with = "super::from_csv")]
383 #[serde(skip_serializing_if = "Vec::is_empty")]
384 pub resource_types: Vec<ResourceTypeId>,
385
386 #[serde(default)]
388 #[serde(serialize_with = "super::csv_encode_uuids")]
389 #[serde(deserialize_with = "super::from_csv")]
390 #[serde(skip_serializing_if = "Vec::is_empty")]
391 pub categories: Vec<CategoryId>,
392}
393
394#[derive(Serialize, Deserialize, Clone, Debug)]
396#[serde(rename_all = "camelCase")]
397pub struct CourseSearchResponse {
398 pub courses: Vec<CourseResponse>,
400
401 pub pages: u32,
403
404 pub total_course_count: u64,
406}
407
408make_path_parts!(CourseDeletePath => "/v1/course/{}" => CourseId);
409
410make_path_parts!(CoursePlayPath => "/v1/course/{}/play" => CourseId);
411
412#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug, Display)]
414#[cfg_attr(feature = "backend", derive(sqlx::Type))]
415#[serde(rename_all = "camelCase")]
416#[repr(i16)]
417pub enum OrderBy {
418 #[strum(serialize = "PlayCount")]
420 PlayCount = 0,
421}
422
423make_path_parts!(CourseAdminDataUpdatePath => "/v1/course/{}/admin" => CourseId);
424
425#[derive(Serialize, Deserialize, Clone, Debug, Default)]
427#[serde(rename_all = "camelCase")]
428pub struct CourseUpdateAdminDataRequest {
429 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
431 pub rating: UpdateNonNullable<CourseRating>,
432
433 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
435 pub blocked: UpdateNonNullable<bool>,
436
437 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
439 pub curated: UpdateNonNullable<bool>,
440
441 #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
443 pub premium: UpdateNonNullable<bool>,
444}