2121require_once ($ CFG ->dirroot . '/course/lib.php ' );
2222require_once ($ CFG ->dirroot . '/course/modlib.php ' );
2323require_once ($ CFG ->dirroot . '/lib/resourcelib.php ' );
24+ require_once ($ CFG ->dirroot . '/mod/book/locallib.php ' );
2425
2526/**
2627 * Handles creating and updating Moodle course structure from repo data.
@@ -46,7 +47,7 @@ public function __construct(\stdClass $course) {
4647 $ this ->course = $ course ;
4748
4849 // Cache module IDs for the types we support.
49- foreach (['page ' , 'label ' , 'url ' ] as $ modname ) {
50+ foreach (['page ' , 'label ' , 'url ' , ' book ' ] as $ modname ) {
5051 $ id = $ DB ->get_field ('modules ' , 'id ' , ['name ' => $ modname ]);
5152 if ($ id ) {
5253 $ this ->moduleids [$ modname ] = (int ) $ id ;
@@ -270,6 +271,177 @@ public function create_url(int $sectionnum, string $name, string $url, string $i
270271 return $ result ->coursemodule ;
271272 }
272273
274+ /**
275+ * Create a Book activity with chapters.
276+ *
277+ * @param int $sectionnum Section number
278+ * @param string $name Book name
279+ * @param array $chapters Ordered array of chapter data: ['title', 'content', 'subchapter', 'importsrc']
280+ * @param array $bookmeta Optional book metadata from book.yaml (numbering, intro)
281+ * @return int The course module ID (cmid)
282+ */
283+ public function create_book (int $ sectionnum , string $ name , array $ chapters , array $ bookmeta = []): int {
284+ global $ DB ;
285+
286+ if (empty ($ this ->moduleids ['book ' ])) {
287+ throw new \moodle_exception ('syncfailed ' , 'local_githubsync ' , '' , 'Book module not available ' );
288+ }
289+
290+ // Map numbering string values to book constants.
291+ $ numberingmap = ['none ' => 0 , 'numbers ' => 1 , 'bullets ' => 2 , 'indented ' => 3 ];
292+ $ numbering = 0 ;
293+ if (!empty ($ bookmeta ['numbering ' ]) && isset ($ numberingmap [$ bookmeta ['numbering ' ]])) {
294+ $ numbering = $ numberingmap [$ bookmeta ['numbering ' ]];
295+ }
296+
297+ $ moduleinfo = new \stdClass ();
298+ $ moduleinfo ->modulename = 'book ' ;
299+ $ moduleinfo ->module = $ this ->moduleids ['book ' ];
300+ $ moduleinfo ->name = !empty ($ bookmeta ['title ' ]) ? $ bookmeta ['title ' ] : $ name ;
301+ $ moduleinfo ->section = $ sectionnum ;
302+ $ moduleinfo ->visible = 1 ;
303+ $ moduleinfo ->visibleoncoursepage = 1 ;
304+
305+ $ moduleinfo ->intro = purify_html ($ bookmeta ['intro ' ] ?? '' );
306+ $ moduleinfo ->introformat = FORMAT_HTML ;
307+ $ moduleinfo ->numbering = $ numbering ;
308+ $ moduleinfo ->navstyle = 1 ;
309+ $ moduleinfo ->customtitles = 0 ;
310+
311+ $ result = add_moduleinfo ($ moduleinfo , $ this ->course );
312+ $ cmid = $ result ->coursemodule ;
313+
314+ // Get the book instance ID.
315+ $ cm = get_coursemodule_from_id ('book ' , $ cmid , 0 , false , MUST_EXIST );
316+ $ bookid = $ cm ->instance ;
317+
318+ // Insert chapters.
319+ $ pagenum = 0 ;
320+ foreach ($ chapters as $ chapter ) {
321+ $ pagenum ++;
322+ $ rec = new \stdClass ();
323+ $ rec ->bookid = $ bookid ;
324+ $ rec ->pagenum = $ pagenum ;
325+ $ rec ->subchapter = ($ pagenum === 1 ) ? 0 : (!empty ($ chapter ['subchapter ' ]) ? 1 : 0 );
326+ $ rec ->title = $ chapter ['title ' ];
327+ $ rec ->content = purify_html ($ chapter ['content ' ]);
328+ $ rec ->contentformat = FORMAT_HTML ;
329+ $ rec ->hidden = 0 ;
330+ $ rec ->importsrc = $ chapter ['importsrc ' ] ?? '' ;
331+ $ rec ->timecreated = time ();
332+ $ rec ->timemodified = time ();
333+ $ DB ->insert_record ('book_chapters ' , $ rec );
334+ }
335+
336+ // Preload chapters to validate structure.
337+ $ book = $ DB ->get_record ('book ' , ['id ' => $ bookid ], '* ' , MUST_EXIST );
338+ book_preload_chapters ($ book );
339+
340+ return $ cmid ;
341+ }
342+
343+ /**
344+ * Update book metadata (name, numbering, intro) from book.yaml.
345+ *
346+ * @param int $cmid Course module ID of the book
347+ * @param array $bookmeta Parsed book.yaml data
348+ */
349+ public function update_book_metadata (int $ cmid , array $ bookmeta ): void {
350+ global $ DB ;
351+
352+ $ cm = get_coursemodule_from_id ('book ' , $ cmid , 0 , false , MUST_EXIST );
353+ $ book = $ DB ->get_record ('book ' , ['id ' => $ cm ->instance ], '* ' , MUST_EXIST );
354+
355+ $ numberingmap = ['none ' => 0 , 'numbers ' => 1 , 'bullets ' => 2 , 'indented ' => 3 ];
356+ $ changed = false ;
357+
358+ if (!empty ($ bookmeta ['title ' ]) && $ bookmeta ['title ' ] !== $ book ->name ) {
359+ $ book ->name = $ bookmeta ['title ' ];
360+ $ changed = true ;
361+ }
362+ if (!empty ($ bookmeta ['numbering ' ]) && isset ($ numberingmap [$ bookmeta ['numbering ' ]])) {
363+ $ newnumbering = $ numberingmap [$ bookmeta ['numbering ' ]];
364+ if ($ newnumbering !== (int ) $ book ->numbering ) {
365+ $ book ->numbering = $ newnumbering ;
366+ $ changed = true ;
367+ }
368+ }
369+ if (isset ($ bookmeta ['intro ' ])) {
370+ $ newintro = purify_html ($ bookmeta ['intro ' ]);
371+ if ($ newintro !== $ book ->intro ) {
372+ $ book ->intro = $ newintro ;
373+ $ book ->introformat = FORMAT_HTML ;
374+ $ changed = true ;
375+ }
376+ }
377+
378+ if ($ changed ) {
379+ $ book ->timemodified = time ();
380+ $ DB ->update_record ('book ' , $ book );
381+ rebuild_course_cache ($ this ->course ->id , true );
382+ }
383+ }
384+
385+ /**
386+ * Update an existing book chapter found by importsrc.
387+ *
388+ * @param int $bookid Book instance ID
389+ * @param string $importsrc The repo path stored in importsrc
390+ * @param string $title Chapter title
391+ * @param string $content Chapter HTML content
392+ * @param bool $subchapter Whether this is a subchapter
393+ * @param int $pagenum New page number
394+ * @return bool True if found and updated
395+ */
396+ public function update_book_chapter (int $ bookid , string $ importsrc , string $ title ,
397+ string $ content , bool $ subchapter , int $ pagenum ): bool {
398+ global $ DB ;
399+
400+ $ chapter = $ DB ->get_record ('book_chapters ' , ['bookid ' => $ bookid , 'importsrc ' => $ importsrc ]);
401+ if (!$ chapter ) {
402+ return false ;
403+ }
404+
405+ $ chapter ->title = $ title ;
406+ $ chapter ->content = purify_html ($ content );
407+ $ chapter ->contentformat = FORMAT_HTML ;
408+ $ chapter ->subchapter = $ subchapter ? 1 : 0 ;
409+ $ chapter ->pagenum = $ pagenum ;
410+ $ chapter ->hidden = 0 ;
411+ $ chapter ->timemodified = time ();
412+ $ DB ->update_record ('book_chapters ' , $ chapter );
413+
414+ return true ;
415+ }
416+
417+ /**
418+ * Create a new chapter in an existing book.
419+ *
420+ * @param int $bookid Book instance ID
421+ * @param string $title Chapter title
422+ * @param string $content Chapter HTML content
423+ * @param bool $subchapter Whether this is a subchapter
424+ * @param int $pagenum Page number for ordering
425+ * @param string $importsrc Repo path for tracking
426+ */
427+ public function create_book_chapter (int $ bookid , string $ title , string $ content ,
428+ bool $ subchapter , int $ pagenum , string $ importsrc ): void {
429+ global $ DB ;
430+
431+ $ rec = new \stdClass ();
432+ $ rec ->bookid = $ bookid ;
433+ $ rec ->pagenum = $ pagenum ;
434+ $ rec ->subchapter = $ subchapter ? 1 : 0 ;
435+ $ rec ->title = $ title ;
436+ $ rec ->content = purify_html ($ content );
437+ $ rec ->contentformat = FORMAT_HTML ;
438+ $ rec ->hidden = 0 ;
439+ $ rec ->importsrc = $ importsrc ;
440+ $ rec ->timecreated = time ();
441+ $ rec ->timemodified = time ();
442+ $ DB ->insert_record ('book_chapters ' , $ rec );
443+ }
444+
273445 /**
274446 * Create an activity based on front matter type.
275447 *
0 commit comments