headless_lms_utils/
document_schema_processor.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use uuid::Uuid;
6
7static DISALLOWED_BLOCKS_IN_TOP_LEVEL_PAGES: &[&str] = &[
8    "moocfi/exercise",
9    "moocfi/exercise-task",
10    "moocfi/exercises-in-chapter",
11    "moocfi/pages-in-chapter",
12    "moocfi/exercises-in-chapter",
13    "moocfi/chapter-progress",
14];
15
16pub use crate::attributes;
17use crate::prelude::*;
18
19#[macro_export]
20macro_rules! attributes {
21    () => {{
22        serde_json::Map::<String, serde_json::Value>::new()
23    }};
24    ($($name: tt: $value: expr_2021),+ $(,)*) => {{
25        let mut map = serde_json::Map::<String, serde_json::Value>::new();
26        $(map.insert($name.into(), serde_json::json!($value));)*
27        map
28    }};
29}
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
32pub struct GutenbergBlock {
33    #[serde(rename = "clientId")]
34    pub client_id: Uuid,
35    pub name: String,
36    #[serde(rename = "isValid")]
37    pub is_valid: bool,
38    pub attributes: Map<String, Value>,
39    #[serde(rename = "innerBlocks")]
40    pub inner_blocks: Vec<GutenbergBlock>,
41}
42
43impl GutenbergBlock {
44    pub fn paragraph(paragraph: &str) -> Self {
45        Self::block_with_name_and_attributes(
46            "core/paragraph",
47            attributes! {
48              "content": paragraph.to_string(),
49              "dropCap": false
50            },
51        )
52    }
53
54    pub fn empty_block_from_name(name: String) -> Self {
55        GutenbergBlock {
56            client_id: Uuid::new_v4(),
57            name,
58            is_valid: true,
59            attributes: Map::new(),
60            inner_blocks: vec![],
61        }
62    }
63    pub fn block_with_name_and_attributes(name: &str, attributes: Map<String, Value>) -> Self {
64        GutenbergBlock {
65            client_id: Uuid::new_v4(),
66            name: name.to_string(),
67            is_valid: true,
68            attributes,
69            inner_blocks: vec![],
70        }
71    }
72    pub fn block_with_name_attributes_and_inner_blocks(
73        name: &str,
74        attributes: Map<String, Value>,
75        inner_blocks: Vec<GutenbergBlock>,
76    ) -> Self {
77        GutenbergBlock {
78            client_id: Uuid::new_v4(),
79            name: name.to_string(),
80            is_valid: true,
81            attributes,
82            inner_blocks,
83        }
84    }
85    pub fn hero_section(title: &str, sub_title: &str) -> Self {
86        GutenbergBlock::block_with_name_and_attributes(
87            "moocfi/hero-section",
88            attributes! {
89                "title": title,
90                "subtitle": sub_title
91            },
92        )
93    }
94    pub fn landing_page_hero_section(title: &str, sub_title: &str) -> Self {
95        GutenbergBlock::block_with_name_attributes_and_inner_blocks(
96            "moocfi/landing-page-hero-section",
97            attributes! {"title": title},
98            vec![GutenbergBlock::block_with_name_and_attributes(
99                "core/paragraph",
100                attributes! {
101                    "align": "center",
102                    "content": sub_title,
103                    "dropCap": false,
104                    "placeholder": "Insert short description of course..."
105                },
106            )],
107        )
108    }
109    pub fn course_objective_section() -> Self {
110        GutenbergBlock::block_with_name_attributes_and_inner_blocks(
111            "moocfi/course-objective-section",
112            attributes! {
113                "title": "In this course you'll..."
114            },
115            vec![GutenbergBlock::block_with_name_attributes_and_inner_blocks(
116                "core/columns",
117                attributes! {
118                    "isStackedOnMobile": true
119                },
120                vec![
121                    GutenbergBlock::block_with_name_attributes_and_inner_blocks(
122                        "core/column",
123                        attributes! {},
124                        vec![
125                            GutenbergBlock::block_with_name_and_attributes(
126                                "core/heading",
127                                attributes! {
128                                    "textAlign": "center",
129                                    "level": 3,
130                                    "content": "Objective #1",
131                                    "anchor": "objective-1",
132                                },
133                            ),
134                            GutenbergBlock::block_with_name_and_attributes(
135                                "core/paragraph",
136                                attributes! {
137                                    "align": "center",
138                                    "dropCap": false,
139                                    "content": "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..."
140                                },
141                            ),
142                        ],
143                    ),
144                    GutenbergBlock::block_with_name_attributes_and_inner_blocks(
145                        "core/column",
146                        attributes! {},
147                        vec![
148                            GutenbergBlock::block_with_name_and_attributes(
149                                "core/heading",
150                                attributes! {
151                                    "textAlign": "center",
152                                    "level": 3,
153                                    "content": "Objective #2",
154                                    "anchor": "objective-2",
155                                },
156                            ),
157                            GutenbergBlock::block_with_name_and_attributes(
158                                "core/paragraph",
159                                attributes! {
160                                    "align": "center",
161                                    "dropCap": false,
162                                    "content": "There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."
163                                },
164                            ),
165                        ],
166                    ),
167                    GutenbergBlock::block_with_name_attributes_and_inner_blocks(
168                        "core/column",
169                        attributes! {},
170                        vec![
171                            GutenbergBlock::block_with_name_and_attributes(
172                                "core/heading",
173                                attributes! {
174                                    "textAlign": "center",
175                                    "level": 3,
176                                    "content": "Objective #3",
177                                    "anchor": "objective-3",
178                                },
179                            ),
180                            GutenbergBlock::block_with_name_and_attributes(
181                                "core/paragraph",
182                                attributes! {
183                                    "align": "center",
184                                    "dropCap": false,
185                                    "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas a tempor risus. Morbi at sapien."
186                                },
187                            ),
188                        ],
189                    ),
190                ],
191            )],
192        )
193    }
194
195    pub fn with_id(self, id: Uuid) -> Self {
196        Self {
197            client_id: id,
198            ..self
199        }
200    }
201}
202
203pub fn contains_blocks_not_allowed_in_top_level_pages(input: &[GutenbergBlock]) -> bool {
204    input
205        .iter()
206        .any(|block| DISALLOWED_BLOCKS_IN_TOP_LEVEL_PAGES.contains(&block.name.as_str()))
207}
208
209pub fn remap_ids_in_content(
210    content: &serde_json::Value,
211    chaged_ids: HashMap<Uuid, Uuid>,
212) -> UtilResult<serde_json::Value> {
213    // naive implementation for now because the structure of the content was not decided at the time of writing this.
214    // In the future we could only edit the necessary fields.
215    let mut content_str = serde_json::to_string(content)?;
216    for (k, v) in chaged_ids.into_iter() {
217        content_str = content_str.replace(&k.to_string(), &v.to_string());
218    }
219    Ok(serde_json::from_str(&content_str)?)
220}
221
222/** Removes the private spec from exercise tasks. */
223pub fn remove_sensitive_attributes(input: Vec<GutenbergBlock>) -> Vec<GutenbergBlock> {
224    input
225        .into_iter()
226        .map(|mut block| {
227            if block.name == "moocfi/exercise-task" {
228                block.attributes = Map::new();
229            }
230            block.inner_blocks = remove_sensitive_attributes(block.inner_blocks);
231            block
232        })
233        .collect()
234}