shared/domain/
resource.rs

1//! Types for Resources.
2use chrono::{DateTime, Utc};
3use macros::make_path_parts;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7pub mod curation;
8
9pub mod report;
10pub use report::{ReportId, ResourceReport};
11
12use crate::api::endpoints::PathPart;
13use crate::domain::UpdateNonNullable;
14
15use super::{
16    additional_resource::AdditionalResource,
17    asset::{DraftOrLive, OrderBy, PrivacyLevel, UserOrMe},
18    category::CategoryId,
19    meta::{AffiliationId, AgeRangeId, ResourceTypeId},
20    module::LiteModule,
21    user::UserId,
22};
23
24wrap_uuid! {
25    /// Wrapper type around [`Uuid`], represents the ID of a Resource.
26    pub struct ResourceId
27}
28
29make_path_parts!(ResourceCreatePath => "/v1/resource");
30
31/// The response returned when a request for `GET`ing a resource is successful.
32
33#[derive(Serialize, Deserialize, Debug, Clone)]
34#[serde(rename_all = "camelCase")]
35pub struct ResourceResponse {
36    /// The ID of the Resource.
37    pub id: ResourceId,
38
39    /// When (if at all) the Resource has published a draft to live.
40    pub published_at: Option<DateTime<Utc>>,
41
42    /// The ID of the Resource's original creator ([`None`] if unknown).
43    pub creator_id: Option<UserId>,
44
45    /// The current author
46    pub author_id: Option<UserId>,
47
48    /// The author's name, as "{given_name} {family_name}".
49    pub author_name: Option<String>,
50
51    /// Number of likes on Resource
52    pub likes: i64,
53
54    /// Number of views for a Resource
55    pub views: i64,
56
57    /// Live is current to Draft
58    pub live_up_to_date: bool,
59
60    /// Liked by current user.
61    pub is_liked: bool,
62
63    /// The data of the requested Resource.
64    pub resource_data: ResourceData,
65
66    /// Admin data for Resource
67    pub admin_data: ResourceAdminData,
68}
69
70/// The over-the-wire representation of a Resource's data. This can either be the live copy or the draft copy.
71#[derive(Serialize, Deserialize, Debug, Clone)]
72#[serde(rename_all = "camelCase")]
73pub struct ResourceData {
74    /// Whether the Resource data is the live copy or the draft.
75    pub draft_or_live: DraftOrLive,
76
77    /// The Resource's name.
78    pub display_name: String,
79
80    /// The Resource's remaining modules.
81    ///
82    /// NOTE: the first module will always exist and will always be of type cover
83    pub cover: Option<LiteModule>,
84
85    /// This resource's age ranges.
86    pub age_ranges: Vec<AgeRangeId>,
87
88    /// This resource's affiliations.
89    pub affiliations: Vec<AffiliationId>,
90
91    /// The language the resource uses.
92    ///
93    /// NOTE: in the format `en`, `eng`, `en-US`, `eng-US` or `eng-USA`. To be replaced with a struct that enforces this.
94    pub language: String,
95
96    /// The resource's categories.
97    pub categories: Vec<CategoryId>,
98
99    /// Description of the resource.
100    pub description: String,
101
102    /// When the Resource was first created.
103    pub created_at: DateTime<Utc>,
104
105    /// When the resource was last edited
106    pub last_edited: Option<DateTime<Utc>>,
107
108    /// The privacy level on the Resource.
109    pub privacy_level: PrivacyLevel,
110
111    /// Lock this resource
112    pub locked: bool,
113
114    /// Other keywords used to searched for resources
115    pub other_keywords: String,
116
117    /// translated keywords used to searched for resources
118    pub translated_keywords: String,
119
120    /// translated descriptions
121    #[serde(default)]
122    pub translated_description: HashMap<String, String>,
123
124    /// Additional resources of this Resource.
125    pub additional_resources: Vec<AdditionalResource>,
126}
127
128/// Request to create a new Resource.
129///
130/// This creates the draft and live [ResourceData](ResourceData) copies with the requested info.
131#[derive(Serialize, Deserialize, Debug, Default)]
132#[serde(rename_all = "camelCase")]
133pub struct ResourceCreateRequest {
134    /// The Resource's name.
135    #[serde(default)]
136    pub display_name: String,
137
138    /// Description of the Resource. Defaults to empty string.
139    #[serde(default)]
140    pub description: String,
141
142    /// This Resource's age ranges.
143    #[serde(default)]
144    #[serde(skip_serializing_if = "Vec::is_empty")]
145    pub age_ranges: Vec<AgeRangeId>,
146
147    /// This Resource's affiliations.
148    #[serde(default)]
149    #[serde(skip_serializing_if = "Vec::is_empty")]
150    pub affiliations: Vec<AffiliationId>,
151
152    /// The language the Resource uses.
153    ///
154    /// If None, uses the user's language.
155    ///
156    /// NOTE: in the format `en`, `eng`, `en-US`, `eng-US` or `eng-USA`. To be replaced with a struct that enforces this.
157    #[serde(default)]
158    pub language: String,
159
160    /// The Resource's categories.
161    #[serde(default)]
162    #[serde(skip_serializing_if = "Vec::is_empty")]
163    pub categories: Vec<CategoryId>,
164}
165
166make_path_parts!(ResourceGetLivePath => "/v1/resource/{}/live" => ResourceId);
167
168make_path_parts!(ResourceGetDraftPath => "/v1/resource/{}/draft" => ResourceId);
169
170make_path_parts!(ResourceUpdateDraftDataPath => "/v1/resource/{}" => ResourceId);
171
172/// Request for updating a Resource's draft data.
173#[derive(Serialize, Deserialize, Debug, Default)]
174#[serde(rename_all = "camelCase")]
175pub struct ResourceUpdateDraftDataRequest {
176    /// The Resource's name.
177    #[serde(default)]
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub display_name: Option<String>,
180
181    /// The language the Resource uses.
182    ///
183    /// NOTE: in the format `en`, `eng`, `en-US`, `eng-US` or `eng-USA`. To be replaced with a struct that enforces this.
184    #[serde(default)]
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub language: Option<String>,
187
188    /// The Resource's categories.
189    #[serde(default)]
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub categories: Option<Vec<CategoryId>>,
192
193    /// The Resource's age ranges.
194    #[serde(default)]
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub age_ranges: Option<Vec<AgeRangeId>>,
197
198    /// The Resource's affiliations.
199    #[serde(default)]
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub affiliations: Option<Vec<AffiliationId>>,
202
203    /// The current author
204    #[serde(default)]
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub author_id: Option<UserId>,
207
208    /// Description of the Resource.
209    #[serde(default)]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub description: Option<String>,
212
213    /// Privacy level for the Resource.
214    #[serde(default)]
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub privacy_level: Option<PrivacyLevel>,
217
218    /// Additional keywords for searches
219    #[serde(default)]
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub other_keywords: Option<String>,
222}
223
224make_path_parts!(ResourcePublishPath => "/v1/resource/{}/draft/publish" => ResourceId);
225
226make_path_parts!(ResourceBrowsePath => "/v1/resource/browse");
227
228/// Query for [`Browse`](crate::api::endpoints::Resource::Browse).
229#[derive(Serialize, Deserialize, Clone, Debug, Default)]
230#[serde(rename_all = "camelCase")]
231pub struct ResourceBrowseQuery {
232    /// Optionally filter by `is_published`
233    #[serde(default)]
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub is_published: Option<bool>,
236
237    /// Optionally filter by author id.
238    #[serde(default)]
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub author_id: Option<UserOrMe>,
241
242    /// The page number of the Resources to get.
243    #[serde(default)]
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub page: Option<u32>,
246
247    /// Optionally browse by draft or live.
248    #[serde(default)]
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub draft_or_live: Option<DraftOrLive>,
251
252    /// Optionally filter Resource by their privacy level
253    #[serde(default)]
254    #[serde(deserialize_with = "super::from_csv")]
255    #[serde(skip_serializing_if = "Vec::is_empty")]
256    pub privacy_level: Vec<PrivacyLevel>,
257
258    /// Optionally filter Resource by blocked status
259    #[serde(default)]
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub blocked: Option<bool>,
262
263    /// The hits per page to be returned
264    #[serde(default)]
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub page_limit: Option<u32>,
267
268    /// Optionally filter by `additional resources`
269    #[serde(default)]
270    #[serde(serialize_with = "super::csv_encode_uuids")]
271    #[serde(deserialize_with = "super::from_csv")]
272    #[serde(skip_serializing_if = "Vec::is_empty")]
273    pub resource_types: Vec<ResourceTypeId>,
274
275    /// The hits per page to be returned
276    #[serde(default)]
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub order_by: Option<OrderBy>,
279}
280
281/// Response for [`Browse`](crate::api::endpoints::Resource::Browse).
282#[derive(Serialize, Deserialize, Clone, Debug)]
283#[serde(rename_all = "camelCase")]
284pub struct ResourceBrowseResponse {
285    /// the Resources returned.
286    pub resources: Vec<ResourceResponse>,
287
288    /// The number of pages found.
289    pub pages: u32,
290
291    /// The total number of Resources found
292    pub total_resource_count: u64,
293}
294
295make_path_parts!(ResourceSearchPath => "/v1/resource");
296
297/// All id's associated with a Resource to delete
298pub struct DeleteUserResources {
299    /// Resource ID to delete.
300    pub resource_id: ResourceId,
301}
302
303/// Search for Resources via the given query string.
304#[derive(Serialize, Deserialize, Clone, Debug, Default)]
305#[serde(rename_all = "camelCase")]
306pub struct ResourceSearchQuery {
307    /// The query string.
308    #[serde(default)]
309    #[serde(skip_serializing_if = "String::is_empty")]
310    pub q: String,
311
312    /// The page number of the Resources to get.
313    #[serde(default)]
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub page: Option<u32>,
316
317    /// Optionally filter by `language`
318    #[serde(default)]
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub language: Option<String>,
321
322    /// Optionally filter by `age_ranges`
323    ///
324    /// Note: Currently does nothing
325    #[serde(default)]
326    #[serde(serialize_with = "super::csv_encode_uuids")]
327    #[serde(deserialize_with = "super::from_csv")]
328    #[serde(skip_serializing_if = "Vec::is_empty")]
329    pub age_ranges: Vec<AgeRangeId>,
330
331    /// Optionally filter by `affiliations`
332    ///
333    /// Note: Currently does nothing
334    #[serde(default)]
335    #[serde(serialize_with = "super::csv_encode_uuids")]
336    #[serde(deserialize_with = "super::from_csv")]
337    #[serde(skip_serializing_if = "Vec::is_empty")]
338    pub affiliations: Vec<AffiliationId>,
339
340    /// Optionally filter by `additional resources`
341    #[serde(default)]
342    #[serde(serialize_with = "super::csv_encode_uuids")]
343    #[serde(deserialize_with = "super::from_csv")]
344    #[serde(skip_serializing_if = "Vec::is_empty")]
345    pub resource_types: Vec<ResourceTypeId>,
346
347    /// Optionally filter by `categories`
348    #[serde(default)]
349    #[serde(serialize_with = "super::csv_encode_uuids")]
350    #[serde(deserialize_with = "super::from_csv")]
351    #[serde(skip_serializing_if = "Vec::is_empty")]
352    pub categories: Vec<CategoryId>,
353
354    /// Optionally filter by `is_published`. This means that the Resource's `publish_at < now()`.
355    #[serde(default)]
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub is_published: Option<bool>,
358
359    /// Optionally filter by author's id
360    #[serde(default)]
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub author_id: Option<UserOrMe>,
363
364    /// Optionally filter by the author's name
365    #[serde(default)]
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub author_name: Option<String>,
368
369    /// Optionally search for Resources using keywords
370    #[serde(default)]
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub other_keywords: Option<String>,
373
374    /// Optionally search for Resources using translated keyword
375    #[serde(default)]
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub translated_keywords: Option<String>,
378
379    /// Optionally search for Resources by privacy level
380    #[serde(default)]
381    #[serde(deserialize_with = "super::from_csv")]
382    #[serde(skip_serializing_if = "Vec::is_empty")]
383    pub privacy_level: Vec<PrivacyLevel>,
384
385    /// Optionally search for blocked or non-blocked Resources
386    #[serde(default)]
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub blocked: Option<bool>,
389
390    /// The hits per page to be returned
391    #[serde(default)]
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub page_limit: Option<u32>,
394
395    /// Optionally filter resources based off of existence of rating
396    #[serde(default)]
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub is_rated: Option<bool>,
399}
400
401/// Response for successful search.
402#[derive(Serialize, Deserialize, Clone, Debug)]
403#[serde(rename_all = "camelCase")]
404pub struct ResourceSearchResponse {
405    /// the resources returned.
406    pub resources: Vec<ResourceResponse>,
407
408    /// The number of pages found.
409    pub pages: u32,
410
411    /// The total number of resources found
412    pub total_resource_count: u64,
413}
414
415/// Response for successfully finding the draft of a resource.
416#[derive(Serialize, Deserialize, Clone, Debug)]
417#[serde(rename_all = "camelCase")]
418pub struct ResourceIdResponse {
419    /// The ID of the resource
420    pub id: ResourceId,
421}
422
423make_path_parts!(ResourceClonePath => "/v1/resource/{}/clone" => ResourceId);
424
425make_path_parts!(ResourceDeletePath => "/v1/resource/{}" => ResourceId);
426
427make_path_parts!(ResourceDeleteAllPath => "/v1/resource");
428
429make_path_parts!(ResourceCoverPath => "/v1/resource/{}/cover" => ResourceId);
430
431make_path_parts!(ResourceCountPath => "/v1/resource/count");
432
433/// Response for total count of public and published resource.
434#[derive(Serialize, Deserialize, Clone, Debug)]
435#[serde(rename_all = "camelCase")]
436pub struct ResourceCountResponse {
437    /// Total number of public and published resources.
438    pub total_count: u64,
439}
440
441/// Response for whether a user has liked a Resource.
442#[derive(Serialize, Deserialize, Clone, Debug)]
443pub struct ResourceLikedResponse {
444    /// Whether the authenticated user has liked the current Resource
445    pub is_liked: bool,
446}
447
448/// These fields can be edited by admin and can be viewed by everyone
449#[derive(Serialize, Deserialize, Clone, Debug)]
450#[serde(rename_all = "camelCase")]
451pub struct ResourceAdminData {
452    /// Rating for resource, weighted for resource search
453    #[serde(default)]
454    pub rating: Option<ResourceRating>,
455
456    /// if true does not appear in search
457    pub blocked: bool,
458
459    /// Indicates resource has been curated by admin
460    pub curated: bool,
461
462    /// Whether the resource is a premium resource
463    pub premium: bool,
464}
465
466/// These fields can be edited by admin and can be viewed by everyone
467#[derive(Serialize, Deserialize, Clone, Debug, Default)]
468#[serde(rename_all = "camelCase")]
469pub struct ResourceUpdateAdminDataRequest {
470    /// Rating for resource, weighted for resource search
471    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
472    pub rating: UpdateNonNullable<ResourceRating>,
473
474    /// if true does not appear in search
475    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
476    pub blocked: UpdateNonNullable<bool>,
477
478    /// Indicates resource has been curated by admin
479    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
480    pub curated: UpdateNonNullable<bool>,
481
482    /// Indicates resource is premium content
483    #[serde(default, skip_serializing_if = "UpdateNonNullable::is_keep")]
484    pub premium: UpdateNonNullable<bool>,
485}
486
487/// Admin rating for Resource
488#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
489#[cfg_attr(feature = "backend", derive(sqlx::Type))]
490#[serde(rename_all = "camelCase")]
491#[repr(i16)]
492pub enum ResourceRating {
493    #[allow(missing_docs)]
494    One = 1,
495    #[allow(missing_docs)]
496    Two = 2,
497    #[allow(missing_docs)]
498    Three = 3,
499}
500
501impl TryFrom<u8> for ResourceRating {
502    type Error = ();
503
504    fn try_from(num: u8) -> Result<Self, Self::Error> {
505        match num {
506            1 => Ok(Self::One),
507            2 => Ok(Self::Two),
508            3 => Ok(Self::Three),
509            _ => Err(()),
510        }
511    }
512}
513
514make_path_parts!(ResourceLikePath => "/v1/resource/{}/like" => ResourceId);
515
516make_path_parts!(ResourceUnlikePath => "/v1/resource/{}/unlike" => ResourceId);
517
518make_path_parts!(ResourceLikedPath => "/v1/resource/{}/like" => ResourceId);
519
520make_path_parts!(ListLikedPath => "/v1/resource/likes");
521
522/// Response for request for list of liked resources.
523#[derive(Serialize, Deserialize, Clone, Debug, Default)]
524#[serde(rename_all = "camelCase")]
525pub struct ListLikedRequest {
526    /// The page number of the resources to get.
527    #[serde(default)]
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub page: Option<u32>,
530
531    /// The hits per page to be returned
532    #[serde(default)]
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub page_limit: Option<u32>,
535}
536/// Response for request for list of liked resources.
537#[derive(Serialize, Deserialize, Clone, Debug)]
538#[serde(rename_all = "camelCase")]
539pub struct ListLikedResponse {
540    /// the resources returned.
541    pub resources: Vec<ResourceResponse>,
542
543    /// The total number of resources liked
544    pub total_resource_count: u64,
545}
546
547make_path_parts!(ResourceViewPath => "/v1/resource/{}/view" => ResourceId);
548
549make_path_parts!(ResourceAdminDataUpdatePath => "/v1/resource/{}/admin" => ResourceId);