diff --git a/assets/scss/layout/_content.scss b/assets/scss/layout/_content.scss index c9f3864278..e67f4372b2 100644 --- a/assets/scss/layout/_content.scss +++ b/assets/scss/layout/_content.scss @@ -21,8 +21,76 @@ margin-bottom: $length-10; >.content-thumbnail-group { + flex-grow: 0; flex-shrink: 0; + + display:block; + height: $length-64; + width: $length-64; + margin-right: $length-10; + + background-size: contain; + + >.thumbnail { + width: 100%; + height: 100%; + border: $length-1 solid $grey-200; + box-sizing: border-box; + } + + >.edit-thumbnail { + display: block; + width: $length-32; + height: $length-32; + margin-top: calc($length-64/2 - $length-32/2); + margin-left: calc($length-64/2 - $length-32/2); + border-radius: $radius-round; + background-color: rgba($grey-200, 0.7); + &.with-thumbnail { + position: relative; + top: -$length-64; + } + &.with-placeholder{ + position: relative; + top: 0; + } + &::after { + content: " "; + display: block; + width: $length-16; + height: $length-16; + position: relative; + top: calc($length-32/2 - $length-16/2); + left: calc($length-32/2 - $length-16/2); + @include sprite; + @include sprite-position($edit-blue); + background-repeat: no-repeat; + } + transition: border-radius .005s ease-in-out; + &:hover { + border-radius: 0; + width: 100%; + height: 100%; + margin: 0; + &::after { + top: calc($length-64/2 - $length-16/2); + left: calc($length-64/2 - $length-16/2); + } + } + } + + &.article-illu { + background-image: url("/static/images/article-illu.png"); + } + + &.tutorial-illu { + background-image: url("/static/images/tutorial-illu.png"); + } + + &.opinion-illu { + background-image: url("/static/images/opinion-illu.png"); + } } >.content-title-and-subtitle-group { diff --git a/templates/tutorialv2/events/descriptions.part.html b/templates/tutorialv2/events/descriptions.part.html index 360720290a..af203e3430 100644 --- a/templates/tutorialv2/events/descriptions.part.html +++ b/templates/tutorialv2/events/descriptions.part.html @@ -57,6 +57,9 @@ {% endif %} +{% elif event.type == "thumbnail_management" %} + {{ event.performer }} a modifié la miniature du contenu. + {% elif event.type == "tags_management" %} {{ event.performer }} a modifié les tags du contenu. diff --git a/templates/tutorialv2/includes/headline/title.part.html b/templates/tutorialv2/includes/headline/title.part.html index 22e89e8753..8ec2bfdeac 100644 --- a/templates/tutorialv2/includes/headline/title.part.html +++ b/templates/tutorialv2/includes/headline/title.part.html @@ -1,12 +1,25 @@ {% load i18n %} {% load crispy_forms_tags %} +{% load captureas %} +
- {% if show_thumbnail and content.image %} -
- + + {% if show_thumbnail %} +
+ {% if content.image %} + + {% endif %} + {% if show_form %} + {% captureas class_modifier %}{% if content.image %}with-thumbnail{% else %}with-placeholder{% endif %}{% endcaptureas %} + + {% trans "Modifier la miniature" %} + + {% crispy form_edit_thumbnail %} + {% endif %}
{% endif %} +
{% spaceless %} diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 153613fb69..63ed611bb9 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -108,29 +108,13 @@ def __init__(self, *args, **kwargs): class ContentForm(ContainerForm): - description = forms.CharField( - label=_("Description"), - max_length=PublishableContent._meta.get_field("description").max_length, - required=False, - ) - - image = forms.FileField( - label=_("Sélectionnez le logo du contenu (max. {} Ko).").format( - str(settings.ZDS_APP["gallery"]["image_max_size"] / 1024) - ), - validators=[with_svg_validator], - required=False, - ) - type = forms.ChoiceField(choices=TYPE_CHOICES, required=False) def _create_layout(self): self.helper.layout = Layout( IncludeEasyMDE(), Field("title"), - Field("description"), Field("type"), - Field("image"), Field("introduction", css_class="md-editor preview-source"), ButtonHolder( StrictButton(_("Aperçu"), type="preview", name="preview", css_class="btn btn-grey preview-btn"), @@ -163,19 +147,6 @@ def __init__(self, *args, **kwargs): if "type" in self.initial: self.helper["type"].wrap(Field, disabled=True) - def clean(self): - cleaned_data = super().clean() - image = cleaned_data.get("image", None) - if image is not None and image.size > settings.ZDS_APP["gallery"]["image_max_size"]: - self._errors["image"] = self.error_class( - [ - _("Votre logo est trop lourd, la limite autorisée est de {} Ko").format( - settings.ZDS_APP["gallery"]["image_max_size"] / 1024 - ) - ] - ) - return cleaned_data - class EditContentForm(ContentForm): title = None @@ -185,7 +156,6 @@ class EditContentForm(ContentForm): def _create_layout(self): self.helper.layout = Layout( IncludeEasyMDE(), - Field("image"), Field("introduction", css_class="md-editor preview-source"), StrictButton(_("Aperçu"), type="preview", name="preview", css_class="btn btn-grey preview-btn"), HTML( @@ -199,7 +169,6 @@ def _create_layout(self): with text=form.conclusion.value %}{% endif %}' ), Field("last_hash"), - Field("subcategory", template="crispy/checkboxselectmultiple.html"), Field("msg_commit"), ButtonHolder(StrictButton("Valider", type="submit")), ) diff --git a/zds/tutorialv2/models/events.py b/zds/tutorialv2/models/events.py index 0cfa001bc8..8617d105c1 100644 --- a/zds/tutorialv2/models/events.py +++ b/zds/tutorialv2/models/events.py @@ -13,6 +13,7 @@ from zds.tutorialv2.views.goals import EditGoals from zds.tutorialv2.views.labels import EditLabels from zds.tutorialv2.views.help import ChangeHelp +from zds.tutorialv2.views.thumbnail import EditThumbnailView from zds.tutorialv2.views.validations_contents import ( ReserveValidation, AskValidationForContent, @@ -48,6 +49,7 @@ signals.contributors_management: "contributors_management", signals.beta_management: "beta_management", signals.validation_management: "validation_management", + signals.thumbnail_management: "thumbnail_management", signals.tags_management: "tags_management", signals.canonical_link_management: "canonical_link_management", signals.goals_management: "goals_management", @@ -135,6 +137,15 @@ def record_event_validation_management(sender, performer, signal, content, versi ).save() +@receiver(signals.thumbnail_management, sender=EditThumbnailView) +def record_event_thumbnail_management(sender, performer, signal, content, **_): + Event.objects.create( + performer=performer, + type=types[signal], + content=content, + ) + + @receiver(signals.tags_management, sender=EditTags) def record_event_tags_management(sender, performer, signal, content, **_): Event( diff --git a/zds/tutorialv2/signals.py b/zds/tutorialv2/signals.py index 5815177dd6..f47dc535c6 100644 --- a/zds/tutorialv2/signals.py +++ b/zds/tutorialv2/signals.py @@ -30,6 +30,10 @@ # Action is either "request", "cancel", "accept", "reject", "revoke", "reserve" or "unreserve". validation_management = Signal() +# Thumbnail management +# For the signal below, the arguments "performer" and "content" shall be provided. +thumbnail_management = Signal() + # Tags management # For the signal below, the arguments "performer" and "content" shall be provided. tags_management = Signal() diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index cf7e9d31c3..14fe2429d9 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -232,8 +232,6 @@ def test_ensure_access(self): def test_basic_tutorial_workflow(self): """General test on the basic workflow of a tutorial: creation, edition, deletion for the author""" - - # login with author self.client.force_login(self.user_author) # create tutorial @@ -262,13 +260,10 @@ def test_basic_tutorial_workflow(self): reverse("content:create-content", kwargs={"created_content_type": "TUTORIAL"}), { "title": title, - "description": description, "introduction": intro, "conclusion": conclusion, "type": "TUTORIAL", "licence": self.licence.pk, - "subcategory": self.subcategory.pk, - "image": (settings.BASE_DIR / "fixtures" / "noir_black.png").open("rb"), }, follow=False, ) @@ -282,7 +277,6 @@ def test_basic_tutorial_workflow(self): self.assertEqual(Gallery.objects.filter(pk=tuto.gallery.pk).count(), 1) self.assertEqual(UserGallery.objects.filter(gallery__pk=tuto.gallery.pk).count(), tuto.authors.count()) - self.assertEqual(Image.objects.filter(gallery__pk=tuto.gallery.pk).count(), 1) # icon is uploaded # access to tutorial result = self.client.get(reverse("content:edit", args=[pk, slug]), follow=False) @@ -308,14 +302,11 @@ def test_basic_tutorial_workflow(self): "type": "TUTORIAL", "subcategory": self.subcategory.pk, "last_hash": versioned.compute_hash(), - "image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb"), }, follow=False, ) self.assertEqual(result.status_code, 302) - self.assertEqual(Image.objects.filter(gallery__pk=tuto.gallery.pk).count(), 2) # new icon is uploaded - tuto = PublishableContent.objects.get(pk=pk) self.assertEqual(tuto.licence, None) versioned = tuto.load_version() diff --git a/zds/tutorialv2/tests/tests_views/tests_editthumbnailview.py b/zds/tutorialv2/tests/tests_views/tests_editthumbnailview.py new file mode 100644 index 0000000000..0cf1dd8d28 --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/tests_editthumbnailview.py @@ -0,0 +1,126 @@ +from datetime import datetime +from unittest.mock import patch + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from django.utils.html import escape + +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents +from zds.tutorialv2.tests.factories import PublishableContentFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.views.thumbnail import EditThumbnailForm, EditThumbnailView + + +@override_for_contents() +class PermissionTests(TutorialTestMixin, TestCase): + """Test permissions and associated behaviors, such as redirections and status codes.""" + + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + + # Create a content + self.content = PublishableContentFactory(author_list=[self.author]) + + # Get information to be reused in tests + self.form_url = reverse("content:edit-thumbnail", kwargs={"pk": self.content.pk}) + thumbnail = (settings.BASE_DIR / "fixtures" / "logo.png").open("rb") + self.form_data = {"image": thumbnail} + self.content_data = {"pk": self.content.pk, "slug": self.content.slug} + self.content_url = reverse("content:view", kwargs=self.content_data) + self.login_url = reverse("member-login") + "?next=" + self.form_url + + def test_not_authenticated(self): + """Test that on form submission, unauthenticated users are redirected to the login page.""" + self.client.logout() # ensure no user is authenticated + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.login_url) + + def test_authenticated_author(self): + """Test that on form submission, authors are redirected to the content page.""" + self.client.force_login(self.author) + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff(self): + """Test that on form submission, staffs are redirected to the content page.""" + self.client.force_login(self.staff) + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_outsider(self): + """Test that on form submission, unauthorized users get a 403.""" + self.client.force_login(self.outsider) + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + +@override_for_contents() +class WorkflowTests(TutorialTestMixin, TestCase): + """Test the workflow of the form, such as validity errors and success messages.""" + + def setUp(self): + self.author = ProfileFactory() + self.content = PublishableContentFactory(author_list=[self.author.user]) + + # Get information to be reused in tests + self.form_url = reverse("content:edit-thumbnail", kwargs={"pk": self.content.pk}) + + self.form_error_messages = EditThumbnailForm.declared_fields["image"].error_messages + self.view_error_messages = EditThumbnailView.error_messages + self.success_message = EditThumbnailView.success_message + + # Log in with an authorized user (e.g the author of the content) + self.client.force_login(self.author.user) + + def get_test_cases(self): + good_thumbnail = (settings.BASE_DIR / "fixtures" / "logo.png").open("rb") + humongus_thumbnail = (settings.BASE_DIR / "fixtures" / "image_test.jpg").open("rb") + return { + "empty_form": {"inputs": {}, "expected_outputs": [self.form_error_messages["required"]]}, + "empty_fields": {"inputs": {"image": ""}, "expected_outputs": [self.form_error_messages["required"]]}, + "basic_success": {"inputs": {"image": good_thumbnail}, "expected_outputs": [self.success_message]}, + "file_too_large": { + "inputs": {"image": humongus_thumbnail}, + "expected_outputs": [self.form_error_messages["file_too_large"]], + }, + } + + def test_form_workflow(self): + test_cases = self.get_test_cases() + for case_name, case in test_cases.items(): + with self.subTest(msg=case_name): + response = self.client.post(self.form_url, case["inputs"], follow=True) + for msg in case["expected_outputs"]: + self.assertContains(response, escape(msg)) + + +@override_for_contents() +class FunctionalTests(TutorialTestMixin, TestCase): + """Test the detailed behavior of the feature, such as updates of the database or repositories.""" + + def setUp(self): + self.author = ProfileFactory() + self.content = PublishableContentFactory(author_list=[self.author.user]) + self.form_url = reverse("content:edit-thumbnail", kwargs={"pk": self.content.pk}) + self.form_data = {"image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb")} + + self.client.force_login(self.author.user) + + @patch("zds.tutorialv2.signals.thumbnail_management") + def test_normal(self, thumbnail_management): + self.assertEqual(self.content.title, self.content.gallery.title) + start_date = datetime.now() + self.assertTrue(self.content.update_date < start_date) + + response = self.client.post(self.form_url, self.form_data, follow=True) + self.assertEqual(response.status_code, 200) + + self.content.refresh_from_db() + + self.assertIsNotNone(self.content.image) + self.assertEqual(self.content.gallery.get_images().count(), 1) + self.assertEqual(thumbnail_management.send.call_count, 1) diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index 33da7aa20d..ee8cb71250 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -2044,37 +2044,25 @@ def test_social_cards_without_image(self): self.check_images_socials(result, "static/images/tutorial-illu", self.chapter1.title, self.tuto.description) def test_social_cards_with_image(self): - """ - Check that all cards are produce for socials network - """ - # connect with author: + """Check that all cards are produced for socials network""" self.client.force_login(self.user_author) - # add image to public tutorial + + # Add image to public tutorial self.client.post( - reverse("content:edit", args=[self.tuto.pk, self.tuto.slug]), - { - "title": self.tuto.title, - "description": self.tuto.description, - "introduction": "", - "conclusion": "", - "type": "TUTORIAL", - "licence": self.tuto.licence.pk, - "subcategory": self.subcategory.pk, - "last_hash": self.tuto.load_version().compute_hash(), - "image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb"), - }, + reverse("content:edit-thumbnail", args=[self.tuto.pk]), + {"image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb")}, follow=True, ) - # test access for guest user (bot of social network for example) + # Test as a guest (bot of social network for example) self.client.logout() - # check tutorial cards + # Check tutorial cards result = self.client.get(reverse("tutorial:view", kwargs={"pk": self.tuto.pk, "slug": self.tuto.slug})) self.assertEqual(result.status_code, 200) - self.check_images_socials(result, "media/galleries/", self.tuto.title, self.tuto.description) - # check part cards + + # Check part cards result = self.client.get( reverse( "tutorial:view-container", @@ -2082,10 +2070,9 @@ def test_social_cards_with_image(self): ) ) self.assertEqual(result.status_code, 200) - self.check_images_socials(result, "media/galleries/", self.part1.title, self.tuto.description) - # check chapter cards + # Check chapter cards result = self.client.get( reverse( "tutorial:view-container", @@ -2098,7 +2085,6 @@ def test_social_cards_with_image(self): ) ) self.assertEqual(result.status_code, 200) - self.check_images_socials(result, "media/galleries/", self.chapter1.title, self.tuto.description) def test_add_help_tuto(self): diff --git a/zds/tutorialv2/urls/urls_contents.py b/zds/tutorialv2/urls/urls_contents.py index cd9feff1f5..32cb55f869 100644 --- a/zds/tutorialv2/urls/urls_contents.py +++ b/zds/tutorialv2/urls/urls_contents.py @@ -10,6 +10,7 @@ EditTitle, EditSubtitle, ) +from zds.tutorialv2.views.thumbnail import EditThumbnailView from zds.tutorialv2.views.display.container import ContainerValidationView from zds.tutorialv2.views.display.content import ContentValidationView from zds.tutorialv2.views.events import EventsList @@ -215,16 +216,12 @@ def get_version_pages(): path("enlever-contributeur//", RemoveContributorFromContent.as_view(), name="remove-contributor"), path("ajouter-auteur//", AddAuthorToContent.as_view(), name="add-author"), path("enlever-auteur//", RemoveAuthorFromContent.as_view(), name="remove-author"), - # Modify the title and subtitle path("modifier-titre//", EditTitle.as_view(), name="edit-title"), path("modifier-sous-titre//", EditSubtitle.as_view(), name="edit-subtitle"), - # Modify the license + path("modifier-miniature//", EditThumbnailView.as_view(), name="edit-thumbnail"), path("modifier-licence//", EditContentLicense.as_view(), name="edit-license"), - # Modify the tags path("modifier-tags//", EditTags.as_view(), name="edit-tags"), - # Modify the canonical link path("modifier-lien-canonique/", EditCanonicalLinkView.as_view(), name="edit-canonical-link"), - # Modify the categories path("modifier-categories//", EditCategoriesView.as_view(), name="edit-categories"), # beta: path("activer-beta///", ManageBetaContent.as_view(action="set"), name="set-beta"), diff --git a/zds/tutorialv2/views/contents.py b/zds/tutorialv2/views/contents.py index 6ba8e9f4c9..21c9380110 100644 --- a/zds/tutorialv2/views/contents.py +++ b/zds/tutorialv2/views/contents.py @@ -16,7 +16,6 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DeleteView -from zds.gallery.mixins import ImageCreateMixin, NotAnImage from zds.gallery.models import Gallery from zds.member.decorator import LoggedWithReadWriteHability from zds.member.utils import get_bot_account @@ -69,43 +68,26 @@ def get_context_data(self, **kwargs): return context 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.creation_date = datetime.now() - - gallery = Gallery.objects.create( + self.content = PublishableContent( + title=form.cleaned_data["title"], + type=form.cleaned_data["type"], + licence=self.request.user.profile.licence, # Use the preferred license of the user if it exists + creation_date=datetime.now(), + ) + + self.content.gallery = Gallery.objects.create( title=self.content.title, slug=slugify(self.content.title), pubdate=datetime.now(), ) - # create image: - if "image" in self.request.FILES: - mixin = ImageCreateMixin() - mixin.gallery = gallery - try: - img = mixin.perform_create(str(_("Icône du contenu")), self.request.FILES["image"]) - except NotAnImage: - form.add_error("image", _("Image invalide")) - return super().form_invalid(form) - img.pubdate = datetime.now() - self.content.gallery = gallery - self.content.save() - if "image" in self.request.FILES: - 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.save() # Commit to database before updating relationships + # Update relationships + self.content.authors.add(self.request.user) self.content.ensure_author_gallery() - self.content.save() - # create a new repo : + # Create a new git repository init_new_repo( self.content, form.cleaned_data["introduction"], @@ -160,26 +142,6 @@ def form_valid(self, form): publishable.update_date = datetime.now() - # update image - if "image" in self.request.FILES: - gallery_defaults = { - "title": publishable.title, - "slug": slugify(publishable.title), - "pubdate": datetime.now(), - } - gallery, _created = Gallery.objects.get_or_create(pk=publishable.gallery.pk, defaults=gallery_defaults) - mixin = ImageCreateMixin() - mixin.gallery = gallery - try: - img = mixin.perform_create(str(_("Icône du contenu")), self.request.FILES["image"]) - except NotAnImage: - form.add_error("image", _("Image invalide")) - return super().form_invalid(form) - img.pubdate = datetime.now() - publishable.image = img - - publishable.save() - # now, update the versioned information sha = versioned.repo_update_top_container( publishable.title, @@ -188,10 +150,7 @@ def form_valid(self, form): form.cleaned_data["conclusion"], form.cleaned_data["msg_commit"], ) - - # update relationships : publishable.sha_draft = sha - publishable.save() self.success_url = reverse("content:view", args=[publishable.pk, publishable.slug]) diff --git a/zds/tutorialv2/views/display/content.py b/zds/tutorialv2/views/display/content.py index e70af96495..7f2ab0e384 100644 --- a/zds/tutorialv2/views/display/content.py +++ b/zds/tutorialv2/views/display/content.py @@ -40,6 +40,7 @@ ) from zds.tutorialv2.utils import last_participation_is_old, mark_read from zds.tutorialv2.views.contents import EditTitleForm, EditSubtitleForm +from zds.tutorialv2.views.thumbnail import EditThumbnailForm from zds.tutorialv2.views.display.config import ( ConfigForContentDraftView, ConfigForVersionView, @@ -141,6 +142,7 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["form_edit_title"] = EditTitleForm(self.versioned_object) context["form_edit_subtitle"] = EditSubtitleForm(self.versioned_object) + context["form_edit_thumbnail"] = EditThumbnailForm(self.versioned_object) context["display_config"] = ConfigForContentDraftView(self.request.user, self.object, self.versioned_object) return context diff --git a/zds/tutorialv2/views/thumbnail.py b/zds/tutorialv2/views/thumbnail.py new file mode 100644 index 0000000000..d2374056fc --- /dev/null +++ b/zds/tutorialv2/views/thumbnail.py @@ -0,0 +1,106 @@ +from datetime import datetime + +from crispy_forms.bootstrap import StrictButton +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Field +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.forms import forms, FileField +from django.shortcuts import redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from zds.gallery.mixins import ImageCreateMixin, NotAnImage +from zds.gallery.models import Gallery +from zds.tutorialv2 import signals +from zds.tutorialv2.mixins import SingleContentFormViewMixin +from zds.tutorialv2.models.database import PublishableContent +from zds.utils import get_current_user +from zds.utils.uuslug_wrapper import slugify + +from zds.utils.validators import with_svg_validator + + +class EditThumbnailForm(forms.Form): + image = FileField( + label=_("Sélectionnez la miniature de la publication (max. {} ko).").format( + settings.ZDS_APP["gallery"]["image_max_size"] // 1024 + ), + validators=[with_svg_validator], + required=True, + error_messages={ + "file_too_large": _("La miniature est trop lourde ; la limite autorisée est de {} ko").format( + settings.ZDS_APP["gallery"]["image_max_size"] // 1024 + ), + }, + ) + + def __init__(self, versioned_content, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + self.helper.form_method = "post" + self.helper.form_action = reverse("content:edit-thumbnail", kwargs={"pk": versioned_content.pk}) + self.helper.form_id = "edit-thumbnail" + self.helper.form_class = "modal modal-flex" + + self.helper.layout = Layout( + Field("image"), + StrictButton("Valider", type="submit"), + ) + + self.previous_page_url = reverse( + "content:view", kwargs={"pk": versioned_content.pk, "slug": versioned_content.slug} + ) + + def clean_image(self): + image = self.cleaned_data.get("image", None) + if image is not None and image.size > settings.ZDS_APP["gallery"]["image_max_size"]: + self.add_error("image", self.declared_fields["image"].error_messages["file_too_large"]) + return self.cleaned_data + + +class EditThumbnailView(LoginRequiredMixin, SingleContentFormViewMixin): + modal_form = True + model = PublishableContent + form_class = EditThumbnailForm + success_message = _("La miniature a bien été changée.") + error_messages = {"invalid_image": _("Image invalide")} + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["versioned_content"] = self.versioned_object + return kwargs + + def form_valid(self, form): + publishable = self.object + + # Get or create gallery + gallery_defaults = { + "title": publishable.title, + "slug": slugify(publishable.title), + "pubdate": datetime.now(), + } + gallery, _created = Gallery.objects.get_or_create(pk=publishable.gallery.pk, defaults=gallery_defaults) + publishable.gallery = gallery + + # Create image + mixin = ImageCreateMixin() + mixin.gallery = gallery + try: + thumbnail = mixin.perform_create(str(_("Icône du contenu")), self.request.FILES["image"]) + except NotAnImage: + form.add_error("image", self.error_messages["invalid_image"]) + return super().form_invalid(form) + thumbnail.pubdate = datetime.now() + + # Update thumbnail + publishable.image = thumbnail + publishable.save() + + messages.success(self.request, self.success_message) + + signals.thumbnail_management.send(sender=self.__class__, performer=get_current_user(), content=publishable) + + return redirect(form.previous_page_url)