def handle(self, *args, **options):
        _file = options["manifest_path"]
        if os.path.isfile(_file) and _file[-5:] == ".json":
            with open(_file) as json_file:
                data = json_handler.load(json_file)
            _type = "TUTORIAL"
            if data["type"].lower() == "article":
                _type = "ARTICLE"
            versioned = VersionedContent("", _type, data["title"], slugify(data["title"]))
            versioned.description = data["description"]
            if "introduction" in data:
                versioned.introduction = data["introduction"]
            if "conclusion" in data:
                versioned.conclusion = data["conclusion"]
            versioned.licence = Licence.objects.filter(code=data["licence"]).first()
            versioned.version = "2.0"
            versioned.slug = slugify(data["title"])
            if "parts" in data:
                # if it is a big tutorial
                for part in data["parts"]:
                    current_part = Container(part["title"], str(part["pk"]) + "_" + slugify(part["title"]))
                    if "introduction" in part:
                        current_part.introduction = part["introduction"]
                    if "conclusion" in part:
                        current_part.conclusion = part["conclusion"]
                    versioned.add_container(current_part)
                    for chapter in part["chapters"]:
                        current_chapter = Container(
                            chapter["title"], str(chapter["pk"]) + "_" + slugify(chapter["title"])
                        )
                        if "introduction" in chapter:
                            current_chapter.introduction = chapter["introduction"]
                        if "conclusion" in chapter:
                            current_chapter.conclusion = chapter["conclusion"]
                        current_part.add_container(current_chapter)
                        for extract in chapter["extracts"]:
                            current_extract = Extract(
                                extract["title"], str(extract["pk"]) + "_" + slugify(extract["title"])
                            )
                            current_chapter.add_extract(current_extract)
                            current_extract.text = current_extract.get_path(True)

            elif "chapter" in data:
                # if it is a mini tutorial
                for extract in data["chapter"]["extracts"]:
                    current_extract = Extract(extract["title"], str(extract["pk"]) + "_" + slugify(extract["title"]))
                    versioned.add_extract(current_extract)
                    current_extract.text = current_extract.get_path(True)

            elif versioned.type == "ARTICLE":
                extract = Extract("text", "text")
                versioned.add_extract(extract)
                extract.text = extract.get_path(True)

            with open(_file, "w") as json_file:
                json_file.write(versioned.get_json())
Exemple #2
0
    def form_valid(self, form):

        # create the object:
        self.content = PublishableContent()
        self.content.title = form.cleaned_data["title"]
        self.content.description = form.cleaned_data["description"]
        self.content.type = form.cleaned_data["type"]
        self.content.licence = self.request.user.profile.licence  # Use the preferred license of the user if it exists
        self.content.source = form.cleaned_data["source"]
        self.content.creation_date = datetime.now()

        # Creating the gallery
        gal = Gallery()
        gal.title = form.cleaned_data["title"]
        gal.slug = slugify(form.cleaned_data["title"])
        gal.pubdate = datetime.now()
        gal.save()

        self.content.gallery = gal
        self.content.save()
        # create image:
        if "image" in self.request.FILES:
            img = Image()
            img.physical = self.request.FILES["image"]
            img.gallery = gal
            img.title = self.request.FILES["image"]
            img.slug = slugify(self.request.FILES["image"].name)
            img.pubdate = datetime.now()
            img.save()
            self.content.image = img

        self.content.save()

        # We need to save the content before changing its author list since it's a many-to-many relationship
        self.content.authors.add(self.request.user)

        self.content.ensure_author_gallery()
        self.content.save()
        # Add subcategories on tutorial
        for subcat in form.cleaned_data["subcategory"]:
            self.content.subcategory.add(subcat)

        self.content.save()

        # create a new repo :
        init_new_repo(
            self.content,
            form.cleaned_data["introduction"],
            form.cleaned_data["conclusion"],
            form.cleaned_data["msg_commit"],
        )

        return super(CreateContent, self).form_valid(form)
Exemple #3
0
def slugify_raise_on_invalid(title, use_old_slugify=False):
    """
    use uuslug to generate a slug but if the title is incorrect (only special chars or slug is empty), an exception
    is raised.

    :param title: to be slugified title
    :type title: str
    :param use_old_slugify: use the function `slugify()` defined in zds.utils instead of the one in uuslug. Usefull \
    for retro-compatibility with the old article/tutorial module, SHOULD NOT be used for the new one !
    :type use_old_slugify: bool
    :raise InvalidSlugError: on incorrect slug
    :return: the slugified title
    :rtype: str
    """

    if not isinstance(title, str):
        raise InvalidSlugError("", source=title)
    if not use_old_slugify:
        slug = slugify(title)
    else:
        slug = old_slugify(title)

    if not check_slug(slug):
        raise InvalidSlugError(slug, source=title)

    return slug
Exemple #4
0
def create_content_gallery(form):
    gal = Gallery()
    gal.title = form.cleaned_data["title"]
    gal.slug = slugify(form.cleaned_data["title"])
    gal.pubdate = datetime.now()
    gal.save()
    return gal
Exemple #5
0
    def items(self):
        """
        :return: The last (typically 5) contents (sorted by publication date).
        """
        subcategories = None
        category = self.query_params.get("category", "").strip()
        if category:
            category = get_object_or_404(Category, slug=category)
            subcategories = category.get_subcategories()
        subcategory = self.query_params.get("subcategory", "").strip()
        if subcategory:
            subcategories = [get_object_or_404(SubCategory, slug=subcategory)]

        tags = None
        tag = self.query_params.get("tag", "").strip()
        if tag:
            tags = [
                get_object_or_404(Tag,
                                  slug=slugify(self.query_params.get("tag")))
            ]

        feed_length = settings.ZDS_APP["content"]["feed_length"]

        contents = PublishedContent.objects.last_contents(
            content_type=[self.content_type],
            subcategories=subcategories,
            tags=tags)[:feed_length]

        return contents
Exemple #6
0
    def perform_create(self, title, user, subtitle=""):
        """Create gallery

        :param title: title
        :type title: str
        :param user:  the user
        :type user: zds.member.models.User
        :param subtitle: subtitle
        :type subtitle: str
        :rtype: Gallery
        """
        gallery = Gallery(title=title)
        gallery.subtitle = subtitle
        gallery.slug = slugify(title)
        gallery.pubdate = datetime.datetime.now()
        gallery.save()

        user_gallery = UserGallery(gallery=gallery,
                                   user=user,
                                   mode=GALLERY_WRITE)
        user_gallery.save()

        self.gallery = gallery
        self.users_and_permissions = {user.pk: {"read": True, "write": True}}

        return self.gallery
Exemple #7
0
    def perform_update(self, data):
        """Update image information

        :param data: things to update
        :type data: dict
        """

        if "physical" in data:
            physical = data.get("physical")
            if physical.size > settings.ZDS_APP["gallery"]["image_max_size"]:
                raise ImageTooLarge(self.image.title, physical.size)

            try:
                ImagePIL.open(physical)
            except OSError:
                raise NotAnImage(self.image.title)

            self.image.physical = physical

        if "title" in data:
            self.image.title = data.get("title")
            self.image.slug = slugify(self.image.title)

        if "legend" in data:
            self.image.legend = data.get("legend")

        self.image.save()

        return self.image
Exemple #8
0
    def perform_create(self, title, physical, legend=""):
        """Create a new image

        :param title: title
        :type title: str
        :param physical:
        :type physical: file
        :param legend: legend (optional)
        :type legend: str
        """
        if physical.size > settings.ZDS_APP["gallery"]["image_max_size"]:
            raise ImageTooLarge(title, physical.size)

        try:
            ImagePIL.open(physical)
        except OSError:
            raise NotAnImage(physical)

        image = Image()
        image.gallery = self.gallery
        image.title = title

        if legend:
            image.legend = legend
        else:
            image.legend = image.title

        image.physical = physical
        image.slug = slugify(title)
        image.pubdate = datetime.datetime.now()
        image.save()

        self.image = image

        return self.image
Exemple #9
0
    def handle(self, *args, **options):

        for c in PublishableContent.objects.all():
            if "'" in c.title:
                good_slug = slugify(c.title)
                if c.slug != good_slug:
                    if os.path.isdir(
                            os.path.join(
                                settings.ZDS_APP["content"]
                                ["repo_private_path"], good_slug)):
                        # this content was created before v16 and is probably broken
                        self.stdout.write(
                            "Fixing pre-v16 content #{} (« {} ») ... ".format(
                                c.pk, c.title),
                            ending="")
                        c.save()
                        if os.path.isdir(c.get_repo_path()):
                            self.stdout.write("[OK]")
                        else:
                            self.stdout.write("[KO]")
                    elif os.path.isdir(
                            os.path.join(
                                settings.ZDS_APP["content"]
                                ["repo_private_path"], c.slug)):
                        # this content was created during v16 and will be broken if nothing is done
                        self.stdout.write(
                            "Fixing in-v16 content #{} (« {} ») ... ".format(
                                c.pk, c.title),
                            ending="")
                        try:
                            versioned = c.load_version()
                        except OSError:
                            self.stdout.write("[KO]")
                        else:
                            c.sha_draft = versioned.repo_update_top_container(
                                c.title,
                                good_slug,
                                versioned.get_introduction(),
                                versioned.get_conclusion(),
                                commit_message="[hotfix] Corrige le slug",
                            )

                            c.save()

                            if os.path.isdir(c.get_repo_path()):
                                self.stdout.write("[OK]")
                            else:
                                self.stdout.write("[KO]")
                    else:
                        self.stderr.write(
                            'Content #{} (« {} ») is an orphan: there is no directory named "{}" or "{}".\n'
                            .format(c.pk, c.title, good_slug, c.slug))
Exemple #10
0
    def perform_update(self, data):
        """Update gallery information

        :param data: things to update
        :type data: dict
        :rtype: Gallery
        """
        if "title" in data:
            self.gallery.title = data.get("title")
            self.gallery.slug = slugify(self.gallery.title)
        if "subtitle" in data:
            self.gallery.subtitle = data.get("subtitle")

        self.gallery.save()
        return self.gallery
Exemple #11
0
    def get_queryset(self):
        """Filter the contents to obtain the list of contents of given type.
        If category parameter is provided, only contents which have this category will be listed.
        :return: list of contents with the right type
        :rtype: list of zds.tutorialv2.models.database.PublishedContent
        """
        sub_query = "SELECT COUNT(*) FROM {} WHERE {}={} AND {}={} AND utils_comment.is_visible=1".format(
            "tutorialv2_contentreaction,utils_comment",
            "tutorialv2_contentreaction.related_content_id",
            "tutorialv2_publishablecontent.id",
            "utils_comment.id",
            "tutorialv2_contentreaction.comment_ptr_id",
        )
        queryset = PublishedContent.objects.filter(must_redirect=False)
        # this condition got more complexe with development of zep13
        # if we do filter by content_type, then every published content can be
        # displayed. Othewise, we have to be sure the content was expressly chosen by
        # someone with staff authorization. Another way to say it "it has to be a
        # validated content (article, tutorial), `ContentWithoutValidation` live their
        # own life in their own page.
        if self.current_content_type:
            queryset = queryset.filter(content_type=self.current_content_type)
        else:
            queryset = queryset.filter(~Q(content_type="OPINION"))
        # prefetch:
        queryset = (
            queryset.prefetch_related("content").prefetch_related(
                "content__subcategory").prefetch_related("content__authors").
            select_related("content__licence").select_related("content__image")
            .select_related("content__last_note").select_related(
                "content__last_note__related_content").select_related(
                    "content__last_note__related_content__public_version"
                ).filter(pk=F("content__public_version__pk")))

        if "category" in self.request.GET:
            self.subcategory = get_object_or_404(
                SubCategory, slug=self.request.GET.get("category"))
            queryset = queryset.filter(
                content__subcategory__in=[self.subcategory])

        if "tag" in self.request.GET:
            self.tag = get_object_or_404(
                Tag, slug=slugify(self.request.GET.get("tag").lower().strip()))
            # TODO: fix me
            # different tags can have same slug such as C/C#/C++, as a first version we get all of them
            queryset = queryset.filter(content__tags__in=[self.tag])
        queryset = queryset.extra(select={"count_note": sub_query})
        return queryset.order_by("-publication_date")
Exemple #12
0
    def form_valid(self, form):

        if self.request.FILES["archive"]:
            try:
                zfile = zipfile.ZipFile(self.request.FILES["archive"], "r")
            except zipfile.BadZipfile:
                messages.error(self.request,
                               _("Cette archive n'est pas au format ZIP."))
                return self.form_invalid(form)

            try:
                new_content = UpdateContentWithArchive.extract_content_from_zip(
                    zfile)
            except BadArchiveError as e:
                messages.error(self.request, e.message)
                return super(CreateContentFromArchive, self).form_invalid(form)
            except KeyError as e:
                messages.error(
                    self.request,
                    _(e.message + " n'est pas correctement renseigné."))
                return super(CreateContentFromArchive, self).form_invalid(form)
            else:

                # Warn the user if the license has been changed
                manifest = json_handler.loads(
                    str(zfile.read("manifest.json"), "utf-8"))
                if new_content.licence and "licence" in manifest and manifest[
                        "licence"] != new_content.licence.code:
                    messages.info(
                        self.request,
                        _("la licence « {} » a été appliquée.".format(
                            new_content.licence.code)))

                # first, create DB object (in order to get a slug)
                self.object = PublishableContent()
                self.object.title = new_content.title
                self.object.description = new_content.description
                self.object.licence = new_content.licence
                self.object.type = new_content.type  # change of type is then allowed !!
                self.object.creation_date = datetime.now()

                self.object.save()

                new_content.slug = self.object.slug  # new slug (choosen via DB)

                # Creating the gallery
                gal = Gallery()
                gal.title = new_content.title
                gal.slug = slugify(new_content.title)
                gal.pubdate = datetime.now()
                gal.save()

                # Attach user to gallery
                self.object.gallery = gal
                self.object.save()

                # Add subcategories on tutorial
                for subcat in form.cleaned_data["subcategory"]:
                    self.object.subcategory.add(subcat)

                # We need to save the tutorial before changing its author list since it's a many-to-many relationship
                self.object.authors.add(self.request.user)
                self.object.save()
                self.object.ensure_author_gallery()
                # ok, now we can import
                introduction = ""
                conclusion = ""

                if new_content.introduction:
                    introduction = str(zfile.read(new_content.introduction),
                                       "utf-8")
                if new_content.conclusion:
                    conclusion = str(zfile.read(new_content.conclusion),
                                     "utf-8")

                commit_message = _("Création de « {} »").format(
                    new_content.title)
                init_new_repo(self.object,
                              introduction,
                              conclusion,
                              commit_message=commit_message)

                # copy all:
                versioned = self.object.load_version()
                try:
                    UpdateContentWithArchive.update_from_new_version_in_zip(
                        versioned, new_content, zfile)
                except BadArchiveError as e:
                    self.object.delete()  # abort content creation
                    messages.error(self.request, e.message)
                    return super(CreateContentFromArchive,
                                 self).form_invalid(form)

                # and end up by a commit !!
                commit_message = form.cleaned_data["msg_commit"]

                if not commit_message:
                    commit_message = _(
                        "Importation d'une archive contenant « {} »").format(
                            new_content.title)
                versioned.slug = self.object.slug  # force slug to ensure path resolution
                sha = versioned.repo_update(
                    versioned.title,
                    versioned.get_introduction(),
                    versioned.get_conclusion(),
                    commit_message,
                    update_slug=True,
                )

                # This HAVE TO happen after commiting files (if not, content are None)
                if "image_archive" in self.request.FILES:
                    try:
                        zfile = zipfile.ZipFile(
                            self.request.FILES["image_archive"], "r")
                    except zipfile.BadZipfile:
                        messages.error(
                            self.request,
                            _("L'archive contenant les images n'est pas au format ZIP."
                              ))
                        return self.form_invalid(form)

                    UpdateContentWithArchive.use_images_from_archive(
                        self.request, zfile, versioned, self.object.gallery)

                    commit_message = _(
                        "Utilisation des images de l'archive pour « {} »"
                    ).format(new_content.title)
                    sha = versioned.commit_changes(
                        commit_message)  # another commit

                # of course, need to update sha
                self.object.sha_draft = sha
                self.object.update_date = datetime.now()
                self.object.save()

                self.success_url = reverse("content:view",
                                           args=[versioned.pk, versioned.slug])

        return super(CreateContentFromArchive, self).form_valid(form)
Exemple #13
0
    def use_images_from_archive(request, zip_file, versioned_content, gallery):
        """Extract image from a gallery and then translate the ``![.+](prefix:filename)`` into the final image we want.
        The ``prefix`` is defined into the settings.
        Note that this function does not perform any commit.

        :param zip_file: ZIP archive
        :type zip_file: zipfile.ZipFile
        :param versioned_content: content
        :type versioned_content: VersionedContent
        :param gallery: gallery of image
        :type gallery: Gallery
        """
        translation_dic = {}

        # create a temporary directory:
        temp = os.path.join(tempfile.gettempdir(), str(time.time()))
        if not os.path.exists(temp):
            os.makedirs(temp)

        for image_path in zip_file.namelist():

            image_basename = os.path.basename(image_path)

            if not image_basename.strip():  # don't deal with directory
                continue

            temp_image_path = os.path.abspath(
                os.path.join(temp, image_basename))

            # create a temporary file for the image
            f_im = open(temp_image_path, "wb")
            f_im.write(zip_file.read(image_path))
            f_im.close()

            # if it's not an image, pass
            try:
                ImagePIL.open(temp_image_path)
            except OSError:
                continue

            # if size is too large, pass
            if os.stat(temp_image_path).st_size > settings.ZDS_APP["gallery"][
                    "image_max_size"]:
                messages.error(
                    request,
                    _('Votre image "{}" est beaucoup trop lourde, réduisez sa taille à moins de {:.0f}'
                      "Kio avant de l'envoyer.").format(
                          image_path,
                          settings.ZDS_APP["gallery"]["image_max_size"] /
                          1024),
                )
                continue

            # create picture in database:
            pic = Image()
            pic.gallery = gallery
            pic.title = image_basename
            pic.slug = slugify(image_basename)
            pic.physical = get_thumbnailer(open(temp_image_path, "rb"),
                                           relative_name=temp_image_path)
            pic.pubdate = datetime.now()
            pic.save()

            translation_dic[image_path] = settings.ZDS_APP["site"][
                "url"] + pic.physical.url

            # finally, remove image
            if os.path.exists(temp_image_path):
                os.remove(temp_image_path)

        zip_file.close()
        if os.path.exists(temp):
            shutil.rmtree(temp)

        # then, modify each extracts
        image_regex = re.compile(
            r"((?P<start>!\[.*?\]\()" +
            settings.ZDS_APP["content"]["import_image_prefix"] +
            r":(?P<path>.*?)(?P<end>\)))")

        for element in versioned_content.traverse(only_container=False):
            if isinstance(element, Container):
                introduction = element.get_introduction()
                introduction = image_regex.sub(
                    lambda g: UpdateContentWithArchive.update_image_link(
                        g, translation_dic), introduction)

                conclusion = element.get_conclusion()
                conclusion = image_regex.sub(
                    lambda g: UpdateContentWithArchive.update_image_link(
                        g, translation_dic), conclusion)
                element.repo_update(element.title,
                                    introduction,
                                    conclusion,
                                    do_commit=False)
            else:
                section_text = element.get_text()
                section_text = image_regex.sub(
                    lambda g: UpdateContentWithArchive.update_image_link(
                        g, translation_dic), section_text)

                element.repo_update(element.title,
                                    section_text,
                                    do_commit=False)
Exemple #14
0
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        if self.kwargs.get("slug", False):
            self.level = 2
            self.max_last_contents = settings.ZDS_APP["content"][
                "max_last_publications_level_2"]
        if self.kwargs.get("slug_category", False):
            self.level = 3
            self.max_last_contents = settings.ZDS_APP["content"][
                "max_last_publications_level_3"]
        if (self.request.GET.get("category", False)
                or self.request.GET.get("subcategory", False)
                or self.request.GET.get("type", False)
                or self.request.GET.get("tag", False)):
            self.level = 4
            self.max_last_contents = 50

        self.template_name = self.templates[self.level]
        recent_kwargs = {}

        if self.level == 1:
            # get categories and subcategories
            categories = ViewPublications.categories_with_contents_count(
                self.handle_types)

            context["categories"] = categories
            context["content_count"] = PublishedContent.objects.last_contents(
                content_type=self.handle_types,
                with_comments_count=False).count()

        elif self.level == 2:
            context["category"] = get_object_or_404(
                Category, slug=self.kwargs.get("slug"))
            context[
                "subcategories"] = ViewPublications.subcategories_with_contents_count(
                    context["category"], self.handle_types)
            recent_kwargs["subcategories"] = context["subcategories"]

        elif self.level == 3:
            subcategory = get_object_or_404(SubCategory,
                                            slug=self.kwargs.get("slug"))
            context["category"] = subcategory.get_parent_category()

            if context["category"].slug != self.kwargs.get("slug_category"):
                raise Http404("wrong slug for category ({} != {})".format(
                    context["category"].slug,
                    self.kwargs.get("slug_category")))

            context["subcategory"] = subcategory
            recent_kwargs["subcategories"] = [subcategory]

        elif self.level == 4:
            category = self.request.GET.get("category", None)
            subcategory = self.request.GET.get("subcategory", None)
            subcategories = None
            if category is not None:
                context["category"] = get_object_or_404(Category,
                                                        slug=category)
                subcategories = context["category"].get_subcategories()
            elif subcategory is not None:
                subcategory = get_object_or_404(
                    SubCategory, slug=self.request.GET.get("subcategory"))
                context["category"] = subcategory.get_parent_category()
                context["subcategory"] = subcategory
                subcategories = [subcategory]

            content_type = self.handle_types
            context["type"] = None
            if "type" in self.request.GET:
                _type = self.request.GET.get("type", "").upper()
                if _type in self.handle_types:
                    content_type = _type
                    context["type"] = TYPE_CHOICES_DICT[_type]
                else:
                    raise Http404(f"wrong type {_type}")

            tag = self.request.GET.get("tag", None)
            tags = None
            if tag is not None:
                tags = [get_object_or_404(Tag, slug=slugify(tag))]
                context["tag"] = tags[0]

            contents_queryset = PublishedContent.objects.last_contents(
                subcategories=subcategories,
                tags=tags,
                content_type=content_type)
            items_per_page = settings.ZDS_APP["content"]["content_per_page"]
            make_pagination(
                context,
                self.request,
                contents_queryset,
                items_per_page,
                context_list_name="filtered_contents",
                with_previous_item=False,
            )

        if self.level < 4:
            last_articles = PublishedContent.objects.last_contents(
                **dict(content_type="ARTICLE", **recent_kwargs))
            context["last_articles"] = last_articles[:self.max_last_contents]
            context["more_articles"] = last_articles.count(
            ) > self.max_last_contents

            last_tutorials = PublishedContent.objects.last_contents(
                **dict(content_type="TUTORIAL", **recent_kwargs))
            context["last_tutorials"] = last_tutorials[:self.max_last_contents]
            context["more_tutorials"] = last_tutorials.count(
            ) > self.max_last_contents

            context["beta_forum"] = (
                Forum.objects.prefetch_related("category").filter(
                    pk=settings.ZDS_APP["forum"]["beta_forum_id"]).last())

        context["level"] = self.level
        return context
Exemple #15
0
    def form_valid(self, form):
        versioned = self.versioned_object
        publishable = self.object

        # check if content has changed:
        current_hash = versioned.compute_hash()
        if current_hash != form.cleaned_data["last_hash"]:
            data = form.data.copy()
            data["last_hash"] = current_hash
            data["introduction"] = versioned.get_introduction()
            data["conclusion"] = versioned.get_conclusion()
            form.data = data
            messages.error(
                self.request,
                _("Une nouvelle version a été postée avant que vous ne validiez."
                  ))
            return self.form_invalid(form)

        # Forbid removing all categories of a validated content
        if publishable.in_public() and not form.cleaned_data["subcategory"]:
            messages.error(
                self.request,
                _("Vous devez choisir au moins une catégorie, car ce contenu est déjà publié."
                  ))
            return self.form_invalid(form)

        # first, update DB (in order to get a new slug if needed)
        title_is_changed = publishable.title != form.cleaned_data["title"]
        publishable.title = form.cleaned_data["title"]
        publishable.description = form.cleaned_data["description"]
        publishable.source = form.cleaned_data["source"]

        publishable.update_date = datetime.now()

        # update gallery and image:
        gal = Gallery.objects.filter(pk=publishable.gallery.pk)
        gal.update(title=publishable.title)
        gal.update(slug=slugify(publishable.title))
        gal.update(update=datetime.now())

        if "image" in self.request.FILES:
            img = Image()
            img.physical = self.request.FILES["image"]
            img.gallery = publishable.gallery
            img.title = self.request.FILES["image"]
            img.slug = slugify(self.request.FILES["image"].name)
            img.pubdate = datetime.now()
            img.save()
            publishable.image = img

        publishable.save(force_slug_update=title_is_changed)
        logger.debug("content %s updated, slug is %s", publishable.pk,
                     publishable.slug)
        # now, update the versioned information
        versioned.description = form.cleaned_data["description"]

        sha = versioned.repo_update_top_container(
            form.cleaned_data["title"],
            publishable.slug,
            form.cleaned_data["introduction"],
            form.cleaned_data["conclusion"],
            form.cleaned_data["msg_commit"],
        )
        logger.debug("slug consistency after repo update repo=%s db=%s",
                     versioned.slug, publishable.slug)
        # update relationships :
        publishable.sha_draft = sha

        publishable.subcategory.clear()
        for subcat in form.cleaned_data["subcategory"]:
            publishable.subcategory.add(subcat)

        publishable.save()

        self.success_url = reverse("content:view",
                                   args=[publishable.pk, publishable.slug])
        return super().form_valid(form)