Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Déplace la modification de la miniature dans un formulaire dédié #6613

Merged
merged 7 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
philippemilink marked this conversation as resolved.
Show resolved Hide resolved
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