shared/domain/
course.rs

1//! Types for Courses.
2use 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    /// Wrapper type around [`Uuid`], represents the ID of a Course.
25    pub struct CourseId
26}
27
28make_path_parts!(CourseCreatePath => "/v1/course");
29
30/// Request to create a new Course.
31///
32/// This creates the draft and live [Course Data](Course Data) copies with the requested info.
33#[derive(Serialize, Deserialize, Debug, Default)]
34#[serde(rename_all = "camelCase")]
35pub struct CourseCreateRequest {
36    /// The Course's name.
37    #[serde(default)]
38    pub display_name: String,
39
40    /// Description of the Course. Defaults to empty string.
41    #[serde(default)]
42    pub description: String,
43
44    /// The language the Course uses.
45    ///
46    /// NOTE: in the format `en`, `eng`, `en-US`, `eng-US` or `eng-USA`. To be replaced with a struct that enforces this.
47    #[serde(default)]
48    pub language: String,
49
50    /// The Course's categories.
51    #[serde(skip_serializing_if = "Vec::is_empty")]
52    #[serde(default)]
53    pub categories: Vec<CategoryId>,
54}
55
56/// The over-the-wire representation of a Course's data. This can either be the live copy or the draft copy.
57#[derive(Serialize, Deserialize, Clone, Debug)]
58#[serde(rename_all = "camelCase")]
59pub struct CourseData {
60    /// Whether the Course data is the live copy or the draft.
61    pub draft_or_live: DraftOrLive,
62
63    /// The Course's name.
64    pub display_name: String,
65
66    /// The language the Course uses.
67    ///
68    /// NOTE: in the format `en`, `eng`, `en-US`, `eng-US` or `eng-USA`. To be replaced with a struct that enforces this.
69    pub language: String,
70
71    /// Description of the Course.
72    pub description: String,
73
74    /// When the Course was last edited
75    pub last_edited: Option<DateTime<Utc>>,
76
77    /// Duration of Course
78    pub duration: Option<u32>,
79
80    /// The privacy level on the Course.
81    pub privacy_level: PrivacyLevel,
82
83    /// Other keywords used to searched for Courses
84    pub other_keywords: String,
85
86    /// translated keywords used to searched for Courses
87    pub translated_keywords: String,
88
89    /// translated descriptions
90    #[serde(default)]
91    pub translated_description: HashMap<String, String>,
92
93    /// This Course's cover.
94    pub cover: Option<LiteModule>,
95
96    /// The Course's categories.
97    pub categories: Vec<CategoryId>,
98
99    /// Additional resources of this Course.
100    pub additional_resources: Vec<AdditionalResource>,
101
102    /// List of Course Units within the Course
103    pub units: Vec<CourseUnit>,
104}
105
106/// Admin rating for a course
107#[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/// These fields can be edited by admin and can be viewed by everyone
134#[derive(Serialize, Deserialize, Clone, Debug, Default)]
135#[serde(rename_all = "camelCase")]
136pub struct CourseAdminData {
137    /// Rating for jig, weighted for jig search
138    #[serde(default)]
139    pub rating: Option<CourseRating>,
140
141    /// if true does not appear in search
142    pub blocked: bool,
143
144    /// Indicates jig has been curated by admin
145    pub curated: bool,
146
147    /// Whether the resource is a premium resource
148    pub premium: bool,
149}
150
151/// The response returned when a request for `GET`ing a Course is successful.
152#[derive(Serialize, Deserialize, Debug, Clone)]
153#[serde(rename_all = "camelCase")]
154pub struct CourseResponse {
155    /// The ID of the Course.
156    pub id: CourseId,
157
158    /// When (if at all) the Course has published a draft to live.
159    pub published_at: Option<DateTime<Utc>>,
160
161    /// The ID of the Course's original creator ([`None`] if unknown).
162    pub creator_id: Option<UserId>,
163
164    /// The current author
165    pub author_id: Option<UserId>,
166
167    /// The author's name, as "{given_name} {family_name}".
168    pub author_name: Option<String>,
169
170    /// Number of likes on Course
171    pub likes: i64,
172
173    /// Number of plays Course
174    pub plays: i64,
175
176    /// Live is current to Draft
177    pub live_up_to_date: bool,
178
179    /// The data of the requested Course.
180    pub course_data: CourseData,
181
182    /// Admin data for a course
183    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/// Request for updating a Course's draft data.
195#[derive(Serialize, Deserialize, Debug, Default)]
196#[serde(rename_all = "camelCase")]
197pub struct CourseUpdateDraftDataRequest {
198    /// The Course's name.
199    #[serde(skip_serializing_if = "Option::is_none")]
200    #[serde(default)]
201    pub display_name: Option<String>,
202
203    /// The current author
204    #[serde(skip_serializing_if = "Option::is_none")]
205    #[serde(default)]
206    pub author_id: Option<UserId>,
207
208    /// Description of the Course.
209    #[serde(skip_serializing_if = "Option::is_none")]
210    #[serde(default)]
211    pub description: Option<String>,
212
213    /// Estimated User Duration of the Course.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    #[serde(default)]
216    pub duration: Option<u32>,
217
218    /// The language the Course uses.
219    ///
220    /// NOTE: in the format `en`, `eng`, `en-US`, `eng-US` or `eng-USA`. To be replaced with a struct that enforces this.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    #[serde(default)]
223    pub language: Option<String>,
224
225    /// Privacy level for the Course.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    #[serde(default)]
228    pub privacy_level: Option<PrivacyLevel>,
229
230    /// Additional keywords for searches
231    #[serde(skip_serializing_if = "Option::is_none")]
232    #[serde(default)]
233    pub other_keywords: Option<String>,
234
235    /// The Course's categories.
236    #[serde(skip_serializing_if = "Option::is_none")]
237    #[serde(default)]
238    pub categories: Option<Vec<CategoryId>>,
239
240    /// The Course's units.
241    #[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/// Query for [`Browse`](crate::api::endpoints::course::Browse).
251#[derive(Serialize, Deserialize, Clone, Debug, Default)]
252#[serde(rename_all = "camelCase")]
253pub struct CourseBrowseQuery {
254    /// Optionally filter by `is_published`
255    #[serde(default)]
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub is_published: Option<bool>,
258
259    /// Optionally filter by author id.
260    #[serde(default)]
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub author_id: Option<UserOrMe>,
263
264    /// The page number of the Courses to get.
265    #[serde(default)]
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub page: Option<u32>,
268
269    /// Optionally browse by draft or live.
270    #[serde(default)]
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub draft_or_live: Option<DraftOrLive>,
273
274    /// Optionally filter Course by their privacy level
275    #[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    /// Optionally filter courses by blocked status
281    #[serde(default)]
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub blocked: Option<bool>,
284
285    /// The hits per page to be returned
286    #[serde(default)]
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub page_limit: Option<u32>,
289
290    /// Optionally filter by `additional resources`
291    #[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    /// Order by sort
298    #[serde(default)]
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub order_by: Option<OrderBy>,
301}
302
303/// Response for [`Browse`](crate::api::endpoints::course::Browse).
304#[derive(Serialize, Deserialize, Clone, Debug)]
305#[serde(rename_all = "camelCase")]
306pub struct CourseBrowseResponse {
307    /// the Courses returned.
308    pub courses: Vec<CourseResponse>,
309
310    /// The number of pages found.
311    pub pages: u32,
312
313    /// The total number of Courses found
314    pub total_course_count: u64,
315}
316
317make_path_parts!(CourseSearchPath => "/v1/course");
318
319/// Search for Courses via the given query string.
320#[derive(Serialize, Deserialize, Clone, Debug, Default)]
321#[serde(rename_all = "camelCase")]
322pub struct CourseSearchQuery {
323    /// The query string.
324    #[serde(default)]
325    #[serde(skip_serializing_if = "String::is_empty")]
326    pub q: String,
327
328    /// The page number of the Courses to get.
329    #[serde(default)]
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub page: Option<u32>,
332
333    /// Optionally filter by `language`
334    #[serde(default)]
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub language: Option<String>,
337
338    /// Optionally filter by `is_published`. This means that the Course's `publish_at < now()`.
339    #[serde(default)]
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub is_published: Option<bool>,
342
343    /// Optionally filter by author's id
344    #[serde(default)]
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub author_id: Option<UserOrMe>,
347
348    /// Optionally filter by the author's name
349    #[serde(default)]
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub author_name: Option<String>,
352
353    /// Optionally search for Courses using keywords
354    #[serde(default)]
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub other_keywords: Option<String>,
357
358    /// Optionally search for Courses using translated keyword
359    #[serde(default)]
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub translated_keywords: Option<String>,
362
363    /// Optionally search for Courses by privacy level
364    #[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    /// Optionally search for blocked or non-blocked courses
370    #[serde(default)]
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub blocked: Option<bool>,
373
374    /// The hits per page to be returned
375    #[serde(default)]
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub page_limit: Option<u32>,
378
379    /// Optionally filter by `additional resources`
380    #[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    /// Optionally filter by `categories`
387    #[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/// Response for successful search.
395#[derive(Serialize, Deserialize, Clone, Debug)]
396#[serde(rename_all = "camelCase")]
397pub struct CourseSearchResponse {
398    /// the Courses returned.
399    pub courses: Vec<CourseResponse>,
400
401    /// The number of pages found.
402    pub pages: u32,
403
404    /// The total number of Courses found
405    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/// Sort browse results
413#[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    /// Order Course by play count
419    #[strum(serialize = "PlayCount")]
420    PlayCount = 0,
421}
422
423make_path_parts!(CourseAdminDataUpdatePath => "/v1/course/{}/admin" => CourseId);
424
425/// These fields can be edited by admin and can be viewed by everyone
426#[derive(Serialize, Deserialize, Clone, Debug, Default)]
427#[serde(rename_all = "camelCase")]
428pub struct CourseUpdateAdminDataRequest {
429    /// Rating for jig, weighted for jig search
430    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
431    pub rating: UpdateNonNullable<CourseRating>,
432
433    /// if true does not appear in search
434    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
435    pub blocked: UpdateNonNullable<bool>,
436
437    /// Indicates jig has been curated by admin
438    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
439    pub curated: UpdateNonNullable<bool>,
440
441    /// Indicates jig is premium content
442    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
443    pub premium: UpdateNonNullable<bool>,
444}