1use anyhow::{Context, Result};
2use headless_lms_base::config::ApplicationConfiguration;
3use sqlx::{Connection, PgConnection};
4use tracing::log::warn;
5use uuid::Uuid;
6
7use headless_lms_models::{
8 PKeyPolicy, certificate_configuration_to_requirements,
9 certificate_configurations::{self, DatabaseCertificateConfiguration},
10 chatbot_configurations::{self, NewChatbotConf},
11 course_instances::{self, CourseInstance, NewCourseInstance},
12 course_language_groups,
13 course_modules::CourseModule,
14 courses::{self, Course, NewCourse},
15 file_uploads, glossary,
16 library::content_management,
17 pages::{self, NewCoursePage, NewPage},
18 roles::{self, RoleDomain, UserRole},
19};
20use headless_lms_utils::document_schema_processor::GutenbergBlock;
21
22use crate::programs::seed::{
23 builder::{context::SeedContext, module::ModuleBuilder},
24 seed_helpers::get_seed_spec_fetcher,
25};
26
27#[derive(Debug, Clone)]
29pub struct CourseInstanceConfig {
30 pub name: Option<String>,
31 pub description: Option<String>,
32 pub support_email: Option<String>,
33 pub teacher_in_charge_name: String,
34 pub teacher_in_charge_email: String,
35 pub opening_time: Option<chrono::DateTime<chrono::Utc>>,
36 pub closing_time: Option<chrono::DateTime<chrono::Utc>>,
37 pub instance_id: Option<Uuid>,
38}
39
40#[derive(Debug, Clone)]
42pub struct PageConfig {
43 pub url: String,
44 pub title: String,
45 pub page_number: i32,
46 pub is_hidden: bool,
47 pub content: Option<Vec<headless_lms_utils::document_schema_processor::GutenbergBlock>>,
48}
49
50#[derive(Debug, Clone)]
52pub struct GlossaryEntry {
53 pub acronym: String,
54 pub definition: String,
55}
56
57#[derive(Debug, Clone)]
59pub struct CertificateConfig {
60 pub background_svg_path: String,
61 pub certificate_id: Option<Uuid>,
62}
63
64#[derive(Debug, Clone)]
66pub struct CourseBuilder {
67 pub name: String,
68 pub slug: String,
69 pub language_code: String,
70 pub can_add_chatbot: bool,
71 pub description: String,
72 pub default_instance_name: &'static [u8],
73 pub modules: Vec<ModuleBuilder>,
74 pub extra_roles: Vec<(Uuid, UserRole)>,
75 pub course_id: Option<Uuid>,
76 pub instances: Vec<CourseInstanceConfig>,
77 pub chatbot_configs: Vec<NewChatbotConf>,
78 pub pages: Vec<PageConfig>,
79 pub glossary_entries: Vec<GlossaryEntry>,
80 pub certificate_config: Option<CertificateConfig>,
81 pub front_page_content: Option<Vec<GutenbergBlock>>,
82 pub chapter_locking_enabled: bool,
83 pub flagged_answers_skip_manual_review_and_allow_retry: bool,
84 pub flagged_answers_threshold: Option<i32>,
85}
86
87impl CourseBuilder {
88 pub fn new(name: impl Into<String>, slug: impl Into<String>) -> Self {
89 Self {
90 name: name.into(),
91 slug: slug.into(),
92 language_code: "en".into(),
93 can_add_chatbot: false,
94 description: "Sample course.".into(),
95 default_instance_name: b"default-instance",
96 modules: vec![],
97 extra_roles: vec![],
98 course_id: None,
99 instances: vec![],
100 chatbot_configs: vec![],
101 pages: vec![],
102 glossary_entries: vec![],
103 certificate_config: None,
104 front_page_content: None,
105 chapter_locking_enabled: false,
106 flagged_answers_skip_manual_review_and_allow_retry: false,
107 flagged_answers_threshold: None,
108 }
109 }
110
111 pub fn flagged_answers_skip_manual_review_and_allow_retry(mut self, v: bool) -> Self {
113 self.flagged_answers_skip_manual_review_and_allow_retry = v;
114 self
115 }
116
117 pub fn flagged_answers_threshold(mut self, v: Option<i32>) -> Self {
119 self.flagged_answers_threshold = v;
120 self
121 }
122
123 pub fn chatbot(mut self, v: bool) -> Self {
124 self.can_add_chatbot = v;
125 self
126 }
127 pub fn chapter_locking_enabled(mut self, v: bool) -> Self {
128 self.chapter_locking_enabled = v;
129 self
130 }
131 pub fn desc(mut self, d: impl Into<String>) -> Self {
132 self.description = d.into();
133 self
134 }
135 pub fn language(mut self, lc: impl Into<String>) -> Self {
136 self.language_code = lc.into();
137 self
138 }
139 pub fn module(mut self, m: ModuleBuilder) -> Self {
140 self.modules.push(m);
141 self
142 }
143 pub fn modules<I: IntoIterator<Item = ModuleBuilder>>(mut self, it: I) -> Self {
144 self.modules.extend(it);
145 self
146 }
147 pub fn role(mut self, user_id: Uuid, role: UserRole) -> Self {
148 self.extra_roles.push((user_id, role));
149 self
150 }
151 pub fn course_id(mut self, id: Uuid) -> Self {
152 self.course_id = Some(id);
153 self
154 }
155
156 pub fn instance(mut self, config: CourseInstanceConfig) -> Self {
157 self.instances.push(config);
158 self
159 }
160
161 pub fn chatbot_config(mut self, config: NewChatbotConf) -> Self {
162 if !self.can_add_chatbot {
163 warn!("Can't add chatbot to this course!!!!");
164 return self;
165 }
166 self.chatbot_configs.push(config);
167 self
168 }
169
170 pub fn top_level_page(
172 mut self,
173 url: impl Into<String>,
174 title: impl Into<String>,
175 page_number: i32,
176 is_hidden: bool,
177 content: Option<Vec<headless_lms_utils::document_schema_processor::GutenbergBlock>>,
178 ) -> Self {
179 self.pages.push(PageConfig {
180 url: url.into(),
181 title: title.into(),
182 page_number,
183 is_hidden,
184 content,
185 });
186 self
187 }
188
189 pub fn glossary_entry(
190 mut self,
191 acronym: impl Into<String>,
192 definition: impl Into<String>,
193 ) -> Self {
194 self.glossary_entries.push(GlossaryEntry {
195 acronym: acronym.into(),
196 definition: definition.into(),
197 });
198 self
199 }
200
201 pub fn certificate_config(
202 mut self,
203 background_svg_path: impl Into<String>,
204 certificate_id: Option<Uuid>,
205 ) -> Self {
206 self.certificate_config = Some(CertificateConfig {
207 background_svg_path: background_svg_path.into(),
208 certificate_id,
209 });
210 self
211 }
212
213 pub fn front_page_content(mut self, content: Vec<GutenbergBlock>) -> Self {
215 self.front_page_content = Some(content);
216 self
217 }
218
219 pub async fn seed(
221 self,
222 conn: &mut PgConnection,
223 app_config: &ApplicationConfiguration,
224 cx: &SeedContext,
225 ) -> Result<(Course, CourseInstance, CourseModule)> {
226 let course_id = self.course_id.unwrap_or(cx.base_course_ns);
227
228 let new_course = NewCourse {
229 name: self.name.clone(),
230 organization_id: cx.org,
231 slug: self.slug.clone(),
232 language_code: self.language_code.clone(),
233 teacher_in_charge_name: "admin".into(),
234 teacher_in_charge_email: "admin@example.com".into(),
235 description: self.description.clone(),
236 is_draft: false,
237 is_test_mode: false,
238 is_unlisted: false,
239 copy_user_permissions: false,
240 is_joinable_by_code_only: false,
241 join_code: None,
242 ask_marketing_consent: false,
243 flagged_answers_threshold: Some(self.flagged_answers_threshold.unwrap_or(3)),
244 can_add_chatbot: self.can_add_chatbot,
245 };
246
247 let mut tx = conn.begin().await.context("starting transaction")?;
249
250 let course_language_group_id = course_language_groups::insert(
251 &mut tx,
252 headless_lms_models::PKeyPolicy::Generate,
253 &new_course.slug,
254 )
255 .await?;
256
257 let course_id = courses::insert(
258 &mut tx,
259 app_config,
260 headless_lms_models::PKeyPolicy::Fixed(course_id),
261 course_language_group_id,
262 &new_course,
263 )
264 .await
265 .context("inserting course")?;
266
267 let course = courses::get_course(&mut tx, course_id)
268 .await
269 .context("getting course")?;
270
271 for chatbot_conf in self.chatbot_configs {
272 let chatbotconf_id = chatbot_conf.chatbotconf_id.unwrap_or_else(Uuid::new_v4);
273 chatbot_configurations::insert(
274 &mut tx,
275 PKeyPolicy::Fixed(chatbotconf_id),
276 NewChatbotConf {
277 course_id: Some(course_id),
278 ..chatbot_conf
279 },
280 )
281 .await
282 .context("inserting chatbot configuration for course")?;
283 }
284
285 let mut last_module = None;
286
287 for (i, m) in self.modules.into_iter().enumerate() {
288 last_module = Some(m.seed(&mut tx, cx, course.id, i as i32).await?);
289 }
290
291 for (user_id, role) in self.extra_roles {
292 roles::insert(&mut tx, user_id, role, RoleDomain::Course(course.id))
293 .await
294 .context("inserting course role")?;
295 }
296
297 let mut default_instance = None;
299 for instance_config in self.instances {
300 let instance_id = instance_config.instance_id.unwrap_or_else(Uuid::new_v4);
301 let instance = course_instances::insert(
302 &mut tx,
303 PKeyPolicy::Fixed(instance_id),
304 NewCourseInstance {
305 course_id: course.id,
306 name: instance_config.name.as_deref(),
307 description: instance_config.description.as_deref(),
308 support_email: instance_config.support_email.as_deref(),
309 teacher_in_charge_name: &instance_config.teacher_in_charge_name,
310 teacher_in_charge_email: &instance_config.teacher_in_charge_email,
311 opening_time: instance_config.opening_time,
312 closing_time: instance_config.closing_time,
313 },
314 )
315 .await
316 .context("inserting course instance")?;
317
318 if default_instance.is_none() {
320 default_instance = Some(instance);
321 }
322 }
323
324 let default_instance =
325 default_instance.expect("At least one course instance must be provided");
326
327 let front_page_blocks = self
328 .front_page_content
329 .unwrap_or_else(content_management::default_course_front_page_content);
330
331 let course_front_page = NewPage {
332 chapter_id: None,
333 content: front_page_blocks,
334 course_id: Some(course.id),
335 exam_id: None,
336 front_page_of_chapter_id: None,
337 title: course.name.clone(),
338 url_path: String::from("/"),
339 exercises: vec![],
340 exercise_slides: vec![],
341 exercise_tasks: vec![],
342 content_search_language: None,
343 hidden: false,
344 };
345
346 pages::insert_page(
347 &mut tx,
348 course_front_page,
349 cx.teacher,
350 get_seed_spec_fetcher(),
351 crate::domain::models_requests::fetch_service_info,
352 )
353 .await
354 .context("inserting course front page")?;
355
356 for page_config in self.pages {
358 let mut course_page = NewCoursePage::new(
359 course.id,
360 page_config.page_number,
361 &page_config.url,
362 &page_config.title,
363 );
364
365 if page_config.is_hidden {
366 course_page = course_page.set_hidden(true);
367 }
368
369 if let Some(content) = page_config.content {
370 course_page = course_page.set_content(content);
371 }
372
373 pages::insert_course_page(&mut tx, &course_page, cx.teacher)
374 .await
375 .context("inserting course page")?;
376 }
377
378 for glossary_entry in self.glossary_entries {
380 glossary::insert(
381 &mut tx,
382 &glossary_entry.acronym,
383 &glossary_entry.definition,
384 course.id,
385 )
386 .await
387 .context("inserting glossary entry")?;
388 }
389
390 if let Some(cert_config) = self.certificate_config {
392 let background_svg_file_upload_id = file_uploads::insert(
393 &mut tx,
394 &format!(
395 "background-{}.svg",
396 last_module
397 .as_ref()
398 .expect("At least one module must be provided")
399 .id
400 ),
401 &cert_config.background_svg_path,
402 "image/svg+xml",
403 None,
404 None,
405 )
406 .await
407 .context("inserting certificate background file")?;
408
409 let certificate_id = cert_config
410 .certificate_id
411 .unwrap_or_else(|| cx.v5(b"886d3e22-5007-4371-94d7-e0ad93a2391c"));
412 let configuration = DatabaseCertificateConfiguration {
413 id: certificate_id,
414 certificate_owner_name_y_pos: None,
415 certificate_owner_name_x_pos: None,
416 certificate_owner_name_font_size: None,
417 certificate_owner_name_text_color: None,
418 certificate_owner_name_text_anchor: None,
419 certificate_validate_url_y_pos: None,
420 certificate_validate_url_x_pos: None,
421 certificate_validate_url_font_size: None,
422 certificate_validate_url_text_color: None,
423 certificate_validate_url_text_anchor: None,
424 certificate_date_y_pos: None,
425 certificate_date_x_pos: None,
426 certificate_date_font_size: None,
427 certificate_date_text_color: None,
428 certificate_date_text_anchor: None,
429 certificate_locale: None,
430 paper_size: None,
431 background_svg_path: cert_config.background_svg_path,
432 background_svg_file_upload_id,
433 overlay_svg_path: None,
434 overlay_svg_file_upload_id: None,
435 render_certificate_grade: false,
436 certificate_grade_y_pos: None,
437 certificate_grade_x_pos: None,
438 certificate_grade_font_size: None,
439 certificate_grade_text_color: None,
440 certificate_grade_text_anchor: None,
441 };
442 let database_configuration =
443 certificate_configurations::insert(&mut tx, &configuration)
444 .await
445 .context("inserting certificate configuration")?;
446 certificate_configuration_to_requirements::insert(
447 &mut tx,
448 database_configuration.id,
449 Some(
450 last_module
451 .as_ref()
452 .expect("At least one module must be provided")
453 .id,
454 ),
455 )
456 .await
457 .context("linking certificate configuration to requirements")?;
458 }
459 tx.commit().await.context("committing transaction")?;
460
461 courses::set_cheater_detection_enabled(conn, course.id, false)
464 .await
465 .context("disabling cheater detection")?;
466
467 if self.chapter_locking_enabled || self.flagged_answers_skip_manual_review_and_allow_retry {
468 use headless_lms_models::courses::CourseUpdate;
469 let course_update = CourseUpdate {
470 flagged_answers_threshold: self.flagged_answers_threshold.unwrap_or(3),
471 flagged_answers_skip_manual_review_and_allow_retry: self
472 .flagged_answers_skip_manual_review_and_allow_retry,
473 chapter_locking_enabled: self.chapter_locking_enabled,
474 ..CourseUpdate::from(&course)
475 };
476 let updated_course = courses::update_course(
477 conn,
478 app_config,
479 course.id,
480 course_update,
481 cx.teacher,
482 false,
483 )
484 .await
485 .context("updating course")?;
486 return Ok((
487 updated_course,
488 default_instance,
489 last_module.expect("At least one module must be provided"),
490 ));
491 }
492
493 Ok((
494 course,
495 default_instance,
496 last_module.expect("At least one module must be provided"),
497 ))
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use crate::programs::seed::builder::module::ModuleBuilder;
504
505 use super::*;
506 use headless_lms_models::roles::UserRole;
507
508 #[test]
509 fn course_builder_new() {
510 let course = CourseBuilder::new("Test Course", "test-course");
511
512 assert_eq!(course.name, "Test Course");
513 assert_eq!(course.slug, "test-course");
514 assert_eq!(course.language_code, "en");
515 assert!(!course.can_add_chatbot);
516 assert_eq!(course.description, "Sample course.");
517 assert_eq!(course.default_instance_name, b"default-instance");
518 assert!(course.modules.is_empty());
519 assert!(course.extra_roles.is_empty());
520 assert!(course.course_id.is_none());
521 }
522
523 #[test]
524 fn course_builder_string_conversion() {
525 let course1 = CourseBuilder::new("String literal", "string-slug");
526 let course2 = CourseBuilder::new(String::from("Owned string"), String::from("owned-slug"));
527
528 assert_eq!(course1.name, "String literal");
529 assert_eq!(course1.slug, "string-slug");
530 assert_eq!(course2.name, "Owned string");
531 assert_eq!(course2.slug, "owned-slug");
532 }
533
534 #[test]
535 fn course_builder_chatbot() {
536 let course = CourseBuilder::new("Test Course", "test-course").chatbot(true);
537 assert!(course.can_add_chatbot);
538
539 let course_disabled = CourseBuilder::new("Test Course", "test-course").chatbot(false);
540 assert!(!course_disabled.can_add_chatbot);
541 }
542
543 #[test]
544 fn course_builder_desc() {
545 let course =
546 CourseBuilder::new("Test Course", "test-course").desc("A comprehensive test course");
547
548 assert_eq!(course.description, "A comprehensive test course");
549
550 let desc_string = String::from("Another description");
551 let course2 = CourseBuilder::new("Test Course", "test-course").desc(desc_string);
552 assert_eq!(course2.description, "Another description");
553 }
554
555 #[test]
556 fn course_builder_language() {
557 let course = CourseBuilder::new("Test Course", "test-course").language("fi-FI");
558
559 assert_eq!(course.language_code, "fi-FI");
560
561 let lang_string = String::from("sv-SE");
562 let course2 = CourseBuilder::new("Test Course", "test-course").language(lang_string);
563 assert_eq!(course2.language_code, "sv-SE");
564 }
565
566 #[test]
567 fn course_builder_module() {
568 let module = ModuleBuilder::new().order(0);
569 let course = CourseBuilder::new("Test Course", "test-course").module(module);
570
571 assert_eq!(course.modules.len(), 1);
572 assert_eq!(course.modules[0].order, Some(0));
573 }
574
575 #[test]
576 fn course_builder_multiple_modules() {
577 let module1 = ModuleBuilder::new().order(0);
578 let module2 = ModuleBuilder::new().order(1);
579 let course = CourseBuilder::new("Test Course", "test-course")
580 .module(module1)
581 .module(module2);
582
583 assert_eq!(course.modules.len(), 2);
584 assert_eq!(course.modules[0].order, Some(0));
585 assert_eq!(course.modules[1].order, Some(1));
586 }
587
588 #[test]
589 fn course_builder_role() {
590 let user_id = Uuid::new_v4();
591 let course =
592 CourseBuilder::new("Test Course", "test-course").role(user_id, UserRole::Teacher);
593
594 assert_eq!(course.extra_roles.len(), 1);
595 assert_eq!(course.extra_roles[0].0, user_id);
596 assert_eq!(course.extra_roles[0].1, UserRole::Teacher);
597 }
598
599 #[test]
600 fn course_builder_multiple_roles() {
601 let user1_id = Uuid::new_v4();
602 let user2_id = Uuid::new_v4();
603 let course = CourseBuilder::new("Test Course", "test-course")
604 .role(user1_id, UserRole::Teacher)
605 .role(user2_id, UserRole::MaterialViewer);
606
607 assert_eq!(course.extra_roles.len(), 2);
608 assert_eq!(course.extra_roles[0].0, user1_id);
609 assert_eq!(course.extra_roles[0].1, UserRole::Teacher);
610 assert_eq!(course.extra_roles[1].0, user2_id);
611 assert_eq!(course.extra_roles[1].1, UserRole::MaterialViewer);
612 }
613
614 #[test]
615 fn course_builder_course_id() {
616 let course_id = Uuid::new_v4();
617 let course = CourseBuilder::new("Test Course", "test-course").course_id(course_id);
618
619 assert_eq!(course.course_id, Some(course_id));
620 }
621
622 #[test]
623 fn course_builder_fluent_interface() {
624 let user_id = Uuid::new_v4();
625 let course_id = Uuid::new_v4();
626 let module = ModuleBuilder::new().order(0);
627
628 let course = CourseBuilder::new("Advanced Course", "advanced-course")
629 .desc("A comprehensive advanced course")
630 .language("fi-FI")
631 .chatbot(true)
632 .module(module)
633 .role(user_id, UserRole::Teacher)
634 .course_id(course_id);
635
636 assert_eq!(course.name, "Advanced Course");
637 assert_eq!(course.slug, "advanced-course");
638 assert_eq!(course.description, "A comprehensive advanced course");
639 assert_eq!(course.language_code, "fi-FI");
640 assert!(course.can_add_chatbot);
641 assert_eq!(course.modules.len(), 1);
642 assert_eq!(course.extra_roles.len(), 1);
643 assert_eq!(course.course_id, Some(course_id));
644 }
645
646 #[test]
647 fn course_builder_default_values() {
648 let course = CourseBuilder::new("Minimal Course", "minimal");
649
650 assert_eq!(course.language_code, "en");
651 assert!(!course.can_add_chatbot);
652 assert_eq!(course.description, "Sample course.");
653 assert_eq!(course.default_instance_name, b"default-instance");
654 assert!(course.modules.is_empty());
655 assert!(course.extra_roles.is_empty());
656 assert!(course.course_id.is_none());
657 }
658
659 #[test]
660 fn course_builder_empty_strings() {
661 let course = CourseBuilder::new("", "");
662
663 assert_eq!(course.name, "");
664 assert_eq!(course.slug, "");
665 assert_eq!(course.language_code, "en");
666 assert_eq!(course.description, "Sample course.");
667 }
668
669 #[test]
670 fn course_builder_method_chaining_order() {
671 let course1 = CourseBuilder::new("Course 1", "course-1")
672 .chatbot(true)
673 .desc("Description 1")
674 .language("fi-FI");
675
676 let course2 = CourseBuilder::new("Course 2", "course-2")
677 .language("fi-FI")
678 .desc("Description 2")
679 .chatbot(true);
680
681 assert_eq!(course1.can_add_chatbot, course2.can_add_chatbot);
682 assert_eq!(course1.language_code, course2.language_code);
683 assert_eq!(course1.description, "Description 1");
684 assert_eq!(course2.description, "Description 2");
685 }
686
687 #[test]
688 fn course_builder_front_page_content() {
689 let custom_content = vec![
690 GutenbergBlock::landing_page_hero_section("Custom Welcome", "Custom Subheading"),
691 GutenbergBlock::course_objective_section(),
692 ];
693
694 let course = CourseBuilder::new("Test Course", "test-course")
695 .front_page_content(custom_content.clone());
696
697 assert_eq!(course.front_page_content, Some(custom_content));
698 }
699
700 #[test]
701 fn course_builder_front_page_content_default() {
702 let course = CourseBuilder::new("Test Course", "test-course");
703
704 assert_eq!(course.front_page_content, None);
705 }
706
707 #[test]
708 fn course_builder_top_level_page() {
709 let course = CourseBuilder::new("Test Course", "test-course")
710 .top_level_page("/welcome", "Welcome Page", 1, false, None)
711 .top_level_page("/hidden", "Hidden Page", 2, true, None);
712
713 assert_eq!(course.pages.len(), 2);
714 assert_eq!(course.pages[0].url, "/welcome");
715 assert_eq!(course.pages[0].title, "Welcome Page");
716 assert_eq!(course.pages[0].page_number, 1);
717 assert!(!course.pages[0].is_hidden);
718 assert_eq!(course.pages[0].content, None);
719
720 assert_eq!(course.pages[1].url, "/hidden");
721 assert_eq!(course.pages[1].title, "Hidden Page");
722 assert_eq!(course.pages[1].page_number, 2);
723 assert!(course.pages[1].is_hidden);
724 assert_eq!(course.pages[1].content, None);
725 }
726
727 #[test]
728 fn course_builder_top_level_page_with_content() {
729 let content = vec![];
730 let course = CourseBuilder::new("Test Course", "test-course").top_level_page(
731 "/content",
732 "Content Page",
733 1,
734 false,
735 Some(content.clone()),
736 );
737
738 assert_eq!(course.pages.len(), 1);
739 assert_eq!(course.pages[0].url, "/content");
740 assert_eq!(course.pages[0].title, "Content Page");
741 assert_eq!(course.pages[0].page_number, 1);
742 assert!(!course.pages[0].is_hidden);
743 assert_eq!(course.pages[0].content, Some(content));
744 }
745
746 #[test]
747 fn course_builder_multiple_top_level_pages() {
748 let course = CourseBuilder::new("Test Course", "test-course")
749 .top_level_page("/welcome", "Welcome", 1, false, None)
750 .top_level_page("/about", "About", 2, false, None)
751 .top_level_page("/secret", "Secret", 3, true, None);
752
753 assert_eq!(course.pages.len(), 3);
754 assert_eq!(course.pages[0].url, "/welcome");
755 assert_eq!(course.pages[1].url, "/about");
756 assert_eq!(course.pages[2].url, "/secret");
757 assert!(course.pages[2].is_hidden);
758 }
759}