Skip to content

Commit

Permalink
Modifie le lien canonique dans un formulaire dédié (#6611)
Browse files Browse the repository at this point in the history
* Supprime le lien canonique du formulaire de contenu
* Ajoute un formulaire dédié au lien canonique
* Formulaire modal spécifique
* Enregistrement dans le journal d'événements
* Ajoute des tests
  • Loading branch information
Arnaud-D authored Apr 28, 2024
1 parent f98b5b7 commit 19f13c8
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 21 deletions.
2 changes: 2 additions & 0 deletions templates/tutorialv2/events/descriptions.part.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
{% elif event.type == "tags_management" %}
<a href="{{ performer_href }}">{{ event.performer }}</a> a modifié les tags du contenu.

{% elif event.type == "canonical_link_management" %}
<a href="{{ performer_href }}">{{ event.performer }}</a> a modifié le lien canonique du contenu.

{% elif event.type == "goals_management" %}
<a href="{{ performer_href }}">{{ event.performer }}</a> a modifié les objectifs du contenu.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ <h3>Éditorialisation</h3>
{% crispy form_edit_tags %}
</li>

<li>
<a href="#edit-canonical-link" class="open-modal ico-after gear blue">
{% trans "Modifier le lien canonique" %}
</a>
{% crispy form_edit_canonical_link %}
</li>

{% if perms.tutorialv2.change_publishablecontent %}
<li>
<a href="#edit-goals" class="open-modal ico-after gear blue">
Expand Down
19 changes: 3 additions & 16 deletions zds/tutorialv2/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,6 @@ class ContentForm(ContainerForm):

type = forms.ChoiceField(choices=TYPE_CHOICES, required=False)

source = forms.URLField(
label=_(
"""Si votre contenu est publié en dehors de Zeste de Savoir (blog, site personnel, etc.),
indiquez le lien de la publication originale : """
),
max_length=PublishableContent._meta.get_field("source").max_length,
required=False,
widget=forms.TextInput(attrs={"placeholder": _("https://...")}),
)

def _create_layout(self):
self.helper.layout = Layout(
IncludeEasyMDE(),
Expand All @@ -158,12 +148,10 @@ def _create_layout(self):
with text=form.conclusion.value %}{% endif %}'
),
Field("last_hash"),
Field("source"),
Field("msg_commit"),
ButtonHolder(StrictButton("Valider", type="submit")),
)

self.helper.layout.append(Field("msg_commit"))
self.helper.layout.append(ButtonHolder(StrictButton("Valider", type="submit")))

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -211,10 +199,9 @@ def _create_layout(self):
with text=form.conclusion.value %}{% endif %}'
),
Field("last_hash"),
Field("source"),
Field("subcategory", template="crispy/checkboxselectmultiple.html"),
Field("msg_commit"),
StrictButton("Valider", type="submit"),
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 @@ -6,6 +6,7 @@
from zds.tutorialv2 import signals
from zds.tutorialv2.views.authors import AddAuthorToContent, RemoveAuthorFromContent
from zds.tutorialv2.views.beta import ManageBetaContent
from zds.tutorialv2.views.canonical import EditCanonicalLinkView
from zds.tutorialv2.views.contributors import AddContributorToContent, RemoveContributorFromContent
from zds.tutorialv2.views.suggestions import AddSuggestion, RemoveSuggestion
from zds.tutorialv2.views.tags import EditTags
Expand Down Expand Up @@ -48,6 +49,7 @@
signals.beta_management: "beta_management",
signals.validation_management: "validation_management",
signals.tags_management: "tags_management",
signals.canonical_link_management: "canonical_link_management",
signals.goals_management: "goals_management",
signals.labels_management: "labels_management",
signals.suggestions_management: "suggestions_management",
Expand Down Expand Up @@ -142,6 +144,15 @@ def record_event_tags_management(sender, performer, signal, content, **_):
).save()


@receiver(signals.canonical_link_management, sender=EditCanonicalLinkView)
def record_event_canonical_link_management(sender, performer, signal, content, **_):
Event(
performer=performer,
type=types[signal],
content=content,
).save()


@receiver(signals.suggestions_management, sender=AddSuggestion)
@receiver(signals.suggestions_management, sender=RemoveSuggestion)
def record_event_suggestion_management(sender, performer, signal, content, action, **_):
Expand Down
4 changes: 4 additions & 0 deletions zds/tutorialv2/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
# For the signal below, the arguments "performer" and "content" shall be provided.
tags_management = Signal()

# Canonical link management
# For the signal below, the arguments "performer" and "content" shall be provided.
canonical_link_management = Signal()

# Suggestions management
# For the signal below, the arguments "performer" and "content" shall be provided.
# Action is either "add" or "remove".
Expand Down
121 changes: 121 additions & 0 deletions zds/tutorialv2/tests/tests_views/tests_editcanonicallinkview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from unittest.mock import patch

from django.test import TestCase
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _

from zds.tutorialv2.views.canonical import EditCanonicalLinkForm, EditCanonicalLinkView
from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents
from zds.tutorialv2.tests.factories import PublishableContentFactory
from zds.member.tests.factories import ProfileFactory, StaffProfileFactory


@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-canonical-link", kwargs={"pk": self.content.pk})
self.form_data = {"source": "https://example.com"}
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):
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):
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):
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):
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):
# Create a user
self.author = ProfileFactory()

# Create a content
self.content = PublishableContentFactory(author_list=[self.author.user])

# Get information to be reused in tests
self.form_url = reverse("content:edit-canonical-link", kwargs={"pk": self.content.pk})
self.error_messages = EditCanonicalLinkForm.declared_fields["source"].error_messages
self.error_messages["too_long"] = _("Assurez-vous que cette valeur comporte au plus")
self.success_message = EditCanonicalLinkView.success_message

# Log in with an authorized user (e.g the author of the content) to perform the tests
self.client.force_login(self.author.user)

def get_test_cases(self):
return {
"no_field": {"inputs": {}, "expected_outputs": [self.success_message]},
"empty": {"inputs": {"source": ""}, "expected_outputs": [self.success_message]},
"valid_1": {"inputs": {"source": "example.com"}, "expected_outputs": [self.success_message]},
"valid_2": {"inputs": {"source": "https://example.com"}, "expected_outputs": [self.success_message]},
"invalid": {"inputs": {"source": "invalid_url"}, "expected_outputs": [self.error_messages["invalid"]]},
}

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-canonical-link", kwargs={"pk": self.content.pk})

# Log in with an authorized user (e.g the author of the content) to perform the tests
self.client.force_login(self.author.user)

@patch("zds.tutorialv2.signals.canonical_link_management")
def test_normal(self, canonical_link_management):
valid_url = "https://example.com"
self.client.post(self.form_url, data={"source": valid_url}, follow=True)
expected = {"source": valid_url, "call_count": 1}
self.check_effects(expected, canonical_link_management)

@patch("zds.tutorialv2.signals.canonical_link_management")
def test_empty(self, canonical_link_management):
self.client.post(self.form_url, data={"source": ""}, follow=True)
expected = {"source": "", "call_count": 1}
self.check_effects(expected, canonical_link_management)

def check_effects(self, expected_outputs, canonical_link_management):
self.content.refresh_from_db()
self.assertEqual(self.content.source, expected_outputs["source"])
self.assertEqual(canonical_link_management.send.call_count, expected_outputs["call_count"])
3 changes: 3 additions & 0 deletions zds/tutorialv2/urls/urls_contents.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.urls import path
from django.views.generic.base import RedirectView

from zds.tutorialv2.views.canonical import EditCanonicalLinkView
from zds.tutorialv2.views.categories import EditCategoriesView
from zds.tutorialv2.views.contents import (
CreateContent,
Expand Down Expand Up @@ -221,6 +222,8 @@ def get_version_pages():
path("modifier-licence/<int:pk>/", EditContentLicense.as_view(), name="edit-license"),
# Modify the tags
path("modifier-tags/<int:pk>/", EditTags.as_view(), name="edit-tags"),
# Modify the canonical link
path("modifier-lien-canonique/<int:pk>", EditCanonicalLinkView.as_view(), name="edit-canonical-link"),
# Modify the categories
path("modifier-categories/<int:pk>/", EditCategoriesView.as_view(), name="edit-categories"),
# beta:
Expand Down
76 changes: 76 additions & 0 deletions zds/tutorialv2/views/canonical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from crispy_forms.bootstrap import StrictButton
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field, ButtonHolder
from django import forms
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

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


class EditCanonicalLinkForm(forms.Form):
source = forms.URLField(
label=_(
"""Si votre contenu est publié en dehors de Zeste de Savoir (blog, site personnel, etc.),
indiquez le lien de la publication originale :"""
),
max_length=PublishableContent._meta.get_field("source").max_length,
required=False,
widget=forms.TextInput(attrs={"placeholder": _("https://...")}),
error_messages={"invalid": _("Entrez un lien valide.")},
)

def __init__(self, content, *args, **kwargs):
kwargs["initial"] = {"source": content.source}
super().__init__(*args, **kwargs)

self.helper = FormHelper()
self.helper.form_method = "post"
self.helper.form_action = reverse("content:edit-canonical-link", kwargs={"pk": content.pk})
self.helper.form_class = "modal modal-flex"
self.helper.form_id = "edit-canonical-link"

self.helper.layout = Layout(
Field("source"),
ButtonHolder(
StrictButton(_("Valider"), type="submit"),
),
)

self.previous_page_url = reverse("content:view", kwargs={"pk": content.pk, "slug": content.slug})


class EditCanonicalLinkView(LoginRequiredMixin, SingleContentFormViewMixin):
model = PublishableContent
form_class = EditCanonicalLinkForm
success_message = _("Le lien canonique a bien été modifié.")
modal_form = True
http_method_names = ["post"]

def dispatch(self, request, *args, **kwargs):
content = get_object_or_404(PublishableContent, pk=self.kwargs["pk"])
success_url_kwargs = {"pk": content.pk, "slug": content.slug}
self.success_url = reverse("content:view", kwargs=success_url_kwargs)
return super().dispatch(request, *args, **kwargs)

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["content"] = self.object
return kwargs

def form_invalid(self, form):
form.previous_page_url = self.success_url
return super().form_invalid(form)

def form_valid(self, form):
self.object.source = form.cleaned_data["source"]
self.object.save()
messages.success(self.request, self.success_message)
signals.canonical_link_management.send(sender=self.__class__, performer=get_current_user(), content=self.object)
return super().form_valid(form)
5 changes: 0 additions & 5 deletions zds/tutorialv2/views/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ def form_valid(self, form):
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()

gallery = Gallery.objects.create(
Expand Down Expand Up @@ -132,7 +131,6 @@ def get_initial(self):

initial["introduction"] = versioned.get_introduction()
initial["conclusion"] = versioned.get_conclusion()
initial["source"] = versioned.source
initial["subcategory"] = self.object.subcategory.all()
initial["last_hash"] = versioned.compute_hash()

Expand Down Expand Up @@ -160,9 +158,6 @@ def form_valid(self, form):
messages.error(self.request, _("Une nouvelle version a été postée avant que vous ne validiez."))
return self.form_invalid(form)

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

publishable.update_date = datetime.now()

# update image
Expand Down
2 changes: 2 additions & 0 deletions zds/tutorialv2/views/display/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
UnpickOpinionForm,
PromoteOpinionToArticleForm,
)
from zds.tutorialv2.views.canonical import EditCanonicalLinkForm
from zds.tutorialv2.views.contributors import ContributionForm
from zds.tutorialv2.views.suggestions import SearchSuggestionForm
from zds.tutorialv2.views.licence import EditContentLicenseForm
Expand Down Expand Up @@ -101,6 +102,7 @@ def get_context_data(self, **kwargs):
context["form_convert"] = PromoteOpinionToArticleForm(self.versioned_object, initial=data_form_convert)
context["form_warn_typo"] = WarnTypoForm(self.versioned_object, self.versioned_object)
context["form_edit_tags"] = EditTagsForm(self.versioned_object, self.object)
context["form_edit_canonical_link"] = EditCanonicalLinkForm(self.object)
context["form_edit_goals"] = EditGoalsForm(self.object)
context["form_edit_labels"] = EditLabelsForm(self.object)
context["is_antispam"] = self.object.antispam(self.request.user)
Expand Down

0 comments on commit 19f13c8

Please sign in to comment.