Repository: zestedesavoir/zds-site

Issue:
Identifiants dupliqués pour les conclusions et introduction dans les epub
- créer un big tuto
- entrer une introduction
- entrer une conclusion
- publier 
- regarder l'epub

Résultat : il n'est pas lisible dans la plupart des lecteurs, le lecteur de edge se contente de mettre " nous n'avons pas pu lire la section".

En vérité c'est dû au fait que nous donnons un identifiant identique à l'intro et la conclusion, dans le cas du tuto de langage c, c'était chapter-le-langage-c.

https://framadrop.org/r/7bbI50vf6h#20JgYExjVOTbNhg6WxFmdyuHZ4WxKiaeq5GnMk7dnoc=


Discussion:

Resolving patch:
diff --git a/zds/tutorialv2/epub_utils.py b/zds/tutorialv2/epub_utils.py
--- a/zds/tutorialv2/epub_utils.py
+++ b/zds/tutorialv2/epub_utils.py
@@ -1,5 +1,7 @@
 import contextlib
 import logging
+import os
+import re
 import shutil
 from collections import namedtuple
 from urllib import parse
@@ -47,7 +49,7 @@ def __traverse_and_identify_images(image_dir):
         yield ebook_image_path, identifier, media_type_map.get(ext.lower(), 'image/png')
 
 
-def build_html_chapter_file(published_object, versioned_object, working_dir, root_dir):
+def build_html_chapter_file(published_object, versioned_object, working_dir, root_dir, image_handler):
     """
     Parses the full html file, extracts the ``<hX>`` tags and splits their content into new files.
     Yields all the produced files.
@@ -60,16 +62,17 @@ def build_html_chapter_file(published_object, versioned_object, working_dir, roo
     :type versioned_object: zds.tutorialv2.models.models_versioned.VersionedContent
     :param published_object: the published content as saved in database
     :type published_object: zds.tutorialv2.models.models_database.PublishedContent
+    :type image_handler: ImageHandling
     :return: a generator of tuples composed as ``[splitted_html_file_relative_path, chapter-identifier, chapter-title]``
     """
     DirTuple = namedtuple('DirTuple', ['absolute', 'relative'])
     img_dir = working_dir.parent / 'images'
     path_to_title_dict = publish_container(published_object, str(working_dir), versioned_object,
                                            template='tutorialv2/export/ebook/chapter.html',
-                                           file_ext='xhtml', image_callback=handle_images,
+                                           file_ext='xhtml', image_callback=image_handler.handle_images,
                                            image_directory=DirTuple(str(img_dir.absolute()),
                                                                     str(img_dir.relative_to(root_dir))),
-                                           relative='.')
+                                           relative='.', intro_ccl_template='tutorialv2/export/ebook/introduction.html')
     for container_path, title in path_to_title_dict.items():
         # TODO: check if a function exists in the std lib to get rid of `root_dir + '/'`
         yield container_path.replace(str(root_dir.absolute()) + '/', ''), 'chapter-' + slugify(title), title
@@ -111,7 +114,7 @@ def build_nav_xhtml(working_dir, content, chapters):
 def build_ebook(published_content_entity, working_dir, final_file_path):
     ops_dir = Path(working_dir, 'ebook', 'OPS')
     text_dir_path = Path(ops_dir, 'Text')
-    style_dir_path = Path(ops_dir, 'Text', 'styles')
+    style_dir_path = Path(ops_dir, 'styles')
     font_dir_path = Path(ops_dir, 'Fonts')
     meta_inf_dir_path = Path(working_dir, 'ebook', 'META-INF')
     target_image_dir = Path(ops_dir, 'images')
@@ -135,12 +138,12 @@ def build_ebook(published_content_entity, working_dir, final_file_path):
 
     with mime_path.open(mode='w', encoding='utf-8') as mimefile:
         mimefile.write(mimetype_conf['content'])
-
+    image_handler = ImageHandling()
     chapters = list(
         build_html_chapter_file(published_content_entity.content,
                                 published_content_entity.content.load_version(sha=published_content_entity.sha_public),
                                 working_dir=text_dir_path,
-                                root_dir=Path(working_dir, 'ebook')))
+                                root_dir=Path(working_dir, 'ebook'), image_handler=image_handler))
     build_toc_ncx(chapters, published_content_entity, ops_dir)
     copy_or_create_empty(settings.ZDS_APP['content']['epub_stylesheets']['toc'], style_dir_path, 'toc.css')
     copy_or_create_empty(settings.ZDS_APP['content']['epub_stylesheets']['full'], style_dir_path, 'zmd.css')
@@ -151,7 +154,9 @@ def build_ebook(published_content_entity, working_dir, final_file_path):
         import_asset(style_images_path, target_image_dir)
     if smiley_images_path.exists():
         import_asset(smiley_images_path, target_image_dir)
-    images = __traverse_and_identify_images(target_image_dir)
+    images = list(__traverse_and_identify_images(target_image_dir))
+    image_handler.names.add('sprite.png')
+    images = image_handler.remove_unused_image(target_image_dir, images)
     build_content_opf(published_content_entity, chapters, images, ops_dir)
     build_container_xml(meta_inf_dir_path)
     build_nav_xhtml(ops_dir, published_content_entity, chapters)
@@ -179,32 +184,45 @@ def copy_or_create_empty(src_path, dst_path, default_name):
             f.write('')
 
 
-def handle_images(relative_path):
-    def _(html_code):
-        soup_parser = BeautifulSoup(html_code, 'lxml')
-        for image in soup_parser.find_all('img'):
-            if not image.get('src', ''):
-                continue
-            image_url = image['src']
-            if image_url.startswith('http://') or image_url.startswith('https://'):
-                splitted = parse.urlsplit(image_url)
-                final_path = splitted.path
-            elif image_url.startswith(settings.MEDIA_URL):
-                final_path = Path(image_url).name
-            elif Path(image_url).is_absolute() and 'images' in image_url:
-                root = Path(image_url)
-                while root.name != 'images':
-                    root = root.parent
-                final_path = str(Path(image_url).relative_to(root))
-            else:
-                final_path = Path(image_url).name
-            image_path_in_ebook = relative_path + '/images/' + str(final_path).replace('%20', '_')
-            image['src'] = str(image_path_in_ebook)
-        ids = {}
-        for element in soup_parser.find_all(name=None, attrs={'id': (lambda s: True)}):
-            while element.get('id', None) and element['id'] in ids:
-                element['id'] += '-1'
-            if element.get('id', None):
-                ids[element['id']] = True
-        return soup_parser.prettify('utf-8').decode('utf-8')
-    return _
+class ImageHandling:
+    def __init__(self):
+        self.names = set()
+        self.url_scheme_matcher = re.compile(r'^https?://')
+
+    def handle_images(self, relative_path):
+        def handle_image_path_with_good_img_dir_path(html_code):
+            soup_parser = BeautifulSoup(html_code, 'lxml')
+            for image in soup_parser.find_all('img'):
+                if not image.get('src', ''):
+                    continue
+                image_url = image['src']
+                if self.url_scheme_matcher.search(image_url):
+                    splitted = parse.urlsplit(image_url)
+                    final_path = splitted.path
+                elif image_url.startswith(settings.MEDIA_URL):
+                    final_path = Path(image_url).name
+                elif Path(image_url).is_absolute() and 'images' in image_url:
+                    root = Path(image_url)
+                    while root.name != 'images':
+                        root = root.parent
+                    final_path = str(Path(image_url).relative_to(root))
+                else:
+                    final_path = Path(image_url).name
+                image_path_in_ebook = relative_path + '/images/' + str(final_path).replace('%20', '_')
+                image['src'] = str(image_path_in_ebook)
+                self.names.add(Path(image_path_in_ebook).name)
+            ids = {}
+            for element in soup_parser.find_all(name=None, attrs={'id': (lambda s: True)}):
+                while element.get('id', None) and element['id'] in ids:
+                    element['id'] += '-1'
+                if element.get('id', None):
+                    ids[element['id']] = True
+            return soup_parser.prettify('utf-8').decode('utf-8')
+        return handle_image_path_with_good_img_dir_path
+
+    def remove_unused_image(self, image_path: Path, imglist):
+        for image in image_path.iterdir():
+            if image.name not in self.names and not image.is_dir():
+                os.remove(str(image))
+                imglist = [i for i in imglist if i[0].name.replace('%20', '_') != image.name]
+        return imglist
diff --git a/zds/tutorialv2/models/versioned.py b/zds/tutorialv2/models/versioned.py
--- a/zds/tutorialv2/models/versioned.py
+++ b/zds/tutorialv2/models/versioned.py
@@ -427,7 +427,7 @@ def get_introduction(self):
         """
         if self.introduction:
             return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree,
-                            self.introduction)
+                            self.introduction.replace('\\', '/'))
 
     def get_conclusion(self):
         """
@@ -436,7 +436,7 @@ def get_conclusion(self):
         """
         if self.conclusion:
             return get_blob(self.top_container().repository.commit(self.top_container().current_version).tree,
-                            self.conclusion)
+                            self.conclusion.replace('\\', '/'))
 
     def get_introduction_online(self):
         """The introduction content for online version.
@@ -1008,7 +1008,8 @@ def get_text(self):
         if self.text:
             return get_blob(
                 self.container.top_container().repository.commit(self.container.top_container().current_version).tree,
-                self.text)
+                self.text.replace('\\', '/'))
+        return ''
 
     def compute_hash(self):
         """Compute an MD5 hash from the text, for comparison purpose
diff --git a/zds/tutorialv2/publish_container.py b/zds/tutorialv2/publish_container.py
--- a/zds/tutorialv2/publish_container.py
+++ b/zds/tutorialv2/publish_container.py
@@ -46,11 +46,12 @@ def publish_container(db_object, base_dir, container, template='tutorialv2/expor
     if not path.isdir(current_dir):
         makedirs(current_dir)
 
+    img_relative_path = '..' if ctx['relative'] == '.' else '../' + ctx['relative']
     if container.has_extracts():  # the container can be rendered in one template
-        img_relative_path = '..' if ctx['relative'] == '.' else '../' + ctx['relative']
         wrapped_image_callback = image_callback(img_relative_path) if image_callback else image_callback
-        args = {'container': container, 'is_js': is_js, 'relative': img_relative_path}
+        args = {'container': container, 'is_js': is_js}
         args.update(ctx)
+        args['relative'] = img_relative_path
         parsed = render_to_string(template, args)
         write_chapter_file(base_dir, container, Path(container.get_prod_path(True, file_ext)),
                            parsed, path_to_title_dict, wrapped_image_callback)
@@ -61,17 +62,23 @@ def publish_container(db_object, base_dir, container, template='tutorialv2/expor
         container.conclusion = None
 
     else:  # separate render of introduction and conclusion
-        wrapped_image_callback = image_callback(ctx['relative']) if image_callback else image_callback
+        wrapped_image_callback_intro_ccl = image_callback(img_relative_path) if image_callback else image_callback
         # create subdirectory
         if not path.isdir(current_dir):
             makedirs(current_dir)
-        ctx['relative'] = '../' + ctx['relative']
+        relative_ccl_path = '../' + ctx.get('relative', '.')
         if container.introduction and container.get_introduction():
             part_path = Path(container.get_prod_path(relative=True), 'introduction.' + file_ext)
-            parsed = emarkdown(container.get_introduction(), db_object.js_support)
+            args = {'text': container.get_introduction()}
+            args.update(ctx)
+            args['relative'] = relative_ccl_path
+            if ctx.get('intro_ccl_template', None):
+                parsed = render_to_string(ctx.get('intro_ccl_template'), args)
+            else:
+                parsed = emarkdown(container.get_introduction(), db_object.js_support)
             container.introduction = str(part_path)
             write_chapter_file(base_dir, container, part_path, parsed, path_to_title_dict,
-                               wrapped_image_callback)
+                               wrapped_image_callback_intro_ccl)
         children = copy.copy(container.children)
         container.children = []
         container.children_dict = {}
@@ -80,14 +87,23 @@ def publish_container(db_object, base_dir, container, template='tutorialv2/expor
             altered_version = copy.copy(child)
             container.children.append(altered_version)
             container.children_dict[altered_version.slug] = altered_version
+            if not child.has_extracts():
+                ctx['relative'] = '../' + ctx['relative']
             result = publish_container(db_object, base_dir, altered_version, file_ext=file_ext,
                                        image_callback=image_callback, template=template, **ctx)
             path_to_title_dict.update(result)
         if container.conclusion and container.get_conclusion():
             part_path = Path(container.get_prod_path(relative=True), 'conclusion.' + file_ext)
-            parsed = emarkdown(container.get_conclusion(), db_object.js_support)
+            args = {'text': container.get_conclusion()}
+            args.update(ctx)
+            args['relative'] = relative_ccl_path
+            if ctx.get('intro_ccl_template', None):
+                parsed = render_to_string(ctx.get('intro_ccl_template'), args)
+            else:
+                parsed = emarkdown(container.get_conclusion(), db_object.js_support)
             container.conclusion = str(part_path)
-            write_chapter_file(base_dir, container, part_path, parsed, path_to_title_dict, wrapped_image_callback)
+            write_chapter_file(base_dir, container, part_path, parsed,
+                               path_to_title_dict, wrapped_image_callback_intro_ccl)
 
     return path_to_title_dict
 
@@ -120,4 +136,8 @@ def write_chapter_file(base_dir, container, part_path, parsed, path_to_title_dic
             raise FailureDuringPublication(
                 _(u'Une erreur est survenue durant la publication de « {} », vérifiez le code markdown')
                 .format(container.title))
-    path_to_title_dict[str(part_path)] = container.title
+    # fix duplicate of introduction and conclusion in ebook ids
+    if part_path.name.startswith('conclusion.') or part_path.name.startswith('introduction.'):
+        path_to_title_dict[str(part_path)] = part_path.name.split('.')[0] + '_' + container.title
+    else:
+        path_to_title_dict[str(part_path)] = container.title


Test patch: