Skip to content

Commit

Permalink
Déplace la modification de la miniature dans un formulaire dédié (#6613)
Browse files Browse the repository at this point in the history
* Transforme la miniature de tutoriel en bouton
* Ajoute le formulaire pour modifier la miniature
* Corrige les tests existants
* Ajoute des tests pour la vue des miniatures
* Ajoute l'inscription dans le journal d'événement
* Change le style du bouton d'image
* Cache l'image par défaut pour les fonds transparents
  • Loading branch information
Arnaud-D authored Sep 15, 2024
1 parent 29b75e0 commit 75989e6
Show file tree
Hide file tree
Showing 13 changed files with 360 additions and 125 deletions.
68 changes: 68 additions & 0 deletions assets/scss/layout/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions templates/tutorialv2/events/descriptions.part.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
{% endif %}


{% elif event.type == "thumbnail_management" %}
<a href="{{ performer_href }}">{{ event.performer }}</a> a modifié la miniature du contenu.

{% elif event.type == "tags_management" %}
<a href="{{ performer_href }}">{{ event.performer }}</a> a modifié les tags du contenu.

Expand Down
19 changes: 16 additions & 3 deletions templates/tutorialv2/includes/headline/title.part.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
{% load i18n %}
{% load crispy_forms_tags %}
{% load captureas %}


<div class="title-block">
{% if show_thumbnail and content.image %}
<div class="content-thumbnail-group">
<img class="thumbnail" src="{{ content.image.physical.tutorial_illu.url }}" alt="" itemprop="thumbnailUrl">

{% if show_thumbnail %}
<div class="content-thumbnail-group {% if not content.image %}{{ content.type|lower }}-illu{% endif %}">
{% if content.image %}
<img class="thumbnail" src="{{ content.image.physical.tutorial_illu.url }}" alt="" itemprop="thumbnailUrl">
{% endif %}
{% if show_form %}
{% captureas class_modifier %}{% if content.image %}with-thumbnail{% else %}with-placeholder{% endif %}{% endcaptureas %}
<a href="#edit-thumbnail" class="open-modal edit-thumbnail {{ class_modifier }}" title="{% trans "Modifier la miniature" %}">
<span class="visuallyhidden">{% trans "Modifier la miniature" %}</span>
</a>
{% crispy form_edit_thumbnail %}
{% endif %}
</div>
{% endif %}

<div class="content-title-and-subtitle-group">
<div class="content-title-group">
{% spaceless %}
Expand Down
31 changes: 0 additions & 31 deletions zds/tutorialv2/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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")),
)
Expand Down
11 changes: 11 additions & 0 deletions zds/tutorialv2/models/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions zds/tutorialv2/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 0 additions & 9 deletions zds/tutorialv2/tests/tests_views/tests_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand All @@ -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)
Expand All @@ -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()
Expand Down
126 changes: 126 additions & 0 deletions zds/tutorialv2/tests/tests_views/tests_editthumbnailview.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 75989e6

Please sign in to comment.